refactor: create order vm | remove: order email account id thingy admin-side

This commit is contained in:
user
2025-10-21 16:26:57 +03:00
parent 49abd1246b
commit 94bb51bdc7
5 changed files with 139 additions and 115 deletions

View File

@@ -6,7 +6,6 @@
import type { FullOrderModel } from "$lib/domains/order/data/entities"; import type { FullOrderModel } from "$lib/domains/order/data/entities";
import ProductIcon from "~icons/solar/box-broken"; import ProductIcon from "~icons/solar/box-broken";
import CreditCardIcon from "~icons/solar/card-broken"; import CreditCardIcon from "~icons/solar/card-broken";
import EmailIcon from "~icons/solar/letter-broken";
let { order }: { order: FullOrderModel } = $props(); let { order }: { order: FullOrderModel } = $props();
@@ -17,17 +16,6 @@
</script> </script>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
{#if order.emailAccountId}
<!-- Email Account Info -->
<div class={cardStyle}>
<div class="flex items-center gap-2">
<Icon icon={EmailIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Account Information</Title>
</div>
<p class="text-gray-800">Email Account ID: #{order.emailAccountId}</p>
</div>
{/if}
<!-- Product Info --> <!-- Product Info -->
<div class={cardStyle}> <div class={cardStyle}>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -88,28 +88,4 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Additional Info -->
{#if order.emailAccountId}
<div class={cardStyle}>
<div class="flex items-center gap-2">
<Icon icon={InfoIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Additional Information</Title>
</div>
<div class="flex flex-col gap-2 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600">Email Account ID</span>
<span class="font-medium">#{order.emailAccountId}</span>
</div>
{#if order.paymentInfoId}
<div class="flex items-center justify-between">
<span class="text-gray-600">Payment Info ID</span>
<span class="font-medium">#{order.paymentInfoId}</span>
</div>
{/if}
</div>
</div>
{/if}
</div> </div>

View File

@@ -81,14 +81,6 @@
return snakeToSpacedPascal(r.status.toLowerCase()); return snakeToSpacedPascal(r.status.toLowerCase());
}, },
}, },
{
header: "Order Type",
accessorKey: "ordertype",
cell: ({ row }) => {
const r = row.original as FullOrderModel;
return r.emailAccountId ? "Agent" : "Customer";
},
},
{ {
header: "Action", header: "Action",
id: "actions", id: "actions",

View File

@@ -1,4 +1,5 @@
import { billingDetailsVM } from "$lib/domains/checkout/payment-info-section/billing.details.vm.svelte"; import { billingDetailsVM } from "$lib/domains/checkout/payment-info-section/billing.details.vm.svelte";
import { calculateFinalPrices } from "$lib/domains/checkout/utils";
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte"; import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
import { customerInfoVM } from "$lib/domains/customerinfo/view/customerinfo.vm.svelte"; import { customerInfoVM } from "$lib/domains/customerinfo/view/customerinfo.vm.svelte";
import { import {
@@ -10,104 +11,176 @@ 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";
/**
* CreateOrderViewModel manages the order creation flow for product checkout.
* Handles step progression, validation, and order submission.
*/
export class CreateOrderViewModel { export class CreateOrderViewModel {
orderStep = $state(OrderCreationStep.ACCOUNT_SELECTION); // Current step in the order creation flow
orderStep = $state(OrderCreationStep.CUSTOMER_INFO);
passengerInfosOk = $state(false); // Loading state
loading = $state(false);
loading = $state(true);
/**
* Sets the current order creation step
* @param step - The step to navigate to
*/
setStep(step: OrderCreationStep) { setStep(step: OrderCreationStep) {
if (step === OrderCreationStep.ACCOUNT_SELECTION && this.accountInfoOk) {
this.orderStep = step; this.orderStep = step;
} else if (
step === OrderCreationStep.TICKET_SELECTION &&
this.ticketInfoOk
) {
this.orderStep = step;
} else {
this.orderStep = step;
}
} }
/**
* Advances to the next step in the order creation flow
*/
setNextStep() { setNextStep() {
if (this.orderStep === OrderCreationStep.ACCOUNT_SELECTION) { if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
this.orderStep = OrderCreationStep.TICKET_SELECTION; // Validate customer info before proceeding
} else if (this.orderStep === OrderCreationStep.TICKET_SELECTION) { if (!this.isCustomerInfoValid()) {
this.orderStep = OrderCreationStep.CUSTOMER_INFO; toast.error("Please complete customer information");
} else if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
this.orderStep = OrderCreationStep.SUMMARY;
} else {
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
}
}
setPrevStep() {
if (this.orderStep === OrderCreationStep.SUMMARY) {
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
} else if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
this.orderStep = OrderCreationStep.TICKET_SELECTION;
} else {
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
}
}
async createOrder() {
const api = get(trpcApiStore);
if (!api) {
return; return;
} }
this.orderStep = OrderCreationStep.PAYMENT;
let basePrice = 0; } else if (this.orderStep === OrderCreationStep.PAYMENT) {
let displayPrice = 0; this.orderStep = OrderCreationStep.SUMMARY;
let discountAmount = 0; }
if (this.ticketInfo) {
basePrice = this.ticketInfo.priceDetails.basePrice;
displayPrice = this.ticketInfo.priceDetails.displayPrice;
discountAmount = this.ticketInfo.priceDetails.discountAmount;
} }
/**
* Goes back to the previous step
*/
setPrevStep() {
if (this.orderStep === OrderCreationStep.SUMMARY) {
this.orderStep = OrderCreationStep.PAYMENT;
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
}
}
/**
* Validates if customer information is complete
* @returns true if customer info is valid, false otherwise
*/
isCustomerInfoValid(): boolean {
if (!customerInfoVM.customerInfo) {
return false;
}
return customerInfoVM.isValid();
}
/**
* Validates if product is selected
* @returns true if product exists, false otherwise
*/
isProductValid(): boolean {
const product = get(productStore);
return product !== null && product.id !== undefined;
}
/**
* Checks if order can be submitted (all validations pass)
* @returns true if order is ready to submit, false otherwise
*/
canSubmitOrder(): boolean {
return this.isProductValid() && this.isCustomerInfoValid();
}
/**
* Creates and submits the order
* @returns true if successful, false otherwise
*/
async createOrder(): Promise<boolean> {
const api = get(trpcApiStore);
if (!api) {
toast.error("API client not initialized");
return false;
}
const product = get(productStore);
if (!product || !customerInfoVM.customerInfo) {
toast.error("Missing required information", {
description: "Product or customer information is incomplete",
});
return false;
}
// Calculate price details from product
const priceDetails = calculateFinalPrices(
product,
customerInfoVM.customerInfo,
);
// Build the order payload
const parsed = createOrderPayloadModel.safeParse({ const parsed = createOrderPayloadModel.safeParse({
product: get(productStore), product: product,
productId: get(productStore)?.id, productId: product.id,
customerInfo: customerInfoVM.customerInfo, customerInfo: customerInfoVM.customerInfo,
paymentInfo: billingDetailsVM.billingDetails, paymentInfo: billingDetailsVM.billingDetails,
orderModel: { orderModel: {
basePrice, ...priceDetails,
displayPrice, productId: product.id,
discountAmount, customerInfoId: customerInfoVM.customerInfo.id,
flightTicketInfoId: 0, paymentInfoId: undefined,
emailAccountId: 0,
}, },
flowId: ckFlowVM.flowId, flowId: ckFlowVM.flowId,
}); });
if (parsed.error) {
console.log(parsed.error.errors);
const msg = parsed.error.errors[0].message;
return toast.error(msg);
}
this.loading = true;
const out = await api.order.createOrder.mutate(parsed.data);
this.loading = false;
console.log(out); if (parsed.error) {
console.error("Order payload validation error:", parsed.error.errors);
const msg = parsed.error.errors[0].message;
toast.error("Invalid order data", {
description: msg,
});
return false;
}
this.loading = true;
try {
const out = await api.order.createOrder.mutate(parsed.data);
if (out.error) { if (out.error) {
return toast.error(out.error.message, { toast.error(out.error.message, {
description: out.error.userHint, description: out.error.userHint,
}); });
} return false;
if (!out.data) {
return toast.error("Order likely failed to create", {
description:
"Please try again, or contact us to resolve the issue",
});
} }
toast.success("Order created successfully, redirecting"); if (!out.data) {
toast.error("Order creation failed", {
description:
"Please try again, or contact support if the issue persists",
});
return false;
}
toast.success("Order created successfully", {
description: "Please wait, redirecting...",
});
// Redirect to success page after a short delay
setTimeout(() => { setTimeout(() => {
window.location.replace("/"); window.location.href = `/order/success?id=${out.data}`;
}, 1000); }, 1000);
return true;
} catch (e) {
console.error("Order creation error:", e);
toast.error("An unexpected error occurred", {
description: "Please try again later",
});
return false;
} finally {
this.loading = false;
}
}
/**
* Resets the view model state
*/
reset() {
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
this.loading = false;
} }
} }

View File

@@ -6,12 +6,9 @@ import { paymentInfoPayloadModel } from "../../paymentinfo/data/entities";
import { productModel } from "../../product/data"; import { productModel } from "../../product/data";
export enum OrderCreationStep { export enum OrderCreationStep {
ACCOUNT_SELECTION = 0, CUSTOMER_INFO = 0,
TICKET_SELECTION = 1, PAYMENT = 1,
// TODO: only keep these remove the above 2 steps SUMMARY = 2,
CUSTOMER_INFO = 2,
PAYMENT = 2,
SUMMARY = 3,
} }
export enum OrderStatus { export enum OrderStatus {
@@ -40,7 +37,6 @@ export const orderModel = z.object({
productId: z.number(), productId: z.number(),
customerInfoId: z.number().nullish().optional(), customerInfoId: z.number().nullish().optional(),
emailAccountId: z.number().nullish().optional(),
paymentInfoId: z.number().nullish().optional(), paymentInfoId: z.number().nullish().optional(),
createdAt: z.coerce.string(), createdAt: z.coerce.string(),
@@ -115,7 +111,6 @@ export const newOrderModel = orderModel.pick({
productId: true, productId: true,
customerInfoId: true, customerInfoId: true,
paymentInfoId: true, paymentInfoId: true,
emailAccountId: true,
}); });
export type NewOrderModel = z.infer<typeof newOrderModel>; export type NewOrderModel = z.infer<typeof newOrderModel>;