refactor: create order vm | remove: order email account id thingy admin-side
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
return;
|
||||||
|
}
|
||||||
|
this.orderStep = OrderCreationStep.PAYMENT;
|
||||||
|
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
|
||||||
this.orderStep = OrderCreationStep.SUMMARY;
|
this.orderStep = OrderCreationStep.SUMMARY;
|
||||||
} else {
|
|
||||||
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goes back to the previous step
|
||||||
|
*/
|
||||||
setPrevStep() {
|
setPrevStep() {
|
||||||
if (this.orderStep === OrderCreationStep.SUMMARY) {
|
if (this.orderStep === OrderCreationStep.SUMMARY) {
|
||||||
|
this.orderStep = OrderCreationStep.PAYMENT;
|
||||||
|
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
|
||||||
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
|
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() {
|
/**
|
||||||
|
* 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);
|
const api = get(trpcApiStore);
|
||||||
if (!api) {
|
if (!api) {
|
||||||
return;
|
toast.error("API client not initialized");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let basePrice = 0;
|
const product = get(productStore);
|
||||||
let displayPrice = 0;
|
if (!product || !customerInfoVM.customerInfo) {
|
||||||
let discountAmount = 0;
|
toast.error("Missing required information", {
|
||||||
if (this.ticketInfo) {
|
description: "Product or customer information is incomplete",
|
||||||
basePrice = this.ticketInfo.priceDetails.basePrice;
|
});
|
||||||
displayPrice = this.ticketInfo.priceDetails.displayPrice;
|
return false;
|
||||||
discountAmount = this.ticketInfo.priceDetails.discountAmount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (parsed.error) {
|
||||||
console.log(parsed.error.errors);
|
console.error("Order payload validation error:", parsed.error.errors);
|
||||||
const msg = parsed.error.errors[0].message;
|
const msg = parsed.error.errors[0].message;
|
||||||
return toast.error(msg);
|
toast.error("Invalid order data", {
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const out = await api.order.createOrder.mutate(parsed.data);
|
|
||||||
|
try {
|
||||||
|
const out = await api.order.createOrder.mutate(parsed.data);
|
||||||
|
|
||||||
|
if (out.error) {
|
||||||
|
toast.error(out.error.message, {
|
||||||
|
description: out.error.userHint,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
window.location.href = `/order/success?id=${out.data}`;
|
||||||
|
}, 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;
|
this.loading = false;
|
||||||
|
|
||||||
console.log(out);
|
|
||||||
|
|
||||||
if (out.error) {
|
|
||||||
return toast.error(out.error.message, {
|
|
||||||
description: out.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.replace("/");
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user