a.... LOT of Refactoring ~ 30% done???
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
||||||
import { CheckoutStep, newOrderModel } from "$lib/domains/order/data/entities";
|
import { CheckoutStep, newOrderModel } from "$lib/domains/order/data/entities";
|
||||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
|
||||||
import {
|
import {
|
||||||
paymentInfoPayloadModel,
|
paymentInfoPayloadModel,
|
||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
@@ -8,9 +7,10 @@ import {
|
|||||||
import { trpcApiStore } from "$lib/stores/api";
|
import { trpcApiStore } from "$lib/stores/api";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { get } from "svelte/store";
|
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 { paymentInfoVM } from "./payment-info-section/payment.info.vm.svelte";
|
||||||
import { calculateTicketPrices } from "./total.calculator";
|
import { calculateFinalPrices } from "./utils";
|
||||||
|
|
||||||
class CheckoutViewModel {
|
class CheckoutViewModel {
|
||||||
checkoutStep = $state(CheckoutStep.Initial);
|
checkoutStep = $state(CheckoutStep.Initial);
|
||||||
@@ -44,14 +44,8 @@ class CheckoutViewModel {
|
|||||||
if (!api) {
|
if (!api) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const ticket = get(flightTicketStore);
|
|
||||||
if (!ticket || !ticket.refOIds) {
|
// TODO: no need to ping now – REMOVE THIS PINGING LOGIC
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const out = await api.ticket.ping.query({
|
|
||||||
tid: ticket.id,
|
|
||||||
refOIds: ticket.refOIds,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkout() {
|
async checkout() {
|
||||||
@@ -65,34 +59,21 @@ class CheckoutViewModel {
|
|||||||
this.checkoutSubmitted = false;
|
this.checkoutSubmitted = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const product = get(productStore);
|
||||||
|
|
||||||
const ticket = get(flightTicketStore);
|
if (!product || !customerInfoVM.customerInfo) {
|
||||||
|
this.checkoutSubmitted = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const prices = calculateTicketPrices(
|
const priceDetails = calculateFinalPrices(
|
||||||
ticket,
|
product,
|
||||||
passengerInfoVM.passengerInfos,
|
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({
|
const parsed = newOrderModel.safeParse({
|
||||||
basePrice: validatedPrices.subtotal,
|
...priceDetails,
|
||||||
discountAmount: validatedPrices.discountAmount,
|
productId: product.id,
|
||||||
displayPrice: validatedPrices.finalTotal,
|
|
||||||
orderPrice: validatedPrices.finalTotal, // Same as displayPrice
|
|
||||||
fullfilledPrice: validatedPrices.finalTotal, // Same as displayPrice
|
|
||||||
pricePerPassenger: validatedPrices.pricePerPassenger,
|
|
||||||
flightTicketInfoId: -1,
|
|
||||||
paymentInfoId: -1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parsed.error) {
|
if (parsed.error) {
|
||||||
@@ -107,7 +88,7 @@ class CheckoutViewModel {
|
|||||||
const pInfoParsed = paymentInfoPayloadModel.safeParse({
|
const pInfoParsed = paymentInfoPayloadModel.safeParse({
|
||||||
method: PaymentMethod.Card,
|
method: PaymentMethod.Card,
|
||||||
cardDetails: paymentInfoVM.cardDetails,
|
cardDetails: paymentInfoVM.cardDetails,
|
||||||
flightTicketInfoId: ticket.id,
|
productId: get(productStore)?.id,
|
||||||
});
|
});
|
||||||
if (pInfoParsed.error) {
|
if (pInfoParsed.error) {
|
||||||
console.log(parsed.error);
|
console.log(parsed.error);
|
||||||
@@ -122,11 +103,10 @@ class CheckoutViewModel {
|
|||||||
console.log("Creating order");
|
console.log("Creating order");
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const out = await api.order.createOrder.mutate({
|
const out = await api.order.createOrder.mutate({
|
||||||
flightTicketId: ticket.id,
|
productId: get(productStore)?.id,
|
||||||
orderModel: parsed.data,
|
orderModel: parsed.data,
|
||||||
passengerInfos: passengerInfoVM.passengerInfos,
|
customerInfo: customerInfoVM.customerInfo!,
|
||||||
paymentInfo: pInfoParsed.data,
|
paymentInfo: pInfoParsed.data,
|
||||||
refOIds: ticket.refOIds,
|
|
||||||
flowId: ckFlowVM.flowId,
|
flowId: ckFlowVM.flowId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2,31 +2,24 @@
|
|||||||
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
import Icon from "$lib/components/atoms/icon.svelte";
|
||||||
import Title from "$lib/components/atoms/title.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 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 { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
||||||
import PassengerBagSelection from "$lib/domains/passengerinfo/view/passenger-bag-selection.svelte";
|
import { CheckoutStep } from "$lib/domains/order/data/entities";
|
||||||
import PassengerPiiForm from "$lib/domains/passengerinfo/view/passenger-pii-form.svelte";
|
|
||||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
|
||||||
import { cn } from "$lib/utils";
|
import { cn } from "$lib/utils";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import RightArrowIcon from "~icons/solar/arrow-right-broken";
|
import RightArrowIcon from "~icons/solar/arrow-right-broken";
|
||||||
import { CheckoutStep } from "../../data/entities";
|
import CustomerPiiForm from "../customerinfo/view/customer-pii-form.svelte";
|
||||||
import { flightTicketStore } from "../../data/store";
|
import { customerInfoVM } from "../customerinfo/view/customerinfo.vm.svelte";
|
||||||
import TripDetails from "../ticket/trip-details.svelte";
|
|
||||||
import { checkoutVM } from "./checkout.vm.svelte";
|
|
||||||
|
|
||||||
const cardStyle =
|
const cardStyle =
|
||||||
"flex w-full flex-col gap-4 rounded-lg bg-white p-4 shadow-lg md:p-8";
|
"flex w-full flex-col gap-4 rounded-lg bg-white p-4 shadow-lg md:p-8";
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const primaryPassenger = passengerInfoVM.passengerInfos[0];
|
const personalInfo = customerInfoVM.customerInfo;
|
||||||
if (!ckFlowVM.flowId || !ckFlowVM.setupDone || !primaryPassenger) return;
|
|
||||||
|
|
||||||
const personalInfo = primaryPassenger.passengerPii;
|
if (!ckFlowVM.flowId || !ckFlowVM.setupDone || !personalInfo) return;
|
||||||
if (!personalInfo) return;
|
|
||||||
|
|
||||||
// to trigger the effect
|
// to trigger the effect
|
||||||
const {
|
const {
|
||||||
@@ -40,11 +33,6 @@
|
|||||||
city,
|
city,
|
||||||
state,
|
state,
|
||||||
country,
|
country,
|
||||||
nationality,
|
|
||||||
gender,
|
|
||||||
dob,
|
|
||||||
passportNo,
|
|
||||||
passportExpiry,
|
|
||||||
} = personalInfo;
|
} = personalInfo;
|
||||||
if (
|
if (
|
||||||
firstName ||
|
firstName ||
|
||||||
@@ -56,12 +44,7 @@
|
|||||||
state ||
|
state ||
|
||||||
zipCode ||
|
zipCode ||
|
||||||
address ||
|
address ||
|
||||||
address2 ||
|
address2
|
||||||
nationality ||
|
|
||||||
gender ||
|
|
||||||
dob ||
|
|
||||||
passportNo ||
|
|
||||||
passportExpiry
|
|
||||||
) {
|
) {
|
||||||
console.log("pi ping");
|
console.log("pi ping");
|
||||||
ckFlowVM.debouncePersonalInfoSync(personalInfo);
|
ckFlowVM.debouncePersonalInfoSync(personalInfo);
|
||||||
@@ -69,9 +52,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function proceedToNextStep() {
|
async function proceedToNextStep() {
|
||||||
passengerInfoVM.validateAllPII();
|
customerInfoVM.validateCustomerInfo();
|
||||||
console.log(passengerInfoVM.piiErrors);
|
console.log(customerInfoVM.errors);
|
||||||
if (!passengerInfoVM.isPIIValid()) {
|
if (!customerInfoVM.isValid()) {
|
||||||
return toast.error("Some or all info is invalid", {
|
return toast.error("Some or all info is invalid", {
|
||||||
description: "Please properly fill out all of the fields",
|
description: "Please properly fill out all of the fields",
|
||||||
});
|
});
|
||||||
@@ -90,49 +73,19 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
passengerInfoVM.setupPassengerInfo(
|
customerInfoVM.initializeCustomerInfo();
|
||||||
$flightTicketStore.passengerCounts,
|
|
||||||
);
|
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $flightTicketStore}
|
{#if customerInfoVM.customerInfo}
|
||||||
<div class={cardStyle}>
|
|
||||||
<TripDetails data={$flightTicketStore} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#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}`}
|
|
||||||
<div class={cardStyle}>
|
|
||||||
<div class="flex flex-row items-center justify-between gap-4">
|
|
||||||
<Title size="h4" maxwidth="max-w-xs">
|
|
||||||
{name}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Badge variant="secondary" class="w-max">
|
|
||||||
{capitalize(info.passengerType)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div class={cn(cardStyle, "border-2 border-gray-200")}>
|
<div class={cn(cardStyle, "border-2 border-gray-200")}>
|
||||||
<Title size="h5">Personal Info</Title>
|
<Title size="h5">Personal Info</Title>
|
||||||
<PassengerPiiForm bind:info={info.passengerPii} {idx} />
|
<CustomerPiiForm bind:info={customerInfoVM.customerInfo} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class={cn(cardStyle, "border-2 border-gray-200")}>
|
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||||
<Title size="h5">Bag Selection</Title>
|
|
||||||
<PassengerBagSelection bind:info={info.bagSelection} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -148,5 +101,4 @@
|
|||||||
/>
|
/>
|
||||||
<Icon icon={RightArrowIcon} cls="w-auto h-6" />
|
<Icon icon={RightArrowIcon} cls="w-auto h-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import * as Select from "$lib/components/ui/select";
|
import * as Select from "$lib/components/ui/select";
|
||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
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";
|
import { billingDetailsVM } from "./billing.details.vm.svelte";
|
||||||
|
|
||||||
let { info = $bindable() }: { info: CustomerInfo } = $props();
|
let { info = $bindable() }: { info: CustomerInfoModel } = $props();
|
||||||
|
|
||||||
function onSubmit(e: SubmitEvent) {
|
function onSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import {
|
import { type CustomerInfoModel, Gender } from "$lib/domains/customerinfo/data";
|
||||||
customerInfoModel,
|
import { customerInfoModel } from "$lib/domains/passengerinfo/data/entities";
|
||||||
type CustomerInfo,
|
|
||||||
} from "$lib/domains/ticket/data/entities/create.entities";
|
|
||||||
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export class BillingDetailsViewModel {
|
export class BillingDetailsViewModel {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
billingDetails = $state<CustomerInfo>(undefined);
|
billingDetails = $state<CustomerInfoModel>(undefined);
|
||||||
|
|
||||||
piiErrors = $state<Partial<Record<keyof CustomerInfo, string>>>({});
|
piiErrors = $state<Partial<Record<keyof CustomerInfoModel, string>>>({});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -34,15 +31,15 @@ export class BillingDetailsViewModel {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
address: "",
|
address: "",
|
||||||
address2: "",
|
address2: "",
|
||||||
} as CustomerInfo;
|
} as CustomerInfoModel;
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPII(info: CustomerInfo) {
|
setPII(info: CustomerInfoModel) {
|
||||||
this.billingDetails = info;
|
this.billingDetails = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePII(info: CustomerInfo) {
|
validatePII(info: CustomerInfoModel) {
|
||||||
try {
|
try {
|
||||||
const result = customerInfoModel.parse(info);
|
const result = customerInfoModel.parse(info);
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
@@ -51,11 +48,11 @@ export class BillingDetailsViewModel {
|
|||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
this.piiErrors = error.errors.reduce(
|
this.piiErrors = error.errors.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const path = curr.path[0] as keyof CustomerInfo;
|
const path = curr.path[0] as keyof CustomerInfoModel;
|
||||||
acc[path] = curr.message;
|
acc[path] = curr.message;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<keyof CustomerInfo, string>,
|
{} as Record<keyof CustomerInfoModel, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
50
apps/frontend/src/lib/domains/checkout/utils.ts
Normal file
50
apps/frontend/src/lib/domains/checkout/utils.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { CustomerInfoModel } from "$lib/domains/customerinfo/data";
|
||||||
import { CheckoutStep } from "$lib/domains/order/data/entities";
|
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 { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import type { Database } from "@pkg/db";
|
import type { Database } from "@pkg/db";
|
||||||
import { and, eq } from "@pkg/db";
|
import { and, eq } from "@pkg/db";
|
||||||
@@ -202,7 +202,7 @@ export class CheckoutFlowRepository {
|
|||||||
|
|
||||||
async syncPersonalInfo(
|
async syncPersonalInfo(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
personalInfo: CustomerInfo,
|
personalInfo: CustomerInfoModel,
|
||||||
): Promise<Result<boolean>> {
|
): Promise<Result<boolean>> {
|
||||||
try {
|
try {
|
||||||
const existingSession = await this.db
|
const existingSession = await this.db
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
customerInfoModel,
|
customerInfoModel,
|
||||||
type CustomerInfo,
|
type CustomerInfoModel,
|
||||||
} from "$lib/domains/passengerinfo/data/entities";
|
} from "$lib/domains/passengerinfo/data/entities";
|
||||||
import {
|
import {
|
||||||
paymentInfoPayloadModel,
|
paymentInfoPayloadModel,
|
||||||
@@ -41,7 +41,7 @@ export const ckflowRouter = createTRPCRouter({
|
|||||||
return getCKUseCases().createFlow({
|
return getCKUseCases().createFlow({
|
||||||
domain: input.domain,
|
domain: input.domain,
|
||||||
refOIds: input.refOIds,
|
refOIds: input.refOIds,
|
||||||
ticketId: input.ticketId,
|
productId: input.productId,
|
||||||
ipAddress,
|
ipAddress,
|
||||||
userAgent,
|
userAgent,
|
||||||
initialUrl: "",
|
initialUrl: "",
|
||||||
@@ -72,7 +72,7 @@ export const ckflowRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return getCKUseCases().syncPersonalInfo(
|
return getCKUseCases().syncPersonalInfo(
|
||||||
input.flowId,
|
input.flowId,
|
||||||
input.personalInfo as CustomerInfo,
|
input.personalInfo as CustomerInfoModel,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -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 type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import { db } from "@pkg/db";
|
import { db } from "@pkg/db";
|
||||||
import { isTimestampMoreThan1MinAgo } from "@pkg/logic/core/date.utils";
|
import { isTimestampMoreThan1MinAgo } from "@pkg/logic/core/date.utils";
|
||||||
@@ -54,7 +54,7 @@ export class CheckoutFlowUseCases {
|
|||||||
return this.repo.executePaymentStep(flowId, payload);
|
return this.repo.executePaymentStep(flowId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncPersonalInfo(flowId: string, personalInfo: CustomerInfo) {
|
async syncPersonalInfo(flowId: string, personalInfo: CustomerInfoModel) {
|
||||||
return this.repo.syncPersonalInfo(flowId, personalInfo);
|
return this.repo.syncPersonalInfo(flowId, personalInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { page } from "$app/state";
|
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 {
|
import {
|
||||||
CKActionType,
|
CKActionType,
|
||||||
SessionOutcome,
|
SessionOutcome,
|
||||||
@@ -6,21 +9,16 @@ import {
|
|||||||
type PendingAction,
|
type PendingAction,
|
||||||
type PendingActions,
|
type PendingActions,
|
||||||
} from "$lib/domains/ckflow/data/entities";
|
} from "$lib/domains/ckflow/data/entities";
|
||||||
|
import type { CustomerInfoModel } from "$lib/domains/customerinfo/data";
|
||||||
import {
|
import {
|
||||||
customerInfoModel,
|
CheckoutStep,
|
||||||
type CustomerInfo,
|
type OrderPriceDetailsModel,
|
||||||
} from "$lib/domains/passengerinfo/data/entities";
|
} 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 { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
||||||
import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
|
import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
|
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import {
|
import { productStore } from "$lib/domains/product/store";
|
||||||
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 { trpcApiStore } from "$lib/stores/api";
|
import { trpcApiStore } from "$lib/stores/api";
|
||||||
import { ClientLogger } from "@pkg/logger/client";
|
import { ClientLogger } from "@pkg/logger/client";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
@@ -185,7 +183,7 @@ export class CKFlowViewModel {
|
|||||||
private paymentInfoDebounceTimer: NodeJS.Timeout | null = null;
|
private paymentInfoDebounceTimer: NodeJS.Timeout | null = null;
|
||||||
syncInterval = 300; // 300ms debounce for syncing
|
syncInterval = 300; // 300ms debounce for syncing
|
||||||
|
|
||||||
updatedPrices = $state<FlightPriceDetails | undefined>(undefined);
|
updatedPrices = $state<OrderPriceDetailsModel | undefined>(undefined);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.actionRunner = new ActionRunner();
|
this.actionRunner = new ActionRunner();
|
||||||
@@ -216,17 +214,10 @@ export class CKFlowViewModel {
|
|||||||
return;
|
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({
|
const info = await api.ckflow.initiateCheckout.mutate({
|
||||||
domain: window.location.hostname,
|
domain: window.location.hostname,
|
||||||
refOIds,
|
refOIds: [],
|
||||||
ticketId: ticket.id,
|
productId: get(productStore)?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (info.error) {
|
if (info.error) {
|
||||||
@@ -236,7 +227,7 @@ export class CKFlowViewModel {
|
|||||||
|
|
||||||
if (!info.data) {
|
if (!info.data) {
|
||||||
toast.error("Error while creating checkout flow", {
|
toast.error("Error while creating checkout flow", {
|
||||||
description: "Try refreshing page or search for ticket again",
|
description: "Try refreshing page or contact us",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -248,7 +239,7 @@ export class CKFlowViewModel {
|
|||||||
this.setupDone = true;
|
this.setupDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncePersonalInfoSync(personalInfo: CustomerInfo) {
|
debouncePersonalInfoSync(personalInfo: CustomerInfoModel) {
|
||||||
this.clearPersonalInfoDebounce();
|
this.clearPersonalInfoDebounce();
|
||||||
this.personalInfoDebounceTimer = setTimeout(() => {
|
this.personalInfoDebounceTimer = setTimeout(() => {
|
||||||
this.syncPersonalInfo(personalInfo);
|
this.syncPersonalInfo(personalInfo);
|
||||||
@@ -262,9 +253,10 @@ export class CKFlowViewModel {
|
|||||||
this.paymentInfoDebounceTimer = setTimeout(() => {
|
this.paymentInfoDebounceTimer = setTimeout(() => {
|
||||||
const paymentInfo = {
|
const paymentInfo = {
|
||||||
cardDetails: paymentInfoVM.cardDetails,
|
cardDetails: paymentInfoVM.cardDetails,
|
||||||
flightTicketInfoId: get(flightTicketStore).id,
|
|
||||||
method: PaymentMethod.Card,
|
method: PaymentMethod.Card,
|
||||||
};
|
orderId: -1,
|
||||||
|
productId: get(productStore)?.id,
|
||||||
|
} as PaymentInfoPayload;
|
||||||
this.syncPaymentInfo(paymentInfo);
|
this.syncPaymentInfo(paymentInfo);
|
||||||
}, this.syncInterval);
|
}, this.syncInterval);
|
||||||
}
|
}
|
||||||
@@ -276,12 +268,12 @@ export class CKFlowViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPersonalInfoValid(personalInfo: CustomerInfo): boolean {
|
isPersonalInfoValid(personalInfo: CustomerInfoModel): boolean {
|
||||||
const parsed = customerInfoModel.safeParse(personalInfo);
|
const parsed = customerInfoModel.safeParse(personalInfo);
|
||||||
return !parsed.error && !!parsed.data;
|
return !parsed.error && !!parsed.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncPersonalInfo(personalInfo: CustomerInfo) {
|
async syncPersonalInfo(personalInfo: CustomerInfoModel) {
|
||||||
if (!this.flowId || !this.setupDone) {
|
if (!this.flowId || !this.setupDone) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -546,7 +538,7 @@ export class CKFlowViewModel {
|
|||||||
const out = await api.ckflow.executePrePaymentStep.mutate({
|
const out = await api.ckflow.executePrePaymentStep.mutate({
|
||||||
flowId: this.flowId!,
|
flowId: this.flowId!,
|
||||||
payload: {
|
payload: {
|
||||||
initialUrl: get(flightTicketStore).checkoutUrl,
|
initialUrl: "",
|
||||||
personalInfo: primaryPassengerInfo,
|
personalInfo: primaryPassengerInfo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -570,9 +562,10 @@ export class CKFlowViewModel {
|
|||||||
|
|
||||||
const paymentInfo = {
|
const paymentInfo = {
|
||||||
cardDetails: paymentInfoVM.cardDetails,
|
cardDetails: paymentInfoVM.cardDetails,
|
||||||
flightTicketInfoId: get(flightTicketStore).id,
|
|
||||||
method: PaymentMethod.Card,
|
method: PaymentMethod.Card,
|
||||||
};
|
orderId: -1,
|
||||||
|
productId: get(productStore)?.id,
|
||||||
|
} as PaymentInfoPayload;
|
||||||
|
|
||||||
const out = await api.ckflow.executePaymentStep.mutate({
|
const out = await api.ckflow.executePaymentStep.mutate({
|
||||||
flowId: this.flowId!,
|
flowId: this.flowId!,
|
||||||
|
|||||||
47
apps/frontend/src/lib/domains/customerinfo/controller.ts
Normal file
47
apps/frontend/src/lib/domains/customerinfo/controller.ts
Normal file
@@ -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<Result<number>> {
|
||||||
|
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<Result<CustomerInfoModel>> {
|
||||||
|
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<Result<CustomerInfoModel[]>> {
|
||||||
|
Logger.info("Retrieving all customer info records");
|
||||||
|
return this.repo.getAllCustomerInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/customerinfo/usecases";
|
|
||||||
@@ -4,102 +4,86 @@
|
|||||||
import Input from "$lib/components/ui/input/input.svelte";
|
import Input from "$lib/components/ui/input/input.svelte";
|
||||||
import * as Select from "$lib/components/ui/select";
|
import * as Select from "$lib/components/ui/select";
|
||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import type { SelectOption } from "$lib/core/data.types";
|
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
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 { 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 } =
|
let { info = $bindable() }: { info: CustomerInfoModel } = $props();
|
||||||
$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[];
|
|
||||||
|
|
||||||
function onSubmit(e: SubmitEvent) {
|
function onSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
passengerInfoVM.validatePII(info, idx);
|
customerInfoVM.validateCustomerInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
let validationTimeout = $state(undefined as undefined | NodeJS.Timer);
|
|
||||||
|
|
||||||
function debounceValidate() {
|
function debounceValidate() {
|
||||||
if (validationTimeout) {
|
customerInfoVM.debounceValidate(info);
|
||||||
clearTimeout(validationTimeout);
|
|
||||||
}
|
|
||||||
validationTimeout = setTimeout(() => {
|
|
||||||
passengerInfoVM.validatePII(info, idx);
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form action="#" class="flex w-full flex-col gap-4" onsubmit={onSubmit}>
|
<form action="#" class="flex w-full flex-col gap-4" onsubmit={onSubmit}>
|
||||||
|
<!-- Name Fields -->
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
<LabelWrapper
|
<LabelWrapper label="First Name" error={customerInfoVM.errors.firstName}>
|
||||||
label="First Name"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].firstName}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="First Name"
|
placeholder="First Name"
|
||||||
bind:value={info.firstName}
|
bind:value={info.firstName}
|
||||||
required
|
required
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
|
minlength={1}
|
||||||
|
maxlength={64}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<LabelWrapper
|
<LabelWrapper
|
||||||
label="Middle Name"
|
label="Middle Name"
|
||||||
error={passengerInfoVM.piiErrors[idx].middleName}
|
error={customerInfoVM.errors.middleName}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Middle Name"
|
placeholder="Middle Name (Optional)"
|
||||||
bind:value={info.middleName}
|
bind:value={info.middleName}
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
required
|
maxlength={64}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<LabelWrapper
|
<LabelWrapper label="Last Name" error={customerInfoVM.errors.lastName}>
|
||||||
label="Last Name"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].lastName}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Last Name"
|
placeholder="Last Name"
|
||||||
bind:value={info.lastName}
|
bind:value={info.lastName}
|
||||||
required
|
required
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
|
minlength={1}
|
||||||
|
maxlength={64}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabelWrapper label="Email" error={passengerInfoVM.piiErrors[idx].email}>
|
<!-- Email Field -->
|
||||||
|
<LabelWrapper label="Email" error={customerInfoVM.errors.email}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
bind:value={info.email}
|
bind:value={info.email}
|
||||||
type="email"
|
type="email"
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
required
|
required
|
||||||
|
maxlength={128}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row lg:flex-col xl:flex-row">
|
<!-- Phone Number Field -->
|
||||||
<LabelWrapper
|
<LabelWrapper label="Phone Number" error={customerInfoVM.errors.phoneNumber}>
|
||||||
label="Phone Number"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].phoneNumber}
|
|
||||||
>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Select.Root
|
<Select.Root
|
||||||
type="single"
|
type="single"
|
||||||
required
|
required
|
||||||
onValueChange={(code) => {
|
onValueChange={(code) => {
|
||||||
info.phoneCountryCode = code;
|
info.phoneCountryCode = code;
|
||||||
|
debounceValidate();
|
||||||
}}
|
}}
|
||||||
name="phoneCode"
|
name="phoneCode"
|
||||||
>
|
>
|
||||||
<Select.Trigger class="w-20">
|
<Select.Trigger class="w-28">
|
||||||
{#if info.phoneCountryCode}
|
{#if info.phoneCountryCode}
|
||||||
{info.phoneCountryCode}
|
{info.phoneCountryCode}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -124,118 +108,17 @@
|
|||||||
required
|
required
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</LabelWrapper>
|
|
||||||
|
|
||||||
<LabelWrapper
|
|
||||||
label="Passport Expiry"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].passportExpiry}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Passport Expiry"
|
|
||||||
value={info.passportExpiry}
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
oninput={(v) => {
|
|
||||||
// @ts-ignore
|
|
||||||
info.passportExpiry = v.target.value;
|
|
||||||
debounceValidate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LabelWrapper>
|
|
||||||
<LabelWrapper
|
|
||||||
label="Passport/ID No"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].passportNo}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Passport or ID card no."
|
|
||||||
bind:value={info.passportNo}
|
|
||||||
minlength={1}
|
minlength={1}
|
||||||
maxlength={20}
|
maxlength={20}
|
||||||
required
|
|
||||||
oninput={() => debounceValidate()}
|
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
|
||||||
</div>
|
</div>
|
||||||
|
</LabelWrapper>
|
||||||
|
|
||||||
|
<!-- Address Section -->
|
||||||
|
<Title size="h5">Address Information</Title>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
<LabelWrapper
|
<LabelWrapper label="Country" error={customerInfoVM.errors.country}>
|
||||||
label="Nationality"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].nationality}
|
|
||||||
>
|
|
||||||
<Select.Root
|
|
||||||
type="single"
|
|
||||||
required
|
|
||||||
onValueChange={(e) => {
|
|
||||||
info.nationality = e;
|
|
||||||
debounceValidate();
|
|
||||||
}}
|
|
||||||
name="role"
|
|
||||||
>
|
|
||||||
<Select.Trigger class="w-full">
|
|
||||||
{capitalize(
|
|
||||||
info.nationality.length > 0 ? info.nationality : "Select",
|
|
||||||
)}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each COUNTRIES_SELECT as country}
|
|
||||||
<Select.Item value={country.value}>
|
|
||||||
{country.label}
|
|
||||||
</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</LabelWrapper>
|
|
||||||
<LabelWrapper
|
|
||||||
label="Gender"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].gender}
|
|
||||||
>
|
|
||||||
<Select.Root
|
|
||||||
type="single"
|
|
||||||
required
|
|
||||||
onValueChange={(e) => {
|
|
||||||
info.gender = e as Gender;
|
|
||||||
debounceValidate();
|
|
||||||
}}
|
|
||||||
name="role"
|
|
||||||
>
|
|
||||||
<Select.Trigger class="w-full">
|
|
||||||
{capitalize(info.gender.length > 0 ? info.gender : "Select")}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each genderOpts as gender}
|
|
||||||
<Select.Item value={gender.value}>
|
|
||||||
{gender.label}
|
|
||||||
</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</LabelWrapper>
|
|
||||||
|
|
||||||
<LabelWrapper
|
|
||||||
label="Date of Birth"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].dob}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Date of Birth"
|
|
||||||
bind:value={info.dob}
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
oninput={() => debounceValidate()}
|
|
||||||
/>
|
|
||||||
</LabelWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- and now for the address info - country, state, city, zip code, address and address 2 -->
|
|
||||||
|
|
||||||
<Title size="h5">Address Info</Title>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
|
||||||
<LabelWrapper
|
|
||||||
label="Country"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].country}
|
|
||||||
>
|
|
||||||
<Select.Root
|
<Select.Root
|
||||||
type="single"
|
type="single"
|
||||||
required
|
required
|
||||||
@@ -243,7 +126,7 @@
|
|||||||
info.country = e;
|
info.country = e;
|
||||||
debounceValidate();
|
debounceValidate();
|
||||||
}}
|
}}
|
||||||
name="role"
|
name="country"
|
||||||
>
|
>
|
||||||
<Select.Trigger class="w-full">
|
<Select.Trigger class="w-full">
|
||||||
{capitalize(
|
{capitalize(
|
||||||
@@ -260,63 +143,60 @@
|
|||||||
</Select.Root>
|
</Select.Root>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<LabelWrapper label="State" error={passengerInfoVM.piiErrors[idx].state}>
|
<LabelWrapper label="State" error={customerInfoVM.errors.state}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="State"
|
placeholder="State/Province"
|
||||||
bind:value={info.state}
|
bind:value={info.state}
|
||||||
required
|
required
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
|
minlength={1}
|
||||||
|
maxlength={128}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
<LabelWrapper label="City" error={passengerInfoVM.piiErrors[idx].city}>
|
<LabelWrapper label="City" error={customerInfoVM.errors.city}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="City"
|
placeholder="City"
|
||||||
bind:value={info.city}
|
bind:value={info.city}
|
||||||
required
|
required
|
||||||
minlength={1}
|
minlength={1}
|
||||||
maxlength={80}
|
maxlength={128}
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<LabelWrapper
|
<LabelWrapper label="Zip Code" error={customerInfoVM.errors.zipCode}>
|
||||||
label="Zip Code"
|
|
||||||
error={passengerInfoVM.piiErrors[idx].zipCode}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Zip Code"
|
placeholder="Zip/Postal Code"
|
||||||
bind:value={info.zipCode}
|
bind:value={info.zipCode}
|
||||||
required
|
required
|
||||||
minlength={1}
|
minlength={1}
|
||||||
|
maxlength={21}
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
maxlength={12}
|
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabelWrapper label="Address" error={passengerInfoVM.piiErrors[idx].address}>
|
<LabelWrapper label="Address" error={customerInfoVM.errors.address}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Address"
|
placeholder="Street Address"
|
||||||
bind:value={info.address}
|
bind:value={info.address}
|
||||||
required
|
required
|
||||||
minlength={1}
|
minlength={1}
|
||||||
maxlength={128}
|
|
||||||
oninput={() => debounceValidate()}
|
oninput={() => debounceValidate()}
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
|
|
||||||
<LabelWrapper
|
<LabelWrapper
|
||||||
label="Address 2"
|
label="Address 2 (Optional)"
|
||||||
error={passengerInfoVM.piiErrors[idx].address2}
|
error={customerInfoVM.errors.address2}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Address 2"
|
placeholder="Apartment, suite, etc. (Optional)"
|
||||||
bind:value={info.address2}
|
bind:value={info.address2}
|
||||||
required
|
oninput={() => debounceValidate()}
|
||||||
minlength={1}
|
|
||||||
maxlength={128}
|
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -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 {
|
export class CustomerInfoViewModel {
|
||||||
customerInfos = $state([] as CustomerInfoModel[]);
|
// State: current customer info being edited/created (single customer for checkout)
|
||||||
|
customerInfo = $state<CustomerInfoModel | null>(null);
|
||||||
|
|
||||||
|
// State: validation errors for the current customer info
|
||||||
|
errors = $state<Partial<Record<keyof CustomerInfoModel, string>>>({});
|
||||||
|
|
||||||
|
// Loading states
|
||||||
loading = $state(false);
|
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<boolean> {
|
||||||
|
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<number | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<CustomerInfoModel>): 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<keyof CustomerInfoModel, string>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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<CustomerInfoModel>, 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();
|
export const customerInfoVM = new CustomerInfoViewModel();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getError, Logger } from "@pkg/logger";
|
|||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||||
import {
|
import {
|
||||||
passengerInfoModel,
|
passengerInfoModel,
|
||||||
type CustomerInfo,
|
type CustomerInfoModel,
|
||||||
type PassengerInfo,
|
type PassengerInfo,
|
||||||
} from "./entities";
|
} from "./entities";
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ export class PassengerInfoRepository {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPassengerPii(payload: CustomerInfo): Promise<Result<number>> {
|
async createPassengerPii(
|
||||||
|
payload: CustomerInfoModel,
|
||||||
|
): Promise<Result<number>> {
|
||||||
try {
|
try {
|
||||||
const out = await this.db
|
const out = await this.db
|
||||||
.insert(passengerPII)
|
.insert(passengerPII)
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { cn } from "$lib/utils";
|
|
||||||
import type { BagSelectionInfo } from "$lib/domains/ticket/data/entities/create.entities";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
|
||||||
import BagIcon from "~icons/lucide/briefcase";
|
|
||||||
import BackpackIcon from "~icons/solar/backpack-linear";
|
|
||||||
import SuitcaseIcon from "~icons/bi/suitcase2";
|
|
||||||
import CheckIcon from "~icons/solar/check-read-linear";
|
|
||||||
import { flightTicketStore } from "$lib/domains/ticket/data/store";
|
|
||||||
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
|
||||||
|
|
||||||
let { info = $bindable() }: { info: BagSelectionInfo } = $props();
|
|
||||||
|
|
||||||
// Average baseline price for checked bags when not specified
|
|
||||||
const BASELINE_CHECKED_BAG_PRICE = 30;
|
|
||||||
|
|
||||||
const getBagDetails = (type: string) => {
|
|
||||||
const bagsInfo = $flightTicketStore?.bagsInfo;
|
|
||||||
if (!bagsInfo) return null;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "personalBag":
|
|
||||||
return bagsInfo.details.personalBags;
|
|
||||||
case "handBag":
|
|
||||||
return bagsInfo.details.handBags;
|
|
||||||
case "checkedBag":
|
|
||||||
return bagsInfo.details.checkedBags;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDimensions = (details: any) => {
|
|
||||||
if (!details?.dimensions) return "";
|
|
||||||
const { length, width, height } = details.dimensions;
|
|
||||||
if (!length || !width || !height) return "";
|
|
||||||
return `${length} x ${width} x ${height} cm`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bagTypes = [
|
|
||||||
{
|
|
||||||
icon: BackpackIcon,
|
|
||||||
type: "personalBag",
|
|
||||||
label: "Personal Bag",
|
|
||||||
description: "Small item (purse, laptop bag)",
|
|
||||||
included: true,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: SuitcaseIcon,
|
|
||||||
type: "handBag",
|
|
||||||
label: "Cabin Bag",
|
|
||||||
description: "Carry-on luggage",
|
|
||||||
included: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: BagIcon,
|
|
||||||
type: "checkedBag",
|
|
||||||
label: "Checked Bag",
|
|
||||||
description: "Check-in luggage",
|
|
||||||
included: false,
|
|
||||||
disabled: false, // We'll always show this option
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function toggleBag(bagType: string) {
|
|
||||||
if (bagType === "handBag") {
|
|
||||||
info.handBags = info.handBags === 1 ? 0 : 1;
|
|
||||||
} else if (
|
|
||||||
bagType === "checkedBag" &&
|
|
||||||
$flightTicketStore?.bagsInfo.hasCheckedBagsSupport
|
|
||||||
) {
|
|
||||||
info.checkedBags = info.checkedBags === 1 ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBagSelected(bagType: string): boolean {
|
|
||||||
if (bagType === "personalBag") return true;
|
|
||||||
if (bagType === "handBag") return info.handBags === 1;
|
|
||||||
if (bagType === "checkedBag")
|
|
||||||
return (
|
|
||||||
info.checkedBags === 1 &&
|
|
||||||
$flightTicketStore?.bagsInfo.hasCheckedBagsSupport
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBagPrice(bagType: string): {
|
|
||||||
price: number;
|
|
||||||
isEstimate: boolean;
|
|
||||||
} {
|
|
||||||
const bagDetails = getBagDetails(bagType);
|
|
||||||
|
|
||||||
if (bagType === "checkedBag") {
|
|
||||||
if (bagDetails && typeof bagDetails.price === "number") {
|
|
||||||
return { price: bagDetails.price, isEstimate: false };
|
|
||||||
}
|
|
||||||
return { price: BASELINE_CHECKED_BAG_PRICE, isEstimate: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
price: bagDetails?.price ?? 0,
|
|
||||||
isEstimate: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
{#each bagTypes as bag}
|
|
||||||
{@const bagDetails = getBagDetails(bag.type)}
|
|
||||||
{@const isAvailable = true}
|
|
||||||
{@const isDisabled = false}
|
|
||||||
<!-- {@const isAvailable =
|
|
||||||
bag.type !== "checkedBag" ||
|
|
||||||
$flightTicketStore?.bagsInfo.hasCheckedBagsSupport}
|
|
||||||
{@const isDisabled =
|
|
||||||
bag.disabled || (bag.type === "checkedBag" && !isAvailable)} -->
|
|
||||||
<!-- {@const { price, isEstimate } = getBagPrice(bag.type)} -->
|
|
||||||
<!-- {@const formattedPrice = convertAndFormatCurrency(price)} -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
"relative flex flex-col items-start gap-4 rounded-lg border-2 p-4 transition-all sm:flex-row sm:items-center",
|
|
||||||
isBagSelected(bag.type)
|
|
||||||
? "border-primary bg-primary/5"
|
|
||||||
: "border-gray-200",
|
|
||||||
!isDisabled &&
|
|
||||||
!bag.included &&
|
|
||||||
"cursor-pointer hover:border-primary/50",
|
|
||||||
isDisabled && "opacity-70",
|
|
||||||
)}
|
|
||||||
role={isDisabled || bag.included ? "presentation" : "button"}
|
|
||||||
onclick={() => !isDisabled && !bag.included && toggleBag(bag.type)}
|
|
||||||
onkeydown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
!isDisabled && !bag.included && toggleBag(bag.type);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="shrink-0">
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
"grid h-10 w-10 place-items-center rounded-full sm:h-12 sm:w-12",
|
|
||||||
isBagSelected(bag.type)
|
|
||||||
? "bg-primary text-white"
|
|
||||||
: "bg-gray-100 text-gray-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon={bag.icon} cls="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-1">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium">{bag.label}</span>
|
|
||||||
{#if bag.included}
|
|
||||||
<span
|
|
||||||
class="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
|
|
||||||
>
|
|
||||||
Included
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if bag.type === "checkedBag" && !isAvailable}
|
|
||||||
<span
|
|
||||||
class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800"
|
|
||||||
>
|
|
||||||
Not available for this flight
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-gray-500">{bag.description}</span>
|
|
||||||
{#if bagDetails}
|
|
||||||
<div
|
|
||||||
class="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500"
|
|
||||||
>
|
|
||||||
{#if bagDetails.weight > 0}
|
|
||||||
<span>
|
|
||||||
Up to {bagDetails.weight}{bagDetails.unit}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if formatDimensions(bagDetails)}
|
|
||||||
<span>{formatDimensions(bagDetails)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-auto flex items-center">
|
|
||||||
<!-- {#if price > 0}
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<span class="font-medium text-primary"
|
|
||||||
>+{formattedPrice}</span
|
|
||||||
>
|
|
||||||
{#if isEstimate}
|
|
||||||
<span class="text-xs text-gray-500"
|
|
||||||
>Estimated price</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else if !bag.included} -->
|
|
||||||
<span class="text-xs font-medium text-emerald-800">Free</span>
|
|
||||||
<!-- {/if} -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if isBagSelected(bag.type)}
|
|
||||||
<div class="absolute right-4 top-4">
|
|
||||||
<Icon icon={CheckIcon} cls="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import type { SelectOption } from "$lib/core/data.types";
|
import type { SelectOption } from "$lib/core/data.types";
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
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 { Gender } from "$lib/domains/ticket/data/entities/index";
|
||||||
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
|
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
|
||||||
import { passengerInfoVM } from "./passenger.info.vm.svelte";
|
import { passengerInfoVM } from "./passenger.info.vm.svelte";
|
||||||
|
|
||||||
let { info = $bindable(), idx }: { info: CustomerInfo; idx: number } =
|
let { info = $bindable(), idx }: { info: CustomerInfoModel; idx: number } =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
const genderOpts = [
|
const genderOpts = [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
customerInfoModel,
|
customerInfoModel,
|
||||||
type BagSelectionInfo,
|
type BagSelectionInfo,
|
||||||
type CustomerInfo,
|
type CustomerInfoModel,
|
||||||
type PassengerInfo,
|
type PassengerInfo,
|
||||||
type SeatSelectionInfo,
|
type SeatSelectionInfo,
|
||||||
} from "$lib/domains/ticket/data/entities/create.entities";
|
} from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
@@ -17,7 +17,9 @@ import { z } from "zod";
|
|||||||
export class PassengerInfoViewModel {
|
export class PassengerInfoViewModel {
|
||||||
passengerInfos = $state<PassengerInfo[]>([]);
|
passengerInfos = $state<PassengerInfo[]>([]);
|
||||||
|
|
||||||
piiErrors = $state<Array<Partial<Record<keyof CustomerInfo, string>>>>([]);
|
piiErrors = $state<Array<Partial<Record<keyof CustomerInfoModel, string>>>>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.passengerInfos = [];
|
this.passengerInfos = [];
|
||||||
@@ -47,7 +49,7 @@ export class PassengerInfoViewModel {
|
|||||||
// zipCode: "123098",
|
// zipCode: "123098",
|
||||||
// address: "address",
|
// address: "address",
|
||||||
// address2: "",
|
// address2: "",
|
||||||
// } as CustomerInfo;
|
// } as CustomerInfoModel;
|
||||||
|
|
||||||
const _defaultPiiObj = {
|
const _defaultPiiObj = {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
@@ -67,7 +69,7 @@ export class PassengerInfoViewModel {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
address: "",
|
address: "",
|
||||||
address2: "",
|
address2: "",
|
||||||
} as CustomerInfo;
|
} as CustomerInfoModel;
|
||||||
|
|
||||||
const _defaultPriceObj = {
|
const _defaultPriceObj = {
|
||||||
currency: "",
|
currency: "",
|
||||||
@@ -137,7 +139,7 @@ export class PassengerInfoViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePII(info: CustomerInfo, idx: number) {
|
validatePII(info: CustomerInfoModel, idx: number) {
|
||||||
try {
|
try {
|
||||||
const result = customerInfoModel.parse(info);
|
const result = customerInfoModel.parse(info);
|
||||||
this.piiErrors[idx] = {};
|
this.piiErrors[idx] = {};
|
||||||
@@ -146,11 +148,11 @@ export class PassengerInfoViewModel {
|
|||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
this.piiErrors[idx] = error.errors.reduce(
|
this.piiErrors[idx] = error.errors.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const path = curr.path[0] as keyof CustomerInfo;
|
const path = curr.path[0] as keyof CustomerInfoModel;
|
||||||
acc[path] = curr.message;
|
acc[path] = curr.message;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<keyof CustomerInfo, string>,
|
{} as Record<keyof CustomerInfoModel, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
4
apps/frontend/src/lib/domains/product/store.ts
Normal file
4
apps/frontend/src/lib/domains/product/store.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { ProductModel } from "./data";
|
||||||
|
|
||||||
|
export const productStore = writable<ProductModel | null>(null);
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
import * as Select from "$lib/components/ui/select";
|
import * as Select from "$lib/components/ui/select";
|
||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
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";
|
import { billingDetailsVM } from "./billing.details.vm.svelte";
|
||||||
|
|
||||||
let { info = $bindable() }: { info: CustomerInfo } = $props();
|
let { info = $bindable() }: { info: CustomerInfoModel } = $props();
|
||||||
|
|
||||||
function onSubmit(e: SubmitEvent) {
|
function onSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
customerInfoModel,
|
customerInfoModel,
|
||||||
type CustomerInfo,
|
type CustomerInfoModel,
|
||||||
} from "$lib/domains/ticket/data/entities/create.entities";
|
} from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export class BillingDetailsViewModel {
|
export class BillingDetailsViewModel {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
billingDetails = $state<CustomerInfo>(undefined);
|
billingDetails = $state<CustomerInfoModel>(undefined);
|
||||||
|
|
||||||
piiErrors = $state<Partial<Record<keyof CustomerInfo, string>>>({});
|
piiErrors = $state<Partial<Record<keyof CustomerInfoModel, string>>>({});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -34,15 +34,15 @@ export class BillingDetailsViewModel {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
address: "",
|
address: "",
|
||||||
address2: "",
|
address2: "",
|
||||||
} as CustomerInfo;
|
} as CustomerInfoModel;
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPII(info: CustomerInfo) {
|
setPII(info: CustomerInfoModel) {
|
||||||
this.billingDetails = info;
|
this.billingDetails = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePII(info: CustomerInfo) {
|
validatePII(info: CustomerInfoModel) {
|
||||||
try {
|
try {
|
||||||
const result = customerInfoModel.parse(info);
|
const result = customerInfoModel.parse(info);
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
@@ -51,11 +51,11 @@ export class BillingDetailsViewModel {
|
|||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
this.piiErrors = error.errors.reduce(
|
this.piiErrors = error.errors.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const path = curr.path[0] as keyof CustomerInfo;
|
const path = curr.path[0] as keyof CustomerInfoModel;
|
||||||
acc[path] = curr.message;
|
acc[path] = curr.message;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<keyof CustomerInfo, string>,
|
{} as Record<keyof CustomerInfoModel, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { authRouter } from "$lib/domains/auth/domain/router";
|
import { authRouter } from "$lib/domains/auth/domain/router";
|
||||||
import { ckflowRouter } from "$lib/domains/ckflow/domain/router";
|
import { ckflowRouter } from "$lib/domains/ckflow/domain/router";
|
||||||
import { currencyRouter } from "$lib/domains/currency/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 { orderRouter } from "$lib/domains/order/domain/router";
|
||||||
import { productRouter } from "$lib/domains/product/router";
|
import { productRouter } from "$lib/domains/product/router";
|
||||||
import { userRouter } from "$lib/domains/user/domain/router";
|
import { userRouter } from "$lib/domains/user/domain/router";
|
||||||
@@ -13,6 +14,7 @@ export const router = createTRPCRouter({
|
|||||||
order: orderRouter,
|
order: orderRouter,
|
||||||
ckflow: ckflowRouter,
|
ckflow: ckflowRouter,
|
||||||
product: productRouter,
|
product: productRouter,
|
||||||
|
customerInfo: customerInfoRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Router = typeof router;
|
export type Router = typeof router;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { CustomerInfoModel, customerInfoModel } from "../../customerinfo/data";
|
||||||
import { CheckoutStep } from "../../order/data/enums";
|
import { CheckoutStep } from "../../order/data/enums";
|
||||||
import {
|
|
||||||
CustomerInfo,
|
|
||||||
customerInfoModel,
|
|
||||||
} from "../../passengerinfo/data/entities";
|
|
||||||
import {
|
import {
|
||||||
PaymentInfoPayload,
|
PaymentInfoPayload,
|
||||||
paymentInfoPayloadModel,
|
paymentInfoPayloadModel,
|
||||||
@@ -77,7 +74,7 @@ export const flowInfoModel = z.object({
|
|||||||
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
||||||
|
|
||||||
pendingActions: pendingActionsModel.default([]),
|
pendingActions: pendingActionsModel.default([]),
|
||||||
personalInfo: z.custom<CustomerInfo>().optional(),
|
personalInfo: z.custom<CustomerInfoModel>().optional(),
|
||||||
paymentInfo: z.custom<PaymentInfoPayload>().optional(),
|
paymentInfo: z.custom<PaymentInfoPayload>().optional(),
|
||||||
refOids: z.array(z.number()).optional(),
|
refOids: z.array(z.number()).optional(),
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export enum Gender {
|
||||||
|
Male = "male",
|
||||||
|
Female = "female",
|
||||||
|
Other = "other",
|
||||||
|
}
|
||||||
|
|
||||||
export const customerInfoModel = z.object({
|
export const customerInfoModel = z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
firstName: z.string().min(1).max(64),
|
firstName: z.string().min(1).max(64),
|
||||||
|
|||||||
@@ -21,14 +21,20 @@ export enum OrderStatus {
|
|||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const orderModel = z.object({
|
export const orderPriceDetailsModel = z.object({
|
||||||
id: z.coerce.number().int().positive(),
|
currency: z.string(),
|
||||||
|
|
||||||
discountAmount: z.coerce.number().min(0),
|
discountAmount: z.coerce.number().min(0),
|
||||||
basePrice: z.coerce.number().min(0),
|
basePrice: z.coerce.number().min(0),
|
||||||
displayPrice: z.coerce.number().min(0),
|
displayPrice: z.coerce.number().min(0),
|
||||||
orderPrice: z.coerce.number().min(0),
|
orderPrice: z.coerce.number().min(0),
|
||||||
fullfilledPrice: z.coerce.number().min(0),
|
fullfilledPrice: z.coerce.number().min(0),
|
||||||
|
});
|
||||||
|
export type OrderPriceDetailsModel = z.infer<typeof orderPriceDetailsModel>;
|
||||||
|
|
||||||
|
export const orderModel = z.object({
|
||||||
|
id: z.coerce.number().int().positive(),
|
||||||
|
|
||||||
|
...orderPriceDetailsModel.shape,
|
||||||
|
|
||||||
status: z.nativeEnum(OrderStatus),
|
status: z.nativeEnum(OrderStatus),
|
||||||
|
|
||||||
@@ -119,5 +125,6 @@ export const createOrderPayloadModel = z.object({
|
|||||||
customerInfo: customerInfoModel,
|
customerInfo: customerInfoModel,
|
||||||
paymentInfo: paymentInfoPayloadModel.optional(),
|
paymentInfo: paymentInfoPayloadModel.optional(),
|
||||||
orderModel: newOrderModel,
|
orderModel: newOrderModel,
|
||||||
|
flowId: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
||||||
|
|||||||
Reference in New Issue
Block a user