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 ProductIcon from "~icons/solar/box-broken";
|
||||
import CreditCardIcon from "~icons/solar/card-broken";
|
||||
import EmailIcon from "~icons/solar/letter-broken";
|
||||
|
||||
let { order }: { order: FullOrderModel } = $props();
|
||||
|
||||
@@ -17,17 +16,6 @@
|
||||
</script>
|
||||
|
||||
<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 -->
|
||||
<div class={cardStyle}>
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -88,28 +88,4 @@
|
||||
</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>
|
||||
|
||||
@@ -81,14 +81,6 @@
|
||||
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",
|
||||
id: "actions",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 { customerInfoVM } from "$lib/domains/customerinfo/view/customerinfo.vm.svelte";
|
||||
import {
|
||||
@@ -10,104 +11,176 @@ import { trpcApiStore } from "$lib/stores/api";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
/**
|
||||
* CreateOrderViewModel manages the order creation flow for product checkout.
|
||||
* Handles step progression, validation, and order submission.
|
||||
*/
|
||||
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(true);
|
||||
// Loading state
|
||||
loading = $state(false);
|
||||
|
||||
/**
|
||||
* Sets the current order creation step
|
||||
* @param step - The step to navigate to
|
||||
*/
|
||||
setStep(step: OrderCreationStep) {
|
||||
if (step === OrderCreationStep.ACCOUNT_SELECTION && this.accountInfoOk) {
|
||||
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() {
|
||||
if (this.orderStep === OrderCreationStep.ACCOUNT_SELECTION) {
|
||||
this.orderStep = OrderCreationStep.TICKET_SELECTION;
|
||||
} else if (this.orderStep === OrderCreationStep.TICKET_SELECTION) {
|
||||
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
|
||||
} 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) {
|
||||
if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
|
||||
// Validate customer info before proceeding
|
||||
if (!this.isCustomerInfoValid()) {
|
||||
toast.error("Please complete customer information");
|
||||
return;
|
||||
}
|
||||
|
||||
let basePrice = 0;
|
||||
let displayPrice = 0;
|
||||
let discountAmount = 0;
|
||||
if (this.ticketInfo) {
|
||||
basePrice = this.ticketInfo.priceDetails.basePrice;
|
||||
displayPrice = this.ticketInfo.priceDetails.displayPrice;
|
||||
discountAmount = this.ticketInfo.priceDetails.discountAmount;
|
||||
this.orderStep = OrderCreationStep.PAYMENT;
|
||||
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
|
||||
this.orderStep = OrderCreationStep.SUMMARY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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({
|
||||
product: get(productStore),
|
||||
productId: get(productStore)?.id,
|
||||
product: product,
|
||||
productId: product.id,
|
||||
customerInfo: customerInfoVM.customerInfo,
|
||||
paymentInfo: billingDetailsVM.billingDetails,
|
||||
orderModel: {
|
||||
basePrice,
|
||||
displayPrice,
|
||||
discountAmount,
|
||||
flightTicketInfoId: 0,
|
||||
emailAccountId: 0,
|
||||
...priceDetails,
|
||||
productId: product.id,
|
||||
customerInfoId: customerInfoVM.customerInfo.id,
|
||||
paymentInfoId: undefined,
|
||||
},
|
||||
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) {
|
||||
return toast.error(out.error.message, {
|
||||
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",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
window.location.replace("/");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,9 @@ import { paymentInfoPayloadModel } from "../../paymentinfo/data/entities";
|
||||
import { productModel } from "../../product/data";
|
||||
|
||||
export enum OrderCreationStep {
|
||||
ACCOUNT_SELECTION = 0,
|
||||
TICKET_SELECTION = 1,
|
||||
// TODO: only keep these remove the above 2 steps
|
||||
CUSTOMER_INFO = 2,
|
||||
PAYMENT = 2,
|
||||
SUMMARY = 3,
|
||||
CUSTOMER_INFO = 0,
|
||||
PAYMENT = 1,
|
||||
SUMMARY = 2,
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
@@ -40,7 +37,6 @@ export const orderModel = z.object({
|
||||
|
||||
productId: z.number(),
|
||||
customerInfoId: z.number().nullish().optional(),
|
||||
emailAccountId: z.number().nullish().optional(),
|
||||
paymentInfoId: z.number().nullish().optional(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
@@ -115,7 +111,6 @@ export const newOrderModel = orderModel.pick({
|
||||
productId: true,
|
||||
customerInfoId: true,
|
||||
paymentInfoId: true,
|
||||
emailAccountId: true,
|
||||
});
|
||||
export type NewOrderModel = z.infer<typeof newOrderModel>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user