From 88d430a15e27438661c55260ca8b856b3c04d2f7 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 21 Oct 2025 17:29:16 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20Done=20(almost)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/domains/customerinfo/controller.ts | 31 ++- .../src/lib/domains/order/domain/router.ts | 218 ++++++++---------- .../view/create/create.order.vm.svelte.ts | 4 +- .../src/routes/(main)/[pageid]/+page.svelte | 2 +- .../checkout/[sid]/[plid]/+page.server.ts | 4 +- .../(main)/checkout/[sid]/[plid]/+page.svelte | 7 +- .../(main)/checkout/success/+page.svelte | 1 + .../(main)/checkout/terminated/+page.svelte | 25 +- packages/logic/domains/order/data/entities.ts | 26 ++- 9 files changed, 149 insertions(+), 169 deletions(-) diff --git a/apps/frontend/src/lib/domains/customerinfo/controller.ts b/apps/frontend/src/lib/domains/customerinfo/controller.ts index 11b24d6..6c996ac 100644 --- a/apps/frontend/src/lib/domains/customerinfo/controller.ts +++ b/apps/frontend/src/lib/domains/customerinfo/controller.ts @@ -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> { + 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> { + Logger.info(`Deleting customer info with ID: ${id}`); + return this.repo.deleteCustomerInfo(id); + } } export function getCustomerInfoController() { diff --git a/apps/frontend/src/lib/domains/order/domain/router.ts b/apps/frontend/src/lib/domains/order/domain/router.ts index fd615f5..96c2163 100644 --- a/apps/frontend/src/lib/domains/order/domain/router.ts +++ b/apps/frontend/src/lib/domains/order/domain/router.ts @@ -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; } - 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; } - 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; + } + 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; + } + 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; + } + + 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; }), - 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); }), }); diff --git a/apps/frontend/src/lib/domains/order/view/create/create.order.vm.svelte.ts b/apps/frontend/src/lib/domains/order/view/create/create.order.vm.svelte.ts index 0bb7768..bf6a56e 100644 --- a/apps/frontend/src/lib/domains/order/view/create/create.order.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/order/view/create/create.order.vm.svelte.ts @@ -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; diff --git a/apps/frontend/src/routes/(main)/[pageid]/+page.svelte b/apps/frontend/src/routes/(main)/[pageid]/+page.svelte index 50f418a..b69a606 100644 --- a/apps/frontend/src/routes/(main)/[pageid]/+page.svelte +++ b/apps/frontend/src/routes/(main)/[pageid]/+page.svelte @@ -23,7 +23,7 @@ description: "Preparing checkout session", }); window.location.replace( - `/checkout/${$checkoutSessionIdStore}/${data.data.id}`, + `/checkout/${$checkoutSessionIdStore}/${data.data.linkId}`, ); }, 3000); }); diff --git a/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts index 06415eb..3e625fc 100644 --- a/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts +++ b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts @@ -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); }; diff --git a/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte index 4e15bab..2027aaf 100644 --- a/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte +++ b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte @@ -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 @@
- No ticket found - + Product not found +

Something went wrong, please try again or contact us

{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation} diff --git a/apps/frontend/src/routes/(main)/checkout/success/+page.svelte b/apps/frontend/src/routes/(main)/checkout/success/+page.svelte index e35da8f..2ce8c2d 100644 --- a/apps/frontend/src/routes/(main)/checkout/success/+page.svelte +++ b/apps/frontend/src/routes/(main)/checkout/success/+page.svelte @@ -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
diff --git a/apps/frontend/src/routes/(main)/checkout/terminated/+page.svelte b/apps/frontend/src/routes/(main)/checkout/terminated/+page.svelte index 11acebc..02bdbd7 100644 --- a/apps/frontend/src/routes/(main)/checkout/terminated/+page.svelte +++ b/apps/frontend/src/routes/(main)/checkout/terminated/+page.svelte @@ -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 @@ Session Terminated

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.

-
- {#if canRedirect} - - {/if} - - -
diff --git a/packages/logic/domains/order/data/entities.ts b/packages/logic/domains/order/data/entities.ts index 37981a9..a3069c3 100644 --- a/packages/logic/domains/order/data/entities.ts +++ b/packages/logic/domains/order/data/entities.ts @@ -102,16 +102,22 @@ export function getDefaultPaginatedOrderInfoModel(): PaginatedOrderInfoModel { }; } -export const newOrderModel = orderModel.pick({ - basePrice: true, - displayPrice: true, - discountAmount: true, - orderPrice: true, - fullfilledPrice: true, - productId: true, - customerInfoId: true, - paymentInfoId: true, -}); +export const newOrderModel = orderModel + .pick({ + basePrice: true, + displayPrice: true, + discountAmount: true, + orderPrice: true, + fullfilledPrice: true, + productId: true, + customerInfoId: true, + paymentInfoId: true, + }) + .extend({ + currency: z.string().default("USD"), + customerInfoId: z.number().optional(), + paymentInfoId: z.number().optional(), + }); export type NewOrderModel = z.infer; export const createOrderPayloadModel = z.object({