diff --git a/apps/frontend/src/lib/domains/checkout/flight-checkout.vm.svelte.ts b/apps/frontend/src/lib/domains/checkout/checkout.vm.svelte.ts similarity index 67% rename from apps/frontend/src/lib/domains/checkout/flight-checkout.vm.svelte.ts rename to apps/frontend/src/lib/domains/checkout/checkout.vm.svelte.ts index 540acca..f997fe1 100644 --- a/apps/frontend/src/lib/domains/checkout/flight-checkout.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/checkout/checkout.vm.svelte.ts @@ -1,6 +1,5 @@ import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte"; import { CheckoutStep, newOrderModel } from "$lib/domains/order/data/entities"; -import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte"; import { paymentInfoPayloadModel, PaymentMethod, @@ -8,9 +7,10 @@ import { import { trpcApiStore } from "$lib/stores/api"; import { toast } from "svelte-sonner"; import { get } from "svelte/store"; -import { flightTicketStore } from "../ticket/data/store"; +import { customerInfoVM } from "../customerinfo/view/customerinfo.vm.svelte"; +import { productStore } from "../product/store"; import { paymentInfoVM } from "./payment-info-section/payment.info.vm.svelte"; -import { calculateTicketPrices } from "./total.calculator"; +import { calculateFinalPrices } from "./utils"; class CheckoutViewModel { checkoutStep = $state(CheckoutStep.Initial); @@ -44,14 +44,8 @@ class CheckoutViewModel { if (!api) { return false; } - const ticket = get(flightTicketStore); - if (!ticket || !ticket.refOIds) { - return false; - } - const out = await api.ticket.ping.query({ - tid: ticket.id, - refOIds: ticket.refOIds, - }); + + // TODO: no need to ping now – REMOVE THIS PINGING LOGIC } async checkout() { @@ -65,34 +59,21 @@ class CheckoutViewModel { this.checkoutSubmitted = false; return false; } + const product = get(productStore); - const ticket = get(flightTicketStore); + if (!product || !customerInfoVM.customerInfo) { + this.checkoutSubmitted = false; + return false; + } - const prices = calculateTicketPrices( - ticket, - passengerInfoVM.passengerInfos, + const priceDetails = calculateFinalPrices( + product, + customerInfoVM.customerInfo, ); - const validatedPrices = { - subtotal: isNaN(prices.subtotal) ? 0 : prices.subtotal, - discountAmount: isNaN(prices.discountAmount) - ? 0 - : prices.discountAmount, - finalTotal: isNaN(prices.finalTotal) ? 0 : prices.finalTotal, - pricePerPassenger: isNaN(prices.pricePerPassenger) - ? 0 - : prices.pricePerPassenger, - }; - const parsed = newOrderModel.safeParse({ - basePrice: validatedPrices.subtotal, - discountAmount: validatedPrices.discountAmount, - displayPrice: validatedPrices.finalTotal, - orderPrice: validatedPrices.finalTotal, // Same as displayPrice - fullfilledPrice: validatedPrices.finalTotal, // Same as displayPrice - pricePerPassenger: validatedPrices.pricePerPassenger, - flightTicketInfoId: -1, - paymentInfoId: -1, + ...priceDetails, + productId: product.id, }); if (parsed.error) { @@ -107,7 +88,7 @@ class CheckoutViewModel { const pInfoParsed = paymentInfoPayloadModel.safeParse({ method: PaymentMethod.Card, cardDetails: paymentInfoVM.cardDetails, - flightTicketInfoId: ticket.id, + productId: get(productStore)?.id, }); if (pInfoParsed.error) { console.log(parsed.error); @@ -122,11 +103,10 @@ class CheckoutViewModel { console.log("Creating order"); this.loading = true; const out = await api.order.createOrder.mutate({ - flightTicketId: ticket.id, + productId: get(productStore)?.id, orderModel: parsed.data, - passengerInfos: passengerInfoVM.passengerInfos, + customerInfo: customerInfoVM.customerInfo!, paymentInfo: pInfoParsed.data, - refOIds: ticket.refOIds, flowId: ckFlowVM.flowId, }); diff --git a/apps/frontend/src/lib/domains/checkout/initial-info-section.svelte b/apps/frontend/src/lib/domains/checkout/initial-info-section.svelte index 112661d..479cc1f 100644 --- a/apps/frontend/src/lib/domains/checkout/initial-info-section.svelte +++ b/apps/frontend/src/lib/domains/checkout/initial-info-section.svelte @@ -2,31 +2,24 @@ import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte"; import Icon from "$lib/components/atoms/icon.svelte"; import Title from "$lib/components/atoms/title.svelte"; - import Badge from "$lib/components/ui/badge/badge.svelte"; import Button from "$lib/components/ui/button/button.svelte"; - import { capitalize } from "$lib/core/string.utils"; + import { checkoutVM } from "$lib/domains/checkout/checkout.vm.svelte"; import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte"; - import PassengerBagSelection from "$lib/domains/passengerinfo/view/passenger-bag-selection.svelte"; - import PassengerPiiForm from "$lib/domains/passengerinfo/view/passenger-pii-form.svelte"; - import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte"; + import { CheckoutStep } from "$lib/domains/order/data/entities"; import { cn } from "$lib/utils"; import { onMount } from "svelte"; import { toast } from "svelte-sonner"; import RightArrowIcon from "~icons/solar/arrow-right-broken"; - import { CheckoutStep } from "../../data/entities"; - import { flightTicketStore } from "../../data/store"; - import TripDetails from "../ticket/trip-details.svelte"; - import { checkoutVM } from "./checkout.vm.svelte"; + import CustomerPiiForm from "../customerinfo/view/customer-pii-form.svelte"; + import { customerInfoVM } from "../customerinfo/view/customerinfo.vm.svelte"; const cardStyle = "flex w-full flex-col gap-4 rounded-lg bg-white p-4 shadow-lg md:p-8"; $effect(() => { - const primaryPassenger = passengerInfoVM.passengerInfos[0]; - if (!ckFlowVM.flowId || !ckFlowVM.setupDone || !primaryPassenger) return; + const personalInfo = customerInfoVM.customerInfo; - const personalInfo = primaryPassenger.passengerPii; - if (!personalInfo) return; + if (!ckFlowVM.flowId || !ckFlowVM.setupDone || !personalInfo) return; // to trigger the effect const { @@ -40,11 +33,6 @@ city, state, country, - nationality, - gender, - dob, - passportNo, - passportExpiry, } = personalInfo; if ( firstName || @@ -56,12 +44,7 @@ state || zipCode || address || - address2 || - nationality || - gender || - dob || - passportNo || - passportExpiry + address2 ) { console.log("pi ping"); ckFlowVM.debouncePersonalInfoSync(personalInfo); @@ -69,9 +52,9 @@ }); async function proceedToNextStep() { - passengerInfoVM.validateAllPII(); - console.log(passengerInfoVM.piiErrors); - if (!passengerInfoVM.isPIIValid()) { + customerInfoVM.validateCustomerInfo(); + console.log(customerInfoVM.errors); + if (!customerInfoVM.isValid()) { return toast.error("Some or all info is invalid", { description: "Please properly fill out all of the fields", }); @@ -90,63 +73,32 @@ onMount(() => { window.scrollTo(0, 0); setTimeout(() => { - passengerInfoVM.setupPassengerInfo( - $flightTicketStore.passengerCounts, - ); + customerInfoVM.initializeCustomerInfo(); }, 200); }); -{#if $flightTicketStore} -
- -
- - {#if passengerInfoVM.passengerInfos.length > 0} - {#each passengerInfoVM.passengerInfos as info, idx} - {@const name = - info.passengerPii.firstName.length > 0 || - info.passengerPii.lastName.length > 0 - ? `${info.passengerPii.firstName} ${info.passengerPii.lastName}` - : `Passenger #${idx + 1}`} -
-
- - {name} - - - - {capitalize(info.passengerType)} - -
-
- Personal Info - -
- -
- Bag Selection - -
-
- {/each} - {/if} - -
-
- - +{#if customerInfoVM.customerInfo} +
+ Personal Info +
{/if} + +
+
+ + +
diff --git a/apps/frontend/src/lib/domains/checkout/payment-info-section/billing-details-form.svelte b/apps/frontend/src/lib/domains/checkout/payment-info-section/billing-details-form.svelte index f4bd3d2..07343c8 100644 --- a/apps/frontend/src/lib/domains/checkout/payment-info-section/billing-details-form.svelte +++ b/apps/frontend/src/lib/domains/checkout/payment-info-section/billing-details-form.svelte @@ -4,10 +4,10 @@ import * as Select from "$lib/components/ui/select"; import { COUNTRIES_SELECT } from "$lib/core/countries"; import { capitalize } from "$lib/core/string.utils"; - import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities"; + import type { CustomerInfoModel } from "$lib/domains/ticket/data/entities/create.entities"; import { billingDetailsVM } from "./billing.details.vm.svelte"; - let { info = $bindable() }: { info: CustomerInfo } = $props(); + let { info = $bindable() }: { info: CustomerInfoModel } = $props(); function onSubmit(e: SubmitEvent) { e.preventDefault(); diff --git a/apps/frontend/src/lib/domains/checkout/payment-info-section/billing.details.vm.svelte.ts b/apps/frontend/src/lib/domains/checkout/payment-info-section/billing.details.vm.svelte.ts index 5d78eb1..4da2ac2 100644 --- a/apps/frontend/src/lib/domains/checkout/payment-info-section/billing.details.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/checkout/payment-info-section/billing.details.vm.svelte.ts @@ -1,15 +1,12 @@ -import { - customerInfoModel, - type CustomerInfo, -} from "$lib/domains/ticket/data/entities/create.entities"; -import { Gender } from "$lib/domains/ticket/data/entities/index"; +import { type CustomerInfoModel, Gender } from "$lib/domains/customerinfo/data"; +import { customerInfoModel } from "$lib/domains/passengerinfo/data/entities"; import { z } from "zod"; export class BillingDetailsViewModel { // @ts-ignore - billingDetails = $state(undefined); + billingDetails = $state(undefined); - piiErrors = $state>>({}); + piiErrors = $state>>({}); constructor() { this.reset(); @@ -34,15 +31,15 @@ export class BillingDetailsViewModel { zipCode: "", address: "", address2: "", - } as CustomerInfo; + } as CustomerInfoModel; this.piiErrors = {}; } - setPII(info: CustomerInfo) { + setPII(info: CustomerInfoModel) { this.billingDetails = info; } - validatePII(info: CustomerInfo) { + validatePII(info: CustomerInfoModel) { try { const result = customerInfoModel.parse(info); this.piiErrors = {}; @@ -51,11 +48,11 @@ export class BillingDetailsViewModel { if (error instanceof z.ZodError) { this.piiErrors = error.errors.reduce( (acc, curr) => { - const path = curr.path[0] as keyof CustomerInfo; + const path = curr.path[0] as keyof CustomerInfoModel; acc[path] = curr.message; return acc; }, - {} as Record, + {} as Record, ); } return null; diff --git a/apps/frontend/src/lib/domains/checkout/total.calculator.ts b/apps/frontend/src/lib/domains/checkout/total.calculator.ts deleted file mode 100644 index d27a936..0000000 --- a/apps/frontend/src/lib/domains/checkout/total.calculator.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { PassengerInfo } from "$lib/domains/passengerinfo/data/entities"; -import type { FlightTicket } from "../ticket/data/entities"; - -export interface BaggageCost { - passengerId: number; - passengerName: string; - personalBagCost: number; - handBagCost: number; - checkedBagCost: number; - totalBaggageCost: number; -} - -export interface PriceBreakdown { - baseTicketPrice: number; - pricePerPassenger: number; - passengerBaggageCosts: BaggageCost[]; - totalBaggageCost: number; - subtotal: number; - discountAmount: number; - finalTotal: number; -} - -export function calculateTicketPrices( - ticket: FlightTicket, - passengerInfos: PassengerInfo[], -): PriceBreakdown { - if (!ticket || !passengerInfos || passengerInfos.length === 0) { - return { - baseTicketPrice: 0, - pricePerPassenger: 0, - passengerBaggageCosts: [], - totalBaggageCost: 0, - subtotal: 0, - discountAmount: 0, - finalTotal: 0, - }; - } - - const displayPrice = ticket.priceDetails?.displayPrice ?? 0; - const originalBasePrice = ticket.priceDetails?.basePrice ?? 0; - const baseTicketPrice = Math.max(displayPrice, originalBasePrice); - const pricePerPassenger = - passengerInfos.length > 0 - ? baseTicketPrice / passengerInfos.length - : baseTicketPrice; - - const passengerBaggageCosts: BaggageCost[] = passengerInfos.map( - (passenger) => { - // const personalBagCost = - // (passenger.bagSelection.personalBags || 0) * - // (ticket?.bagsInfo.details.personalBags.price ?? 0); - // const handBagCost = - // (passenger.bagSelection.handBags || 0) * - // (ticket?.bagsInfo.details.handBags.price ?? 0); - // const checkedBagCost = - // (passenger.bagSelection.checkedBags || 0) * - // (ticket?.bagsInfo.details.checkedBags.price ?? 0); - - return { - passengerId: passenger.id, - passengerName: `${passenger.passengerPii.firstName} ${passenger.passengerPii.lastName}`, - personalBagCost: 0, - handBagCost: 0, - checkedBagCost: 0, - totalBaggageCost: 0, - // totalBaggageCost: personalBagCost + handBagCost + checkedBagCost, - }; - }, - ); - - // const totalBaggageCost = passengerBaggageCosts.reduce( - // (acc, curr) => acc + curr.totalBaggageCost, - // 0, - // ); - const totalBaggageCost = 0; - - const subtotal = baseTicketPrice + totalBaggageCost; - - const discountAmount = - originalBasePrice > displayPrice - ? (ticket?.priceDetails.discountAmount ?? 0) - : 0; - - const finalTotal = subtotal - discountAmount; - - return { - baseTicketPrice, - pricePerPassenger, - passengerBaggageCosts, - totalBaggageCost, - subtotal, - discountAmount, - finalTotal, - }; -} diff --git a/apps/frontend/src/lib/domains/checkout/utils.ts b/apps/frontend/src/lib/domains/checkout/utils.ts new file mode 100644 index 0000000..23f8da1 --- /dev/null +++ b/apps/frontend/src/lib/domains/checkout/utils.ts @@ -0,0 +1,50 @@ +import type { CustomerInfoModel } from "@pkg/logic/domains/customerinfo/data"; +import type { OrderPriceDetailsModel } from "@pkg/logic/domains/order/data/entities"; +import type { ProductModel } from "@pkg/logic/domains/product/data"; + +/** + * Calculates final prices for a product checkout using OrderPriceDetailsModel + * @param product - The product being purchased + * @param customerInfo - Customer information (included for future extensibility) + * @returns OrderPriceDetailsModel with all price fields calculated + */ +export function calculateFinalPrices( + product: ProductModel | null, + customerInfo?: CustomerInfoModel | null, +): OrderPriceDetailsModel { + if (!product) { + return { + currency: "USD", + basePrice: 0, + discountAmount: 0, + displayPrice: 0, + orderPrice: 0, + fullfilledPrice: 0, + }; + } + + const basePrice = product.price || 0; + const discountPrice = product.discountPrice || 0; + + // Calculate discount amount: if discountPrice is set and less than base price + const hasDiscount = discountPrice > 0 && discountPrice < basePrice; + const discountAmount = hasDiscount ? basePrice - discountPrice : 0; + + // Display price is either the discounted price or the base price + const displayPrice = hasDiscount ? discountPrice : basePrice; + + // For single product checkout: + // - orderPrice = displayPrice (what customer pays) + // - fullfilledPrice = displayPrice (same as order price for immediate fulfillment) + const orderPrice = displayPrice; + const fullfilledPrice = displayPrice; + + return { + currency: "USD", + basePrice, + discountAmount, + displayPrice, + orderPrice, + fullfilledPrice, + }; +} diff --git a/apps/frontend/src/lib/domains/ckflow/data/repository.ts b/apps/frontend/src/lib/domains/ckflow/data/repository.ts index 31f80ef..b38bb3d 100644 --- a/apps/frontend/src/lib/domains/ckflow/data/repository.ts +++ b/apps/frontend/src/lib/domains/ckflow/data/repository.ts @@ -1,5 +1,5 @@ +import type { CustomerInfoModel } from "$lib/domains/customerinfo/data"; import { CheckoutStep } from "$lib/domains/order/data/entities"; -import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities"; import type { Database } from "@pkg/db"; import { and, eq } from "@pkg/db"; @@ -202,7 +202,7 @@ export class CheckoutFlowRepository { async syncPersonalInfo( flowId: string, - personalInfo: CustomerInfo, + personalInfo: CustomerInfoModel, ): Promise> { try { const existingSession = await this.db diff --git a/apps/frontend/src/lib/domains/ckflow/domain/router.ts b/apps/frontend/src/lib/domains/ckflow/domain/router.ts index 356c122..0a23a47 100644 --- a/apps/frontend/src/lib/domains/ckflow/domain/router.ts +++ b/apps/frontend/src/lib/domains/ckflow/domain/router.ts @@ -1,6 +1,6 @@ import { customerInfoModel, - type CustomerInfo, + type CustomerInfoModel, } from "$lib/domains/passengerinfo/data/entities"; import { paymentInfoPayloadModel, @@ -41,7 +41,7 @@ export const ckflowRouter = createTRPCRouter({ return getCKUseCases().createFlow({ domain: input.domain, refOIds: input.refOIds, - ticketId: input.ticketId, + productId: input.productId, ipAddress, userAgent, initialUrl: "", @@ -72,7 +72,7 @@ export const ckflowRouter = createTRPCRouter({ .mutation(async ({ input }) => { return getCKUseCases().syncPersonalInfo( input.flowId, - input.personalInfo as CustomerInfo, + input.personalInfo as CustomerInfoModel, ); }), diff --git a/apps/frontend/src/lib/domains/ckflow/domain/usecases.ts b/apps/frontend/src/lib/domains/ckflow/domain/usecases.ts index c229f76..ff93f7c 100644 --- a/apps/frontend/src/lib/domains/ckflow/domain/usecases.ts +++ b/apps/frontend/src/lib/domains/ckflow/domain/usecases.ts @@ -1,4 +1,4 @@ -import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities"; +import type { CustomerInfoModel } from "$lib/domains/customerinfo/data"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities"; import { db } from "@pkg/db"; import { isTimestampMoreThan1MinAgo } from "@pkg/logic/core/date.utils"; @@ -54,7 +54,7 @@ export class CheckoutFlowUseCases { return this.repo.executePaymentStep(flowId, payload); } - async syncPersonalInfo(flowId: string, personalInfo: CustomerInfo) { + async syncPersonalInfo(flowId: string, personalInfo: CustomerInfoModel) { return this.repo.syncPersonalInfo(flowId, personalInfo); } diff --git a/apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts b/apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts index d063882..aa24c03 100644 --- a/apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts @@ -1,4 +1,7 @@ import { page } from "$app/state"; +import { checkoutVM } from "$lib/domains/checkout/checkout.vm.svelte"; +import { billingDetailsVM } from "$lib/domains/checkout/payment-info-section/billing.details.vm.svelte"; +import { paymentInfoVM } from "$lib/domains/checkout/payment-info-section/payment.info.vm.svelte"; import { CKActionType, SessionOutcome, @@ -6,21 +9,16 @@ import { type PendingAction, type PendingActions, } from "$lib/domains/ckflow/data/entities"; +import type { CustomerInfoModel } from "$lib/domains/customerinfo/data"; import { - customerInfoModel, - type CustomerInfo, -} from "$lib/domains/passengerinfo/data/entities"; + CheckoutStep, + type OrderPriceDetailsModel, +} from "$lib/domains/order/data/entities"; +import { customerInfoModel } from "$lib/domains/passengerinfo/data/entities"; import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities"; import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities"; -import { - CheckoutStep, - type FlightPriceDetails, -} from "$lib/domains/ticket/data/entities"; -import { flightTicketStore } from "$lib/domains/ticket/data/store"; -import { checkoutVM } from "$lib/domains/ticket/view/checkout/checkout.vm.svelte"; -import { billingDetailsVM } from "$lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte"; -import { paymentInfoVM } from "$lib/domains/ticket/view/checkout/payment-info-section/payment.info.vm.svelte"; +import { productStore } from "$lib/domains/product/store"; import { trpcApiStore } from "$lib/stores/api"; import { ClientLogger } from "@pkg/logger/client"; import { toast } from "svelte-sonner"; @@ -185,7 +183,7 @@ export class CKFlowViewModel { private paymentInfoDebounceTimer: NodeJS.Timeout | null = null; syncInterval = 300; // 300ms debounce for syncing - updatedPrices = $state(undefined); + updatedPrices = $state(undefined); constructor() { this.actionRunner = new ActionRunner(); @@ -216,17 +214,10 @@ export class CKFlowViewModel { return; } - const ticket = get(flightTicketStore); - const refOIds = ticket.refOIds; - if (!refOIds) { - this.setupDone = true; - return; // Since we don't have any attached order(s), we don't need to worry about this dude - } - const info = await api.ckflow.initiateCheckout.mutate({ domain: window.location.hostname, - refOIds, - ticketId: ticket.id, + refOIds: [], + productId: get(productStore)?.id, }); if (info.error) { @@ -236,7 +227,7 @@ export class CKFlowViewModel { if (!info.data) { toast.error("Error while creating checkout flow", { - description: "Try refreshing page or search for ticket again", + description: "Try refreshing page or contact us", }); return; } @@ -248,7 +239,7 @@ export class CKFlowViewModel { this.setupDone = true; } - debouncePersonalInfoSync(personalInfo: CustomerInfo) { + debouncePersonalInfoSync(personalInfo: CustomerInfoModel) { this.clearPersonalInfoDebounce(); this.personalInfoDebounceTimer = setTimeout(() => { this.syncPersonalInfo(personalInfo); @@ -262,9 +253,10 @@ export class CKFlowViewModel { this.paymentInfoDebounceTimer = setTimeout(() => { const paymentInfo = { cardDetails: paymentInfoVM.cardDetails, - flightTicketInfoId: get(flightTicketStore).id, method: PaymentMethod.Card, - }; + orderId: -1, + productId: get(productStore)?.id, + } as PaymentInfoPayload; this.syncPaymentInfo(paymentInfo); }, this.syncInterval); } @@ -276,12 +268,12 @@ export class CKFlowViewModel { ); } - isPersonalInfoValid(personalInfo: CustomerInfo): boolean { + isPersonalInfoValid(personalInfo: CustomerInfoModel): boolean { const parsed = customerInfoModel.safeParse(personalInfo); return !parsed.error && !!parsed.data; } - async syncPersonalInfo(personalInfo: CustomerInfo) { + async syncPersonalInfo(personalInfo: CustomerInfoModel) { if (!this.flowId || !this.setupDone) { return; } @@ -546,7 +538,7 @@ export class CKFlowViewModel { const out = await api.ckflow.executePrePaymentStep.mutate({ flowId: this.flowId!, payload: { - initialUrl: get(flightTicketStore).checkoutUrl, + initialUrl: "", personalInfo: primaryPassengerInfo, }, }); @@ -570,9 +562,10 @@ export class CKFlowViewModel { const paymentInfo = { cardDetails: paymentInfoVM.cardDetails, - flightTicketInfoId: get(flightTicketStore).id, method: PaymentMethod.Card, - }; + orderId: -1, + productId: get(productStore)?.id, + } as PaymentInfoPayload; const out = await api.ckflow.executePaymentStep.mutate({ flowId: this.flowId!, diff --git a/apps/frontend/src/lib/domains/currency/utils.ts b/apps/frontend/src/lib/domains/currency/utils.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/frontend/src/lib/domains/customerinfo/controller.ts b/apps/frontend/src/lib/domains/customerinfo/controller.ts new file mode 100644 index 0000000..dcd117d --- /dev/null +++ b/apps/frontend/src/lib/domains/customerinfo/controller.ts @@ -0,0 +1,47 @@ +import { Logger } from "@pkg/logger"; +import type { Result } from "@pkg/result"; +import type { CreateCustomerInfoPayload, CustomerInfoModel } from "./data"; +import type { CustomerInfoRepository } from "./repository"; + +/** + * CustomerInfoController handles business logic for customer information operations. + * Coordinates between the repository layer and the UI/API layer. + */ +export class CustomerInfoController { + repo: CustomerInfoRepository; + + constructor(repo: CustomerInfoRepository) { + this.repo = repo; + } + + /** + * Creates a new customer information record + * @param payload - Customer information data to create + * @returns Result containing the new customer info ID or error + */ + async createCustomerInfo( + payload: CreateCustomerInfoPayload, + ): Promise> { + Logger.info("Creating customer info", { email: payload.email }); + return this.repo.createCustomerInfo(payload); + } + + /** + * Retrieves customer information by ID + * @param id - Customer info ID to retrieve + * @returns Result containing customer info model or error + */ + async getCustomerInfo(id: number): Promise> { + Logger.info(`Retrieving customer info with ID: ${id}`); + return this.repo.getCustomerInfoById(id); + } + + /** + * Retrieves all customer information records + * @returns Result containing array of customer info models or error + */ + async getAllCustomerInfo(): Promise> { + Logger.info("Retrieving all customer info records"); + return this.repo.getAllCustomerInfo(); + } +} diff --git a/apps/frontend/src/lib/domains/customerinfo/router.ts b/apps/frontend/src/lib/domains/customerinfo/router.ts deleted file mode 100644 index 2ad9489..0000000 --- a/apps/frontend/src/lib/domains/customerinfo/router.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { protectedProcedure } from "$lib/server/trpc/t"; -import { createTRPCRouter } from "$lib/trpc/t"; -import { z } from "zod"; -import { createCustomerInfoPayload, updateCustomerInfoPayload } from "./data"; -import { getCustomerInfoUseCases } from "./usecases"; - -export const customerInfoRouter = createTRPCRouter({ - getAllCustomerInfo: protectedProcedure.query(async ({}) => { - const controller = getCustomerInfoUseCases(); - return controller.getAllCustomerInfo(); - }), - - getCustomerInfoById: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const controller = getCustomerInfoUseCases(); - return controller.getCustomerInfoById(input.id); - }), - - createCustomerInfo: protectedProcedure - .input(createCustomerInfoPayload) - .mutation(async ({ input }) => { - const controller = getCustomerInfoUseCases(); - return controller.createCustomerInfo(input); - }), - - updateCustomerInfo: protectedProcedure - .input(updateCustomerInfoPayload) - .mutation(async ({ input }) => { - const controller = getCustomerInfoUseCases(); - return controller.updateCustomerInfo(input); - }), - - deleteCustomerInfo: protectedProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - const controller = getCustomerInfoUseCases(); - return controller.deleteCustomerInfo(input.id); - }), -}); diff --git a/apps/frontend/src/lib/domains/customerinfo/usecases.ts b/apps/frontend/src/lib/domains/customerinfo/usecases.ts deleted file mode 100644 index 29e3df0..0000000 --- a/apps/frontend/src/lib/domains/customerinfo/usecases.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@pkg/logic/domains/customerinfo/usecases"; diff --git a/apps/frontend/src/lib/domains/customerinfo/view/customer-pii-form.svelte b/apps/frontend/src/lib/domains/customerinfo/view/customer-pii-form.svelte index cd31173..0708418 100644 --- a/apps/frontend/src/lib/domains/customerinfo/view/customer-pii-form.svelte +++ b/apps/frontend/src/lib/domains/customerinfo/view/customer-pii-form.svelte @@ -4,238 +4,121 @@ import Input from "$lib/components/ui/input/input.svelte"; import * as Select from "$lib/components/ui/select"; import { COUNTRIES_SELECT } from "$lib/core/countries"; - import type { SelectOption } from "$lib/core/data.types"; import { capitalize } from "$lib/core/string.utils"; - import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities"; - import { Gender } from "$lib/domains/ticket/data/entities/index"; import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc"; + import type { CustomerInfoModel } from "../data"; + import { customerInfoVM } from "./customerinfo.vm.svelte"; - let { info = $bindable(), idx }: { info: CustomerInfo; idx: number } = - $props(); - - const genderOpts = [ - { label: capitalize(Gender.Male), value: Gender.Male }, - { label: capitalize(Gender.Female), value: Gender.Female }, - { label: capitalize(Gender.Other), value: Gender.Other }, - ] as SelectOption[]; + let { info = $bindable() }: { info: CustomerInfoModel } = $props(); function onSubmit(e: SubmitEvent) { e.preventDefault(); - passengerInfoVM.validatePII(info, idx); + customerInfoVM.validateCustomerInfo(info); } - let validationTimeout = $state(undefined as undefined | NodeJS.Timer); - function debounceValidate() { - if (validationTimeout) { - clearTimeout(validationTimeout); - } - validationTimeout = setTimeout(() => { - passengerInfoVM.validatePII(info, idx); - }, 500); + customerInfoVM.debounceValidate(info); }
+
- + debounceValidate()} + minlength={1} + maxlength={64} /> debounceValidate()} - required + maxlength={64} /> - + debounceValidate()} + minlength={1} + maxlength={64} />
- + + debounceValidate()} required + maxlength={128} /> -
- -
- { - info.phoneCountryCode = code; - }} - name="phoneCode" - > - - {#if info.phoneCountryCode} - {info.phoneCountryCode} - {:else} - Select - {/if} - - - {#each PHONE_COUNTRY_CODES as { country, phoneCode }} - - - {phoneCode} ({country}) - - - {/each} - - - - debounceValidate()} - class="flex-1" - /> -
-
- - - + +
+ { - // @ts-ignore - info.passportExpiry = v.target.value; + onValueChange={(code) => { + info.phoneCountryCode = code; debounceValidate(); }} - /> - - + name="phoneCode" + > + + {#if info.phoneCountryCode} + {info.phoneCountryCode} + {:else} + Select + {/if} + + + {#each PHONE_COUNTRY_CODES as { country, phoneCode }} + + + {phoneCode} ({country}) + + + {/each} + + + debounceValidate()} + class="flex-1" minlength={1} maxlength={20} - required - oninput={() => debounceValidate()} /> - -
+
+
+ + + Address Information
- - { - info.nationality = e; - debounceValidate(); - }} - name="role" - > - - {capitalize( - info.nationality.length > 0 ? info.nationality : "Select", - )} - - - {#each COUNTRIES_SELECT as country} - - {country.label} - - {/each} - - - - - { - info.gender = e as Gender; - debounceValidate(); - }} - name="role" - > - - {capitalize(info.gender.length > 0 ? info.gender : "Select")} - - - {#each genderOpts as gender} - - {gender.label} - - {/each} - - - - - - debounceValidate()} - /> - -
- - - - Address Info - -
- + {capitalize( @@ -260,63 +143,60 @@ - + debounceValidate()} + minlength={1} + maxlength={128} />
+
- + debounceValidate()} /> - + debounceValidate()} - maxlength={12} />
- + debounceValidate()} /> debounceValidate()} /> diff --git a/apps/frontend/src/lib/domains/customerinfo/view/customerinfo.vm.svelte.ts b/apps/frontend/src/lib/domains/customerinfo/view/customerinfo.vm.svelte.ts index b202bd4..2d550ff 100644 --- a/apps/frontend/src/lib/domains/customerinfo/view/customerinfo.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/customerinfo/view/customerinfo.vm.svelte.ts @@ -1,10 +1,281 @@ -import type { CustomerInfoModel } from "../data"; +import { trpcApiStore } from "$lib/stores/api"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; +import { z } from "zod"; +import type { + CreateCustomerInfoPayload, + CustomerInfoModel, + UpdateCustomerInfoPayload, +} from "../data"; +import { customerInfoModel } from "../data"; +/** + * CustomerInfoViewModel manages single customer information state for checkout. + * Handles validation, API interactions, and form state for one customer per checkout. + */ export class CustomerInfoViewModel { - customerInfos = $state([] as CustomerInfoModel[]); + // State: current customer info being edited/created (single customer for checkout) + customerInfo = $state(null); + // State: validation errors for the current customer info + errors = $state>>({}); + + // Loading states loading = $state(false); - query = $state(""); + formLoading = $state(false); + + /** + * Initializes customer info with default empty values for checkout + */ + initializeCustomerInfo() { + this.customerInfo = this.getDefaultCustomerInfo() as CustomerInfoModel; + this.errors = {}; + } + + /** + * Fetches a single customer info record by ID and sets it as current + * @param id - Customer info ID to fetch + * @returns true if successful, false otherwise + */ + async fetchCustomerInfoById(id: number): Promise { + const api = get(trpcApiStore); + if (!api) { + toast.error("API client not initialized"); + return false; + } + + this.loading = true; + try { + const result = await api.customerInfo.getCustomerInfoById.query({ + id, + }); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + this.customerInfo = result.data || null; + return true; + } catch (e) { + console.error(e); + toast.error("Failed to fetch customer information", { + description: "Please try again later", + }); + return false; + } finally { + this.loading = false; + } + } + + /** + * Creates a new customer information record + * @param payload - Customer info data to create + * @returns Customer info ID if successful, null otherwise + */ + async createCustomerInfo( + payload: CreateCustomerInfoPayload, + ): Promise { + const api = get(trpcApiStore); + if (!api) { + toast.error("API client not initialized"); + return null; + } + + // Validate before submitting + if (!this.validateCustomerInfo(payload)) { + toast.error("Please fix validation errors before submitting"); + return null; + } + + this.formLoading = true; + try { + const result = + await api.customerInfo.createCustomerInfo.mutate(payload); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return null; + } + + toast.success("Customer information saved successfully"); + return result.data || null; + } catch (e) { + console.error(e); + toast.error("Failed to save customer information", { + description: "Please try again later", + }); + return null; + } finally { + this.formLoading = false; + } + } + + /** + * Updates the current customer information record + * @param payload - Customer info data to update (must include id) + * @returns true if successful, false otherwise + */ + async updateCustomerInfo( + payload: UpdateCustomerInfoPayload, + ): Promise { + const api = get(trpcApiStore); + if (!api) { + toast.error("API client not initialized"); + return false; + } + + if (!payload.id) { + toast.error("Customer ID is required for update"); + return false; + } + + this.formLoading = true; + try { + const result = + await api.customerInfo.updateCustomerInfo.mutate(payload); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + toast.success("Customer information updated successfully"); + + // Update local state with the updated data + if (this.customerInfo && this.customerInfo.id === payload.id) { + this.customerInfo = { ...this.customerInfo, ...payload }; + } + + return true; + } catch (e) { + console.error(e); + toast.error("Failed to update customer information", { + description: "Please try again later", + }); + return false; + } finally { + this.formLoading = false; + } + } + + /** + * Validates customer information data using Zod schema + * @param info - Customer info to validate + * @returns true if valid, false otherwise + */ + validateCustomerInfo(info?: Partial): boolean { + try { + customerInfoModel.parse(info || this.customerInfo); + this.errors = {}; + return true; + } catch (error) { + if (error instanceof z.ZodError) { + this.errors = error.errors.reduce( + (acc, curr) => { + const path = curr.path[0] as keyof CustomerInfoModel; + acc[path] = curr.message; + return acc; + }, + {} as Record, + ); + } + return false; + } + } + + /** + * Debounced validation for real-time form feedback + * @param info - Customer info to validate + * @param delay - Debounce delay in milliseconds (default: 500) + */ + private validationTimeout: NodeJS.Timeout | undefined; + + debounceValidate(info: Partial, delay = 500) { + if (this.validationTimeout) { + clearTimeout(this.validationTimeout); + } + this.validationTimeout = setTimeout(() => { + this.validateCustomerInfo(info); + }, delay); + } + + /** + * Checks if the current customer info has validation errors + * @returns true if there are no errors, false otherwise + */ + isValid(): boolean { + return Object.keys(this.errors).length === 0; + } + + /** + * Checks if customer info is filled out (has required fields) + * @returns true if customer info exists and has data, false otherwise + */ + isFilled(): boolean { + if (!this.customerInfo) return false; + + return ( + this.customerInfo.firstName.length > 0 && + this.customerInfo.lastName.length > 0 && + this.customerInfo.email.length > 0 && + this.customerInfo.phoneNumber.length > 0 + ); + } + + /** + * Sets the current customer info (for editing existing records) + * @param customerInfo - Customer info to edit + */ + setCustomerInfo(customerInfo: CustomerInfoModel) { + this.customerInfo = { ...customerInfo }; + this.errors = {}; + } + + /** + * Resets the customer info and errors + */ + resetCustomerInfo() { + this.customerInfo = null; + this.errors = {}; + } + + /** + * Returns a default/empty customer info object for forms + * @returns Default customer info payload + */ + getDefaultCustomerInfo(): CreateCustomerInfoPayload { + return { + firstName: "", + middleName: "", + lastName: "", + email: "", + phoneCountryCode: "", + phoneNumber: "", + country: "", + state: "", + city: "", + zipCode: "", + address: "", + address2: null, + }; + } + + /** + * Resets all state (for cleanup or re-initialization) + */ + reset() { + this.customerInfo = null; + this.errors = {}; + this.loading = false; + this.formLoading = false; + if (this.validationTimeout) { + clearTimeout(this.validationTimeout); + } + } } export const customerInfoVM = new CustomerInfoViewModel(); diff --git a/apps/frontend/src/lib/domains/passengerinfo/data/repository.ts b/apps/frontend/src/lib/domains/passengerinfo/data/repository.ts index 444d48d..39513b4 100644 --- a/apps/frontend/src/lib/domains/passengerinfo/data/repository.ts +++ b/apps/frontend/src/lib/domains/passengerinfo/data/repository.ts @@ -4,7 +4,7 @@ import { getError, Logger } from "@pkg/logger"; import { ERROR_CODES, type Result } from "@pkg/result"; import { passengerInfoModel, - type CustomerInfo, + type CustomerInfoModel, type PassengerInfo, } from "./entities"; @@ -15,7 +15,9 @@ export class PassengerInfoRepository { this.db = db; } - async createPassengerPii(payload: CustomerInfo): Promise> { + async createPassengerPii( + payload: CustomerInfoModel, + ): Promise> { try { const out = await this.db .insert(passengerPII) diff --git a/apps/frontend/src/lib/domains/passengerinfo/view/passenger-bag-selection.svelte b/apps/frontend/src/lib/domains/passengerinfo/view/passenger-bag-selection.svelte deleted file mode 100644 index 07f03ed..0000000 --- a/apps/frontend/src/lib/domains/passengerinfo/view/passenger-bag-selection.svelte +++ /dev/null @@ -1,214 +0,0 @@ - - -
- {#each bagTypes as bag} - {@const bagDetails = getBagDetails(bag.type)} - {@const isAvailable = true} - {@const isDisabled = false} - - - - -
!isDisabled && !bag.included && toggleBag(bag.type)} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - !isDisabled && !bag.included && toggleBag(bag.type); - } - }} - > -
-
- -
-
- -
-
- {bag.label} - {#if bag.included} - - Included - - {/if} - - {#if bag.type === "checkedBag" && !isAvailable} - - Not available for this flight - - {/if} -
- {bag.description} - {#if bagDetails} -
- {#if bagDetails.weight > 0} - - Up to {bagDetails.weight}{bagDetails.unit} - - {/if} - {#if formatDimensions(bagDetails)} - {formatDimensions(bagDetails)} - {/if} -
- {/if} -
- -
- - Free - -
- - {#if isBagSelected(bag.type)} -
- -
- {/if} -
- {/each} -
diff --git a/apps/frontend/src/lib/domains/passengerinfo/view/passenger-pii-form.svelte b/apps/frontend/src/lib/domains/passengerinfo/view/passenger-pii-form.svelte index f6bceab..f7eca33 100644 --- a/apps/frontend/src/lib/domains/passengerinfo/view/passenger-pii-form.svelte +++ b/apps/frontend/src/lib/domains/passengerinfo/view/passenger-pii-form.svelte @@ -6,12 +6,12 @@ import { COUNTRIES_SELECT } from "$lib/core/countries"; import type { SelectOption } from "$lib/core/data.types"; import { capitalize } from "$lib/core/string.utils"; - import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities"; + import type { CustomerInfoModel } from "$lib/domains/ticket/data/entities/create.entities"; import { Gender } from "$lib/domains/ticket/data/entities/index"; import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc"; import { passengerInfoVM } from "./passenger.info.vm.svelte"; - let { info = $bindable(), idx }: { info: CustomerInfo; idx: number } = + let { info = $bindable(), idx }: { info: CustomerInfoModel; idx: number } = $props(); const genderOpts = [ diff --git a/apps/frontend/src/lib/domains/passengerinfo/view/passenger.info.vm.svelte.ts b/apps/frontend/src/lib/domains/passengerinfo/view/passenger.info.vm.svelte.ts index a04d380..56755bf 100644 --- a/apps/frontend/src/lib/domains/passengerinfo/view/passenger.info.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/passengerinfo/view/passenger.info.vm.svelte.ts @@ -1,7 +1,7 @@ import { customerInfoModel, type BagSelectionInfo, - type CustomerInfo, + type CustomerInfoModel, type PassengerInfo, type SeatSelectionInfo, } from "$lib/domains/ticket/data/entities/create.entities"; @@ -17,7 +17,9 @@ import { z } from "zod"; export class PassengerInfoViewModel { passengerInfos = $state([]); - piiErrors = $state>>>([]); + piiErrors = $state>>>( + [], + ); reset() { this.passengerInfos = []; @@ -47,7 +49,7 @@ export class PassengerInfoViewModel { // zipCode: "123098", // address: "address", // address2: "", - // } as CustomerInfo; + // } as CustomerInfoModel; const _defaultPiiObj = { firstName: "", @@ -67,7 +69,7 @@ export class PassengerInfoViewModel { zipCode: "", address: "", address2: "", - } as CustomerInfo; + } as CustomerInfoModel; const _defaultPriceObj = { currency: "", @@ -137,7 +139,7 @@ export class PassengerInfoViewModel { } } - validatePII(info: CustomerInfo, idx: number) { + validatePII(info: CustomerInfoModel, idx: number) { try { const result = customerInfoModel.parse(info); this.piiErrors[idx] = {}; @@ -146,11 +148,11 @@ export class PassengerInfoViewModel { if (error instanceof z.ZodError) { this.piiErrors[idx] = error.errors.reduce( (acc, curr) => { - const path = curr.path[0] as keyof CustomerInfo; + const path = curr.path[0] as keyof CustomerInfoModel; acc[path] = curr.message; return acc; }, - {} as Record, + {} as Record, ); } return null; diff --git a/apps/frontend/src/lib/domains/product/store.ts b/apps/frontend/src/lib/domains/product/store.ts new file mode 100644 index 0000000..fa28268 --- /dev/null +++ b/apps/frontend/src/lib/domains/product/store.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; +import type { ProductModel } from "./data"; + +export const productStore = writable(null); diff --git a/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing-details-form.svelte b/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing-details-form.svelte index f4bd3d2..07343c8 100644 --- a/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing-details-form.svelte +++ b/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing-details-form.svelte @@ -4,10 +4,10 @@ import * as Select from "$lib/components/ui/select"; import { COUNTRIES_SELECT } from "$lib/core/countries"; import { capitalize } from "$lib/core/string.utils"; - import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities"; + import type { CustomerInfoModel } from "$lib/domains/ticket/data/entities/create.entities"; import { billingDetailsVM } from "./billing.details.vm.svelte"; - let { info = $bindable() }: { info: CustomerInfo } = $props(); + let { info = $bindable() }: { info: CustomerInfoModel } = $props(); function onSubmit(e: SubmitEvent) { e.preventDefault(); diff --git a/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte.ts b/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte.ts index 5d78eb1..1ca39ff 100644 --- a/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte.ts @@ -1,15 +1,15 @@ import { customerInfoModel, - type CustomerInfo, + type CustomerInfoModel, } from "$lib/domains/ticket/data/entities/create.entities"; import { Gender } from "$lib/domains/ticket/data/entities/index"; import { z } from "zod"; export class BillingDetailsViewModel { // @ts-ignore - billingDetails = $state(undefined); + billingDetails = $state(undefined); - piiErrors = $state>>({}); + piiErrors = $state>>({}); constructor() { this.reset(); @@ -34,15 +34,15 @@ export class BillingDetailsViewModel { zipCode: "", address: "", address2: "", - } as CustomerInfo; + } as CustomerInfoModel; this.piiErrors = {}; } - setPII(info: CustomerInfo) { + setPII(info: CustomerInfoModel) { this.billingDetails = info; } - validatePII(info: CustomerInfo) { + validatePII(info: CustomerInfoModel) { try { const result = customerInfoModel.parse(info); this.piiErrors = {}; @@ -51,11 +51,11 @@ export class BillingDetailsViewModel { if (error instanceof z.ZodError) { this.piiErrors = error.errors.reduce( (acc, curr) => { - const path = curr.path[0] as keyof CustomerInfo; + const path = curr.path[0] as keyof CustomerInfoModel; acc[path] = curr.message; return acc; }, - {} as Record, + {} as Record, ); } return null; diff --git a/apps/frontend/src/lib/trpc/router/index.ts b/apps/frontend/src/lib/trpc/router/index.ts index dcc1b5d..7c23ff4 100644 --- a/apps/frontend/src/lib/trpc/router/index.ts +++ b/apps/frontend/src/lib/trpc/router/index.ts @@ -1,6 +1,7 @@ import { authRouter } from "$lib/domains/auth/domain/router"; import { ckflowRouter } from "$lib/domains/ckflow/domain/router"; import { currencyRouter } from "$lib/domains/currency/domain/router"; +import { customerInfoRouter } from "$lib/domains/customerinfo/router"; import { orderRouter } from "$lib/domains/order/domain/router"; import { productRouter } from "$lib/domains/product/router"; import { userRouter } from "$lib/domains/user/domain/router"; @@ -13,6 +14,7 @@ export const router = createTRPCRouter({ order: orderRouter, ckflow: ckflowRouter, product: productRouter, + customerInfo: customerInfoRouter, }); export type Router = typeof router; diff --git a/packages/logic/domains/ckflow/data/entities.ts b/packages/logic/domains/ckflow/data/entities.ts index bd59b75..af806da 100644 --- a/packages/logic/domains/ckflow/data/entities.ts +++ b/packages/logic/domains/ckflow/data/entities.ts @@ -1,9 +1,6 @@ import { z } from "zod"; +import { CustomerInfoModel, customerInfoModel } from "../../customerinfo/data"; import { CheckoutStep } from "../../order/data/enums"; -import { - CustomerInfo, - customerInfoModel, -} from "../../passengerinfo/data/entities"; import { PaymentInfoPayload, paymentInfoPayloadModel, @@ -77,7 +74,7 @@ export const flowInfoModel = z.object({ paymentInfoLastSyncedAt: z.string().datetime().optional(), pendingActions: pendingActionsModel.default([]), - personalInfo: z.custom().optional(), + personalInfo: z.custom().optional(), paymentInfo: z.custom().optional(), refOids: z.array(z.number()).optional(), diff --git a/packages/logic/domains/customerinfo/data.ts b/packages/logic/domains/customerinfo/data.ts index 21ec85f..45b5cf0 100644 --- a/packages/logic/domains/customerinfo/data.ts +++ b/packages/logic/domains/customerinfo/data.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export enum Gender { + Male = "male", + Female = "female", + Other = "other", +} + export const customerInfoModel = z.object({ id: z.number().optional(), firstName: z.string().min(1).max(64), diff --git a/packages/logic/domains/order/data/entities.ts b/packages/logic/domains/order/data/entities.ts index 1a9fc14..76cc861 100644 --- a/packages/logic/domains/order/data/entities.ts +++ b/packages/logic/domains/order/data/entities.ts @@ -21,14 +21,20 @@ export enum OrderStatus { CANCELLED = "CANCELLED", } -export const orderModel = z.object({ - id: z.coerce.number().int().positive(), - +export const orderPriceDetailsModel = z.object({ + currency: z.string(), discountAmount: z.coerce.number().min(0), basePrice: z.coerce.number().min(0), displayPrice: z.coerce.number().min(0), orderPrice: z.coerce.number().min(0), fullfilledPrice: z.coerce.number().min(0), +}); +export type OrderPriceDetailsModel = z.infer; + +export const orderModel = z.object({ + id: z.coerce.number().int().positive(), + + ...orderPriceDetailsModel.shape, status: z.nativeEnum(OrderStatus), @@ -119,5 +125,6 @@ export const createOrderPayloadModel = z.object({ customerInfo: customerInfoModel, paymentInfo: paymentInfoPayloadModel.optional(), orderModel: newOrderModel, + flowId: z.string().optional(), }); export type CreateOrderModel = z.infer;