💥 Done (almost)

This commit is contained in:
user
2025-10-21 17:29:16 +03:00
parent 8a169f84cc
commit 88d430a15e
9 changed files with 149 additions and 169 deletions

View File

@@ -1,7 +1,12 @@
import { db } from "@pkg/db";
import { Logger } from "@pkg/logger";
import type { Result } from "@pkg/result";
import type { CreateCustomerInfoPayload, CustomerInfoModel } from "./data";
import type { CustomerInfoRepository } from "./repository";
import type {
CreateCustomerInfoPayload,
CustomerInfoModel,
UpdateCustomerInfoPayload,
} from "./data";
import { CustomerInfoRepository } from "./repository";
/**
* CustomerInfoController handles business logic for customer information operations.
@@ -44,6 +49,28 @@ export class CustomerInfoController {
Logger.info("Retrieving all customer info records");
return this.repo.getAllCustomerInfo();
}
/**
* Updates existing customer information
* @param payload - Customer information data to update (must include id)
* @returns Result containing success boolean or error
*/
async updateCustomerInfo(
payload: UpdateCustomerInfoPayload,
): Promise<Result<boolean>> {
Logger.info(`Updating customer info with ID: ${payload.id}`);
return this.repo.updateCustomerInfo(payload);
}
/**
* Deletes customer information by ID
* @param id - Customer info ID to delete
* @returns Result containing success boolean or error
*/
async deleteCustomerInfo(id: number): Promise<Result<boolean>> {
Logger.info(`Deleting customer info with ID: ${id}`);
return this.repo.deleteCustomerInfo(id);
}
}
export function getCustomerInfoController() {

View File

@@ -1,10 +1,9 @@
import { SessionOutcome } from "$lib/domains/ckflow/data/entities";
import { getCKUseCases } from "$lib/domains/ckflow/domain/usecases";
import { getCustomerInfoController } from "$lib/domains/customerinfo/controller";
import { EmailerUseCases } from "$lib/domains/email/domain/usecases";
import {
CheckoutStep,
createOrderPayloadModel,
OrderCreationStep,
} from "$lib/domains/order/data/entities";
import { PaymentInfoRepository } from "$lib/domains/paymentinfo/data/repository";
import { PaymentInfoUseCases } from "$lib/domains/paymentinfo/domain/usecases";
@@ -12,66 +11,104 @@ import { getProductUseCases } from "$lib/domains/product/usecases";
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
import { db } from "@pkg/db";
import { getError, Logger } from "@pkg/logger";
import { ERROR_CODES } from "@pkg/result";
import { ERROR_CODES, type Result } from "@pkg/result";
import { z } from "zod";
import { OrderRepository } from "../data/repository";
import { OrderController } from "./controller";
export const orderRouter = createTRPCRouter({
/**
* Creates a new order for product checkout
* Handles customer info creation, payment info creation, and order creation
*/
createOrder: publicProcedure
.input(createOrderPayloadModel)
.mutation(async ({ input }) => {
const pduc = new PaymentInfoUseCases(new PaymentInfoRepository(db));
const oc = new OrderController(new OrderRepository(db));
const cc = getCustomerInfoController();
const puc = getProductUseCases();
const emailUC = new EmailerUseCases();
const paymentInfoUC = new PaymentInfoUseCases(
new PaymentInfoRepository(db),
);
const orderController = new OrderController(new OrderRepository(db));
const customerInfoController = getCustomerInfoController();
const productUC = getProductUseCases();
const ftRes = await tc.uncacheAndSaveTicket(input.flightTicketId!);
if (ftRes.error || !ftRes.data) {
return { error: ftRes.error };
}
if (!input.flightTicketId || !input.paymentInfo) {
// Validate required inputs
if (!input.productId || !input.customerInfo) {
return {
error: getError({
code: ERROR_CODES.INPUT_ERROR,
message: "Received invalid input",
detail: "The entered data is incomplete or invalid",
userHint: "Enter valid order data to complete the order",
message: "Missing required order information",
detail: "Product ID and customer information are required",
userHint:
"Please ensure product and customer information are provided",
}),
};
}
const pdRes = await pduc.createPaymentInfo(input.paymentInfo!);
if (pdRes.error || !pdRes.data) {
return { error: pdRes.error };
} as Result<string>;
}
Logger.info(`Setting flight ticket info id ${ftRes.data}`);
input.orderModel.flightTicketInfoId = ftRes.data;
Logger.info(`Setting payment details id ${pdRes.data}`);
input.orderModel.paymentInfoId = pdRes.data;
Logger.info("Creating order");
const out = await oc.createOrder(input.orderModel);
if (out.error || !out.data) {
await pduc.deletePaymentInfo(pdRes.data!);
return { error: out.error };
// Verify product exists
Logger.info(`Verifying product with ID: ${input.productId}`);
const productRes = await productUC.getProductById(input.productId);
if (productRes.error || !productRes.data) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Product not found",
detail: `Product with ID ${input.productId} does not exist`,
userHint: "Please select a valid product",
}),
} as Result<string>;
}
Logger.info(`Creating customer infos with oid: ${out.data}`);
const pOut = await pc.createPassengerInfos(
input.passengerInfos,
out.data.id,
ftRes.data!,
pdRes.data,
// Create customer information
Logger.info("Creating customer information");
const customerRes = await customerInfoController.createCustomerInfo(
input.customerInfo,
);
if (pOut.error) {
await oc.deleteOrder(out.data.id);
return { error: pOut.error };
if (customerRes.error || !customerRes.data) {
return { error: customerRes.error } as Result<string>;
}
const customerInfoId = customerRes.data;
Logger.info(`Customer info created with ID: ${customerInfoId}`);
// Create payment information if provided
let paymentInfoId: number | undefined = undefined;
if (input.paymentInfo) {
Logger.info("Creating payment information");
const paymentRes = await paymentInfoUC.createPaymentInfo(
input.paymentInfo,
);
if (paymentRes.error || !paymentRes.data) {
// Cleanup customer info on payment failure
await customerInfoController.deleteCustomerInfo(
customerInfoId,
);
return { error: paymentRes.error } as Result<string>;
}
paymentInfoId = paymentRes.data;
Logger.info(`Payment info created with ID: ${paymentInfoId}`);
}
// Set IDs in order model
input.orderModel.productId = input.productId;
input.orderModel.customerInfoId = customerInfoId;
input.orderModel.paymentInfoId = paymentInfoId;
// Create the order
Logger.info("Creating order");
const orderRes = await orderController.createOrder(input.orderModel);
if (orderRes.error || !orderRes.data) {
// Cleanup on order creation failure
if (paymentInfoId) {
await paymentInfoUC.deletePaymentInfo(paymentInfoId);
}
await customerInfoController.deleteCustomerInfo(customerInfoId);
return { error: orderRes.error } as Result<string>;
}
const orderId = orderRes.data.id;
const orderUID = orderRes.data.uid;
Logger.info(`Order created successfully with ID: ${orderId}`);
// Update checkout flow state if flowId is provided
if (input.flowId) {
Logger.info(
`Updating checkout flow state for flow ${input.flowId}`,
@@ -79,7 +116,7 @@ export const orderRouter = createTRPCRouter({
try {
await getCKUseCases().cleanupFlow(input.flowId, {
sessionOutcome: SessionOutcome.COMPLETED,
checkoutStep: CheckoutStep.Complete,
checkoutStep: OrderCreationStep.SUMMARY,
});
Logger.info(
`Checkout flow ${input.flowId} marked as completed`,
@@ -90,94 +127,29 @@ export const orderRouter = createTRPCRouter({
);
Logger.error(err);
}
} else {
Logger.warn(
`No flow id found to mark as completed: ${input.flowId}`,
);
}
const uid = out.data.uid;
Logger.debug(
"Rotating the link id for the product now that it's been used",
);
if (!uid) {
return out;
}
try {
// Get order details for email
const orderDetails = await oc.getOrderByPNR(uid);
const out = await productUC.refreshProductLinkId(input.productId);
if (!orderDetails.data) {
return out;
}
const order = orderDetails.data;
const ticketInfo = order.flightTicketInfo;
const primaryPassenger = order.passengerInfos[0]?.passengerPii!;
Logger.debug(
`Product link id rotated: ${JSON.stringify(out, null, 2)}`,
);
if (!ticketInfo || !primaryPassenger) {
Logger.warn(
`No email address found for passenger to send PNR confirmation`,
);
return out;
}
// Get passenger email address to send confirmation to
const passengerEmail = primaryPassenger.email;
if (!passengerEmail) {
Logger.warn(
`No email address found for passenger to send PNR confirmation`,
);
return out;
}
Logger.info(
`Sending PNR confirmation email to ${passengerEmail}`,
);
// Send the email with React component directly
const emailResult = await emailUC.sendEmailWithTemplate({
to: passengerEmail,
subject: `Flight Confirmation: ${ticketInfo.departure} to ${ticketInfo.arrival} - PNR: ${uid}`,
template: "uid-confirmation",
templateData: {
uid: uid,
origin: ticketInfo.departure,
destination: ticketInfo.arrival,
departureDate: new Date(
ticketInfo.departureDate,
).toISOString(),
returnDate: new Date(
ticketInfo.returnDate,
)?.toISOString(),
passengerName: `${primaryPassenger.firstName}`,
baseUrl: "https://FlyTicketTravel.com",
logoPath:
"https://FlyTicketTravel.com/assets/logos/logo-main.svg",
companyName: "FlyTicketTravel",
},
});
if (emailResult.error) {
Logger.error(
`Failed to send PNR confirmation email: ${emailResult.error.message}`,
);
} else {
Logger.info(
`PNR confirmation email sent to ${passengerEmail}`,
);
}
} catch (emailError) {
// Don't fail the order if email sending fails
Logger.error(
`Error sending PNR confirmation email: ${emailError}`,
);
Logger.error(emailError);
}
Logger.info("Done with order creation, returning");
return out;
Logger.info("Order creation completed successfully");
return { data: orderUID } as Result<string>;
}),
findByUID: publicProcedure
/**
* Finds an order by its ID
*/
findByIdUID: publicProcedure
.input(z.object({ uid: z.string() }))
.query(async ({ input }) => {
const oc = new OrderController(new OrderRepository(db));
return oc.getOrderByUID(input.uid);
const orderController = new OrderController(new OrderRepository(db));
return orderController.getOrderByUID(input.uid);
}),
});

View File

@@ -119,8 +119,6 @@ export class CreateOrderViewModel {
orderModel: {
...priceDetails,
productId: product.id,
customerInfoId: customerInfoVM.customerInfo.id,
paymentInfoId: undefined,
},
flowId: ckFlowVM.flowId,
});
@@ -160,7 +158,7 @@ export class CreateOrderViewModel {
// Redirect to success page after a short delay
setTimeout(() => {
window.location.href = `/order/success?id=${out.data}`;
window.location.href = `/order/success?uid=${out.data}`;
}, 1000);
return true;

View File

@@ -23,7 +23,7 @@
description: "Preparing checkout session",
});
window.location.replace(
`/checkout/${$checkoutSessionIdStore}/${data.data.id}`,
`/checkout/${$checkoutSessionIdStore}/${data.data.linkId}`,
);
}, 3000);
});

View File

@@ -2,7 +2,5 @@ import { getProductUseCases } from "$lib/domains/product/usecases";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params }) => {
return await getProductUseCases().getProductByLinkId(
Number(params.plid ?? "-1"),
);
return await getProductUseCases().getProductByLinkId(params.plid);
};

View File

@@ -2,7 +2,6 @@
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import CheckoutConfirmationSection from "$lib/domains/checkout/checkout-confirmation-section.svelte";
import CheckoutLoadingSection from "$lib/domains/checkout/checkout-loading-section.svelte";
import CheckoutStepsIndicator from "$lib/domains/checkout/checkout-steps-indicator.svelte";
@@ -51,10 +50,8 @@
<div class="grid h-full min-h-screen w-full place-items-center">
<div class="flex flex-col items-center justify-center gap-4">
<Icon icon={SearchIcon} cls="w-12 h-12" />
<Title size="h4" color="black">No ticket found</Title>
<Button href="/search" variant="default">
Search for another ticket
</Button>
<Title size="h4" color="black">Product not found</Title>
<p>Something went wrong, please try again or contact us</p>
</div>
</div>
{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation}

View File

@@ -3,6 +3,7 @@
import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
import CheckIcon from "~icons/ic/round-check";
// Maybe todo? if the `uid` search param is present, do something?? figure out later
</script>
<div class="grid min-h-[80vh] w-full place-items-center px-4 sm:px-6">

View File

@@ -3,11 +3,8 @@
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import CloseIcon from "~icons/mdi/window-close";
import ArrowRightIcon from "~icons/solar/multiple-forward-right-line-duotone";
import RestartIcon from "~icons/material-symbols/restart-alt-rounded";
import { onMount } from "svelte";
import CloseIcon from "~icons/mdi/window-close";
let canRedirect = $state(false);
@@ -37,25 +34,9 @@
<Title size="h3" center weight="medium">Session Terminated</Title>
<p class="w-full max-w-prose text-center text-gray-600">
Unfortunately, your session has been terminated due to inactivity
or expiration of the booking.
or something went wrong, please contact us to walk through the
steps again.
</p>
<div class="flex w-full flex-col justify-center gap-4 md:flex-row">
{#if canRedirect}
<Button
variant="default"
size="lg"
href={`/checkout/${sid}/${tid}`}
>
<Icon icon={RestartIcon} cls="w-auto h-6" />
<span>Try Again</span>
</Button>
{/if}
<Button variant="defaultInverted" size="lg" href="/search">
<Icon icon={ArrowRightIcon} cls="w-auto h-6" />
<span>Back To Search</span>
</Button>
</div>
</div>
</MaxWidthWrapper>
</div>