💥💣 ALMOST THERE???? DUNNO PROBABLY - 90% done
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UserIcon from "~icons/solar/user-broken";
|
import UserIcon from "~icons/solar/user-broken";
|
||||||
import type { CustomerInfoModel } from "../data";
|
import type { CustomerInfoModel } from "../data";
|
||||||
import InfoCard from "./info-card.svelte";
|
import CInfoCard from "./cinfo-card.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
customerInfo,
|
customerInfo,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InfoCard icon={UserIcon} title="Customer Information">
|
<CInfoCard icon={UserIcon} title="Customer Information">
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-xs text-gray-500">Full Name</span>
|
<span class="text-xs text-gray-500">Full Name</span>
|
||||||
@@ -54,4 +54,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</InfoCard>
|
</CInfoCard>
|
||||||
|
|||||||
@@ -45,3 +45,7 @@ export class CustomerInfoController {
|
|||||||
return this.repo.getAllCustomerInfo();
|
return this.repo.getAllCustomerInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCustomerInfoController() {
|
||||||
|
return new CustomerInfoController(new CustomerInfoRepository(db));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { ERROR_CODES, type Result } from "$lib/core/data.types";
|
import { ERROR_CODES, type Result } from "$lib/core/data.types";
|
||||||
import { and, eq, isNotNull, or, type Database } from "@pkg/db";
|
import { and, eq, isNotNull, or, type Database } from "@pkg/db";
|
||||||
import { order, passengerInfo } from "@pkg/db/schema";
|
import { order } from "@pkg/db/schema";
|
||||||
import { getError, Logger } from "@pkg/logger";
|
import { getError, Logger } from "@pkg/logger";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
fullOrderModel,
|
fullOrderModel,
|
||||||
limitedOrderWithTicketInfoModel,
|
limitedOrderWithProductModel,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
type FullOrderModel,
|
type FullOrderModel,
|
||||||
type LimitedOrderWithTicketInfoModel,
|
type LimitedOrderWithProductModel,
|
||||||
type NewOrderModel,
|
type NewOrderModel,
|
||||||
} from "./entities";
|
} from "./entities";
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ export class OrderRepository {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listActiveOrders(): Promise<Result<LimitedOrderWithTicketInfoModel[]>> {
|
async listActiveOrders(): Promise<Result<LimitedOrderWithProductModel[]>> {
|
||||||
const conditions = [
|
const conditions = [
|
||||||
or(
|
or(
|
||||||
eq(order.status, OrderStatus.PENDING_FULLFILLMENT),
|
eq(order.status, OrderStatus.PENDING_FULFILLMENT),
|
||||||
eq(order.status, OrderStatus.PARTIALLY_FULFILLED),
|
eq(order.status, OrderStatus.PARTIALLY_FULFILLED),
|
||||||
),
|
),
|
||||||
isNotNull(order.agentId),
|
isNotNull(order.agentId),
|
||||||
@@ -34,29 +34,29 @@ export class OrderRepository {
|
|||||||
displayPrice: true,
|
displayPrice: true,
|
||||||
basePrice: true,
|
basePrice: true,
|
||||||
discountAmount: true,
|
discountAmount: true,
|
||||||
pricePerPassenger: true,
|
orderPrice: true,
|
||||||
fullfilledPrice: true,
|
fullfilledPrice: true,
|
||||||
status: true,
|
status: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
flightTicketInfo: {
|
product: true,
|
||||||
|
customerInfo: {
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
firstName: true,
|
||||||
departure: true,
|
middleName: true,
|
||||||
arrival: true,
|
lastName: true,
|
||||||
departureDate: true,
|
email: true,
|
||||||
returnDate: true,
|
country: true,
|
||||||
flightType: true,
|
state: true,
|
||||||
passengerCounts: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const out = [] as LimitedOrderWithTicketInfoModel[];
|
const out = [] as LimitedOrderWithProductModel[];
|
||||||
|
|
||||||
for (const order of qRes) {
|
for (const order of qRes) {
|
||||||
const parsed = limitedOrderWithTicketInfoModel.safeParse({
|
const parsed = limitedOrderWithProductModel.safeParse({
|
||||||
...order,
|
...order,
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
@@ -78,21 +78,13 @@ export class OrderRepository {
|
|||||||
return { data: out };
|
return { data: out };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderByPNR(pnr: string): Promise<Result<FullOrderModel>> {
|
async getOrderByUID(uid: string): Promise<Result<FullOrderModel>> {
|
||||||
const out = await this.db.query.order.findFirst({
|
const out = await this.db.query.order.findFirst({
|
||||||
where: eq(order.pnr, pnr),
|
where: eq(order.uid, uid),
|
||||||
with: { flightTicketInfo: true },
|
with: { customerInfo: true, product: true },
|
||||||
});
|
});
|
||||||
if (!out) return {};
|
if (!out) return {};
|
||||||
const relatedPassengerInfos = await this.db.query.passengerInfo.findMany({
|
const parsed = fullOrderModel.safeParse({ ...out });
|
||||||
where: eq(passengerInfo.orderId, out.id),
|
|
||||||
with: { passengerPii: true },
|
|
||||||
});
|
|
||||||
const parsed = fullOrderModel.safeParse({
|
|
||||||
...out,
|
|
||||||
emailAccount: undefined,
|
|
||||||
passengerInfos: relatedPassengerInfos,
|
|
||||||
});
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return {
|
return {
|
||||||
error: getError(
|
error: getError(
|
||||||
@@ -111,21 +103,25 @@ export class OrderRepository {
|
|||||||
|
|
||||||
async createOrder(
|
async createOrder(
|
||||||
payload: NewOrderModel,
|
payload: NewOrderModel,
|
||||||
): Promise<Result<{ id: number; pnr: string }>> {
|
): Promise<Result<{ id: number; uid: string }>> {
|
||||||
const pnr = nanoid(9).toUpperCase();
|
const uid = nanoid(12).toUpperCase();
|
||||||
try {
|
try {
|
||||||
const out = await this.db
|
const out = await this.db
|
||||||
.insert(order)
|
.insert(order)
|
||||||
.values({
|
.values({
|
||||||
|
uid,
|
||||||
displayPrice: payload.displayPrice.toFixed(3),
|
displayPrice: payload.displayPrice.toFixed(3),
|
||||||
basePrice: payload.basePrice.toFixed(3),
|
basePrice: payload.basePrice.toFixed(3),
|
||||||
discountAmount: payload.discountAmount.toFixed(3),
|
discountAmount: payload.discountAmount.toFixed(3),
|
||||||
|
fullfilledPrice: payload.fullfilledPrice.toFixed(3),
|
||||||
|
orderPrice: payload.orderPrice.toFixed(3),
|
||||||
|
|
||||||
flightTicketInfoId: payload.flightTicketInfoId,
|
|
||||||
paymentInfoId: payload.paymentInfoId,
|
paymentInfoId: payload.paymentInfoId,
|
||||||
|
|
||||||
status: OrderStatus.PENDING_FULLFILLMENT,
|
status: OrderStatus.PENDING_FULFILLMENT,
|
||||||
pnr,
|
|
||||||
|
customerInfoId: payload.customerInfoId,
|
||||||
|
productId: payload.productId,
|
||||||
|
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -133,7 +129,7 @@ export class OrderRepository {
|
|||||||
.returning({ id: order.id })
|
.returning({ id: order.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return { data: { id: out[0]?.id, pnr } };
|
return { data: { id: out[0]?.id, uid } };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
error: getError(
|
error: getError(
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export class OrderController {
|
|||||||
return this.repo.createOrder(payload);
|
return this.repo.createOrder(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderByPNR(pnr: string) {
|
async getOrderByUID(uid: string) {
|
||||||
return this.repo.getOrderByPNR(pnr);
|
return this.repo.getOrderByUID(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async markOrdersAsFulfilled(oids: number[]) {
|
async markOrdersAsFulfilled(oids: number[]) {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { SessionOutcome } from "$lib/domains/ckflow/data/entities";
|
import { SessionOutcome } from "$lib/domains/ckflow/data/entities";
|
||||||
import { getCKUseCases } from "$lib/domains/ckflow/domain/usecases";
|
import { getCKUseCases } from "$lib/domains/ckflow/domain/usecases";
|
||||||
|
import { getCustomerInfoController } from "$lib/domains/customerinfo/controller";
|
||||||
import { EmailerUseCases } from "$lib/domains/email/domain/usecases";
|
import { EmailerUseCases } from "$lib/domains/email/domain/usecases";
|
||||||
import { createOrderPayloadModel } from "$lib/domains/order/data/entities";
|
import {
|
||||||
import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository";
|
CheckoutStep,
|
||||||
import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller";
|
createOrderPayloadModel,
|
||||||
|
} from "$lib/domains/order/data/entities";
|
||||||
import { PaymentInfoRepository } from "$lib/domains/paymentinfo/data/repository";
|
import { PaymentInfoRepository } from "$lib/domains/paymentinfo/data/repository";
|
||||||
import { PaymentInfoUseCases } from "$lib/domains/paymentinfo/domain/usecases";
|
import { PaymentInfoUseCases } from "$lib/domains/paymentinfo/domain/usecases";
|
||||||
import { CheckoutStep } from "$lib/domains/ticket/data/entities";
|
import { getProductUseCases } from "$lib/domains/product/usecases";
|
||||||
import { getTC } from "$lib/domains/ticket/domain/controller";
|
|
||||||
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
|
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
|
||||||
import { db } from "@pkg/db";
|
import { db } from "@pkg/db";
|
||||||
import { getError, Logger } from "@pkg/logger";
|
import { getError, Logger } from "@pkg/logger";
|
||||||
@@ -22,10 +23,8 @@ export const orderRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const pduc = new PaymentInfoUseCases(new PaymentInfoRepository(db));
|
const pduc = new PaymentInfoUseCases(new PaymentInfoRepository(db));
|
||||||
const oc = new OrderController(new OrderRepository(db));
|
const oc = new OrderController(new OrderRepository(db));
|
||||||
const pc = new PassengerInfoController(
|
const cc = getCustomerInfoController();
|
||||||
new PassengerInfoRepository(db),
|
const puc = getProductUseCases();
|
||||||
);
|
|
||||||
const tc = getTC();
|
|
||||||
const emailUC = new EmailerUseCases();
|
const emailUC = new EmailerUseCases();
|
||||||
|
|
||||||
const ftRes = await tc.uncacheAndSaveTicket(input.flightTicketId!);
|
const ftRes = await tc.uncacheAndSaveTicket(input.flightTicketId!);
|
||||||
@@ -97,14 +96,14 @@ export const orderRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pnr = out.data.pnr;
|
const uid = out.data.uid;
|
||||||
|
|
||||||
if (!pnr) {
|
if (!uid) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Get order details for email
|
// Get order details for email
|
||||||
const orderDetails = await oc.getOrderByPNR(pnr);
|
const orderDetails = await oc.getOrderByPNR(uid);
|
||||||
|
|
||||||
if (!orderDetails.data) {
|
if (!orderDetails.data) {
|
||||||
return out;
|
return out;
|
||||||
@@ -134,10 +133,10 @@ export const orderRouter = createTRPCRouter({
|
|||||||
// Send the email with React component directly
|
// Send the email with React component directly
|
||||||
const emailResult = await emailUC.sendEmailWithTemplate({
|
const emailResult = await emailUC.sendEmailWithTemplate({
|
||||||
to: passengerEmail,
|
to: passengerEmail,
|
||||||
subject: `Flight Confirmation: ${ticketInfo.departure} to ${ticketInfo.arrival} - PNR: ${pnr}`,
|
subject: `Flight Confirmation: ${ticketInfo.departure} to ${ticketInfo.arrival} - PNR: ${uid}`,
|
||||||
template: "pnr-confirmation",
|
template: "uid-confirmation",
|
||||||
templateData: {
|
templateData: {
|
||||||
pnr: pnr,
|
uid: uid,
|
||||||
origin: ticketInfo.departure,
|
origin: ticketInfo.departure,
|
||||||
destination: ticketInfo.arrival,
|
destination: ticketInfo.arrival,
|
||||||
departureDate: new Date(
|
departureDate: new Date(
|
||||||
@@ -175,10 +174,10 @@ export const orderRouter = createTRPCRouter({
|
|||||||
return out;
|
return out;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
findByPNR: publicProcedure
|
findByUID: publicProcedure
|
||||||
.input(z.object({ pnr: z.string() }))
|
.input(z.object({ uid: z.string() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const oc = new OrderController(new OrderRepository(db));
|
const oc = new OrderController(new OrderRepository(db));
|
||||||
return oc.getOrderByPNR(input.pnr);
|
return oc.getOrderByUID(input.uid);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
import Icon from "$lib/components/atoms/icon.svelte";
|
||||||
import EmailIcon from "~icons/solar/letter-broken";
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
import TicketIcon from "~icons/solar/ticket-broken";
|
|
||||||
import CreditCardIcon from "~icons/solar/card-broken";
|
|
||||||
import { Badge } from "$lib/components/ui/badge";
|
|
||||||
import type { FullOrderModel } from "$lib/domains/order/data/entities";
|
|
||||||
import TicketLegsOverview from "$lib/domains/ticket/view/ticket/ticket-legs-overview.svelte";
|
|
||||||
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
||||||
|
import type { FullOrderModel } from "$lib/domains/order/data/entities";
|
||||||
|
import CreditCardIcon from "~icons/solar/card-broken";
|
||||||
|
|
||||||
let { order }: { order: FullOrderModel } = $props();
|
let { order }: { order: FullOrderModel } = $props();
|
||||||
|
|
||||||
@@ -16,37 +12,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
{#if order.emailAccount}
|
<span>TODO: SHOW PRODUCT DETSIL INFO HERE</span>
|
||||||
<!-- 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">{order.emailAccount.email}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Flight Ticket Info -->
|
|
||||||
<div class={cardStyle}>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={TicketIcon} cls="w-5 h-5" />
|
|
||||||
<Title size="h5" color="black">Flight Details</Title>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{order.flightTicketInfo.flightType}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="secondary">
|
|
||||||
{order.flightTicketInfo.cabinClass}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TicketLegsOverview data={order.flightTicketInfo} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={cardStyle}>
|
<div class={cardStyle}>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { FullOrderModel } from "@pkg/logic/domains/order/data/entities";
|
|
||||||
import { trpcApiStore } from "$lib/stores/api";
|
import { trpcApiStore } from "$lib/stores/api";
|
||||||
import { get } from "svelte/store";
|
import type { FullOrderModel } from "@pkg/logic/domains/order/data/entities";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
export class TrackViewModel {
|
export class TrackViewModel {
|
||||||
pnr = $state("");
|
uid = $state("");
|
||||||
loading = $state(false);
|
loading = $state(false);
|
||||||
bookingData = $state<FullOrderModel | undefined>(undefined);
|
bookingData = $state<FullOrderModel | undefined>(undefined);
|
||||||
error = $state<string | null>(null);
|
error = $state<string | null>(null);
|
||||||
|
|
||||||
async searchBooking() {
|
async searchBooking() {
|
||||||
if (!this.pnr) {
|
if (!this.uid) {
|
||||||
this.error = "Please enter a PNR number";
|
this.error = "Please enter a PNR number";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export class TrackViewModel {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const result = await api.order.findByPNR.query({ pnr: this.pnr });
|
const result = await api.order.findByUID.query({ uid: this.uid });
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
this.error = result.error.message;
|
this.error = result.error.message;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "$lib/domains/passengerinfo/data/entities";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/ticket/data/entities/enums";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/ticket/data/entities/index";
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
import { round } from "$lib/core/num.utils";
|
|
||||||
import {
|
|
||||||
getCKUseCases,
|
|
||||||
type CheckoutFlowUseCases,
|
|
||||||
} from "$lib/domains/ckflow/domain/usecases";
|
|
||||||
import { and, arrayContains, eq, or, type Database } from "@pkg/db";
|
|
||||||
import { flightTicketInfo, order } from "@pkg/db/schema";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import {
|
|
||||||
flightPriceDetailsModel,
|
|
||||||
flightTicketModel,
|
|
||||||
TicketType,
|
|
||||||
type FlightPriceDetails,
|
|
||||||
type FlightTicket,
|
|
||||||
type TicketSearchDTO,
|
|
||||||
} from "./entities";
|
|
||||||
import type { ScrapedTicketsDataSource } from "./scrape.data.source";
|
|
||||||
|
|
||||||
export class TicketRepository {
|
|
||||||
private db: Database;
|
|
||||||
private ckUseCases: CheckoutFlowUseCases;
|
|
||||||
private scraper: ScrapedTicketsDataSource;
|
|
||||||
|
|
||||||
constructor(db: Database, scraper: ScrapedTicketsDataSource) {
|
|
||||||
this.db = db;
|
|
||||||
this.scraper = scraper;
|
|
||||||
this.ckUseCases = getCKUseCases();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCheckoutUrlByRefOId(refOId: number): Promise<Result<string>> {
|
|
||||||
try {
|
|
||||||
const _o = await this.db.query.order.findFirst({
|
|
||||||
where: eq(order.id, refOId),
|
|
||||||
with: {
|
|
||||||
flightTicketInfo: {
|
|
||||||
columns: { id: true, checkoutUrl: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const chktUrl = _o?.flightTicketInfo?.checkoutUrl;
|
|
||||||
console.log(_o);
|
|
||||||
return { data: chktUrl };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
|
||||||
message: "Failed to fetch ticket url",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
detail: "An error occurred while fetching ticket url",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketIdbyRefOid(refOid: number): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: arrayContains(flightTicketInfo.refOIds, [refOid]),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
if (out && out.id) {
|
|
||||||
return { data: out?.id };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
} catch (e) {
|
|
||||||
Logger.debug(refOid);
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.DATABASE_ERROR,
|
|
||||||
message: "Failed to lookup ticket",
|
|
||||||
detail: "A database error occured while getting ticket by id",
|
|
||||||
userHint: "Contact us to resolve this issue",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchDTO,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
try {
|
|
||||||
let out = await this.scraper.searchForTickets(payload);
|
|
||||||
if (out.error || (out.data && out.data.length < 1)) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NETWORK_ERROR,
|
|
||||||
message: "Failed to search for tickets",
|
|
||||||
detail: "Could not fetch tickets for the given payload",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: err,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketById(id: number): Promise<Result<FlightTicket>> {
|
|
||||||
const out = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: eq(flightTicketInfo.id, id),
|
|
||||||
});
|
|
||||||
if (!out) {
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
|
||||||
message: "Ticket not found",
|
|
||||||
userHint:
|
|
||||||
"Please check if the selected ticket is correct, or try again",
|
|
||||||
detail: "The ticket is not found by the id provided",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const parsed = flightTicketModel.safeParse(out);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to parse ticket",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to parse ticket",
|
|
||||||
},
|
|
||||||
JSON.stringify(parsed.error.errors),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { data: parsed.data };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketIdByInfo(info: {
|
|
||||||
ticketId: string;
|
|
||||||
arrival: string;
|
|
||||||
departure: string;
|
|
||||||
cabinClass: string;
|
|
||||||
departureDate: string;
|
|
||||||
returnDate: string;
|
|
||||||
}): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: or(
|
|
||||||
eq(flightTicketInfo.ticketId, info.ticketId),
|
|
||||||
and(
|
|
||||||
eq(flightTicketInfo.arrival, info.arrival),
|
|
||||||
eq(flightTicketInfo.departure, info.departure),
|
|
||||||
eq(flightTicketInfo.cabinClass, info.cabinClass),
|
|
||||||
eq(
|
|
||||||
flightTicketInfo.departureDate,
|
|
||||||
new Date(info.departureDate),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
if (out && out.id) {
|
|
||||||
return { data: out?.id };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
} catch (e) {
|
|
||||||
Logger.debug(info);
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.DATABASE_ERROR,
|
|
||||||
message: "Failed to lookup ticket",
|
|
||||||
detail: "A database error occured while getting ticket by id",
|
|
||||||
userHint: "Contact us to resolve this issue",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTicket(
|
|
||||||
payload: FlightTicket,
|
|
||||||
isCache = false,
|
|
||||||
): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
let rd = new Date();
|
|
||||||
if (
|
|
||||||
payload.returnDate &&
|
|
||||||
payload.returnDate.length > 0 &&
|
|
||||||
payload.flightType !== TicketType.OneWay
|
|
||||||
) {
|
|
||||||
rd = new Date(payload.returnDate);
|
|
||||||
}
|
|
||||||
const out = await this.db
|
|
||||||
.insert(flightTicketInfo)
|
|
||||||
.values({
|
|
||||||
ticketId: payload.ticketId,
|
|
||||||
flightType: payload.flightType,
|
|
||||||
|
|
||||||
arrival: payload.arrival,
|
|
||||||
departure: payload.departure,
|
|
||||||
cabinClass: payload.cabinClass,
|
|
||||||
departureDate: new Date(payload.departureDate),
|
|
||||||
returnDate: rd,
|
|
||||||
|
|
||||||
priceDetails: payload.priceDetails,
|
|
||||||
passengerCounts: payload.passengerCounts,
|
|
||||||
bagsInfo: payload.bagsInfo,
|
|
||||||
|
|
||||||
lastAvailable: payload.lastAvailable,
|
|
||||||
|
|
||||||
flightIteneraries: payload.flightIteneraries,
|
|
||||||
|
|
||||||
dates: payload.dates,
|
|
||||||
|
|
||||||
checkoutUrl: payload.checkoutUrl ?? "",
|
|
||||||
refOIds: payload.refOIds ?? [],
|
|
||||||
|
|
||||||
refundable: payload.refundable ?? false,
|
|
||||||
isCache: isCache ? true : (payload.isCache ?? isCache),
|
|
||||||
|
|
||||||
shareId: payload.shareId,
|
|
||||||
})
|
|
||||||
.returning({ id: flightTicketInfo.id })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!out || out.length === 0) {
|
|
||||||
Logger.error("Failed to create ticket");
|
|
||||||
Logger.debug(out);
|
|
||||||
Logger.debug(payload);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to create ticket",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to create ticket",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: out[0].id };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while creating ticket",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while creating ticket",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTicketPrices(
|
|
||||||
tid: number,
|
|
||||||
payload: FlightPriceDetails,
|
|
||||||
): Promise<Result<FlightPriceDetails>> {
|
|
||||||
const cond = eq(flightTicketInfo.id, tid);
|
|
||||||
const currInfo = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: cond,
|
|
||||||
columns: { priceDetails: true },
|
|
||||||
});
|
|
||||||
if (!currInfo) {
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
|
||||||
message: "Could not find ticket",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
detail: "Could not fin the ticket by the provided id",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const info = flightPriceDetailsModel.parse(currInfo.priceDetails);
|
|
||||||
Logger.info("Updating the price details from:");
|
|
||||||
Logger.debug(info);
|
|
||||||
const discountAmt = round(info.basePrice - payload.displayPrice);
|
|
||||||
const newInfo = {
|
|
||||||
...info,
|
|
||||||
discountAmount: discountAmt,
|
|
||||||
displayPrice: payload.displayPrice,
|
|
||||||
} as FlightPriceDetails;
|
|
||||||
Logger.info("... to:");
|
|
||||||
Logger.debug(newInfo);
|
|
||||||
const out = await this.db
|
|
||||||
.update(flightTicketInfo)
|
|
||||||
.set({ priceDetails: newInfo })
|
|
||||||
.where(cond)
|
|
||||||
.execute();
|
|
||||||
Logger.info("Updated the price info");
|
|
||||||
Logger.debug(out);
|
|
||||||
return { data: newInfo };
|
|
||||||
}
|
|
||||||
|
|
||||||
async uncacheAndSaveTicket(tid: number): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db
|
|
||||||
.update(flightTicketInfo)
|
|
||||||
.set({ isCache: false })
|
|
||||||
.where(eq(flightTicketInfo.id, tid))
|
|
||||||
.returning({ id: flightTicketInfo.id })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!out || out.length === 0) {
|
|
||||||
Logger.error("Failed to uncache ticket");
|
|
||||||
Logger.debug(out);
|
|
||||||
Logger.debug(tid);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to process ticket at this moment",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
detail: "Failed to uncache ticket",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: out[0].id };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while uncaching ticket",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while uncaching ticket",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRefOIdsForTicket(
|
|
||||||
tid: number,
|
|
||||||
oids: number[],
|
|
||||||
): Promise<Result<boolean>> {
|
|
||||||
Logger.info(`Setting refOIds(${oids}) for ticket ${tid}`);
|
|
||||||
const out = await this.db
|
|
||||||
.update(flightTicketInfo)
|
|
||||||
.set({ refOIds: oids })
|
|
||||||
.where(eq(flightTicketInfo.id, tid))
|
|
||||||
.execute();
|
|
||||||
return { data: out.length > 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
async areCheckoutAlreadyLiveForOrder(refOids: number[]): Promise<boolean> {
|
|
||||||
return this.ckUseCases.areCheckoutAlreadyLiveForOrder(refOids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import type { FlightTicket, TicketSearchDTO } from "./entities";
|
|
||||||
import { betterFetch } from "@better-fetch/fetch";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
|
|
||||||
export class ScrapedTicketsDataSource {
|
|
||||||
scraperUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
|
|
||||||
constructor(scraperUrl: string, apiKey: string) {
|
|
||||||
this.scraperUrl = scraperUrl;
|
|
||||||
this.apiKey = apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchDTO,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
const { data, error } = await betterFetch<Result<FlightTicket[]>>(
|
|
||||||
`${this.scraperUrl}/api/v1/tickets/search`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NETWORK_ERROR,
|
|
||||||
message: "Failed to search for tickets",
|
|
||||||
detail: "Could not fetch tickets for the given payload",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: error,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
JSON.stringify(error, null, 4),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`Returning ${data.data?.length} tickets`);
|
|
||||||
|
|
||||||
return { data: data.data ?? [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { nanoid } from "nanoid";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import {
|
|
||||||
CabinClass,
|
|
||||||
TicketType,
|
|
||||||
type FlightTicket,
|
|
||||||
type TicketSearchPayload,
|
|
||||||
} from "./entities";
|
|
||||||
|
|
||||||
export const flightTicketStore = writable<FlightTicket>();
|
|
||||||
|
|
||||||
export const ticketSearchStore = writable<TicketSearchPayload>({
|
|
||||||
loadMore: false,
|
|
||||||
sessionId: nanoid(),
|
|
||||||
ticketType: TicketType.Return,
|
|
||||||
cabinClass: CabinClass.Economy,
|
|
||||||
passengerCounts: { adults: 1, children: 0 },
|
|
||||||
departure: "",
|
|
||||||
arrival: "",
|
|
||||||
departureDate: "",
|
|
||||||
returnDate: "",
|
|
||||||
meta: {},
|
|
||||||
});
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
import { DiscountType, type CouponModel } from "@pkg/logic/domains/coupon/data";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import type {
|
|
||||||
FlightPriceDetails,
|
|
||||||
FlightTicket,
|
|
||||||
TicketSearchPayload,
|
|
||||||
} from "../data/entities";
|
|
||||||
import { TicketRepository } from "../data/repository";
|
|
||||||
import { ScrapedTicketsDataSource } from "../data/scrape.data.source";
|
|
||||||
|
|
||||||
export class TicketController {
|
|
||||||
private repo: TicketRepository;
|
|
||||||
private TICKET_SCRAPERS = ["kiwi"];
|
|
||||||
|
|
||||||
constructor(repo: TicketRepository) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchPayload,
|
|
||||||
coupon: CouponModel | undefined,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
const result = await this.repo.searchForTickets({
|
|
||||||
sessionId: payload.sessionId,
|
|
||||||
ticketSearchPayload: payload,
|
|
||||||
providers: this.TICKET_SCRAPERS,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error || !result.data) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!coupon) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`Auto-applying coupon ${coupon.code} to all search results`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result.data.map((ticket) => {
|
|
||||||
return this.applyDiscountToTicket(ticket, coupon);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyDiscountToTicket(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
coupon: CouponModel,
|
|
||||||
): FlightTicket {
|
|
||||||
// Calculate discount amount
|
|
||||||
let discountAmount = 0;
|
|
||||||
|
|
||||||
if (coupon.discountType === DiscountType.PERCENTAGE) {
|
|
||||||
discountAmount =
|
|
||||||
(ticket.priceDetails.displayPrice *
|
|
||||||
Number(coupon.discountValue)) /
|
|
||||||
100;
|
|
||||||
} else {
|
|
||||||
discountAmount = Number(coupon.discountValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply maximum discount limit if specified
|
|
||||||
if (
|
|
||||||
coupon.maxDiscountAmount &&
|
|
||||||
discountAmount > Number(coupon.maxDiscountAmount)
|
|
||||||
) {
|
|
||||||
discountAmount = Number(coupon.maxDiscountAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if minimum order value is not met
|
|
||||||
if (
|
|
||||||
coupon.minOrderValue &&
|
|
||||||
ticket.priceDetails.displayPrice < Number(coupon.minOrderValue)
|
|
||||||
) {
|
|
||||||
return ticket;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a copy of the ticket with the discount applied
|
|
||||||
const discountedTicket = { ...ticket };
|
|
||||||
|
|
||||||
discountedTicket.priceDetails = {
|
|
||||||
...ticket.priceDetails,
|
|
||||||
discountAmount:
|
|
||||||
(ticket.priceDetails.discountAmount || 0) + discountAmount,
|
|
||||||
displayPrice: ticket.priceDetails.displayPrice - discountAmount,
|
|
||||||
appliedCoupon: coupon.code,
|
|
||||||
couponDescription:
|
|
||||||
coupon.description ||
|
|
||||||
`Coupon discount of ${coupon.discountType === DiscountType.PERCENTAGE ? coupon.discountValue + "%" : convertAndFormatCurrency(parseFloat(coupon.discountValue.toString()))}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return discountedTicket;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cacheTicket(sid: string, payload: FlightTicket) {
|
|
||||||
Logger.info(
|
|
||||||
`Caching ticket for ${sid} | ${payload.departure}:${payload.arrival}`,
|
|
||||||
);
|
|
||||||
const refOIds = payload.refOIds;
|
|
||||||
if (!refOIds) {
|
|
||||||
// In this case we're not going for any of our fancy checkout jazz
|
|
||||||
return this.repo.createTicket(payload, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const areAnyLive =
|
|
||||||
await this.repo.areCheckoutAlreadyLiveForOrder(refOIds);
|
|
||||||
|
|
||||||
Logger.info(`Any of the OIds has checkout session live ?? ${areAnyLive}`);
|
|
||||||
|
|
||||||
if (areAnyLive) {
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "This ticket offer has expired",
|
|
||||||
userHint:
|
|
||||||
"Please select another one or perform search again to fetch latest offers",
|
|
||||||
actionable: false,
|
|
||||||
detail: "Failed to ticket",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Logger.info("nu'uh seems greenlight to make a ticket");
|
|
||||||
return this.repo.createTicket(payload, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTicketPrices(tid: number, payload: FlightPriceDetails) {
|
|
||||||
return this.repo.updateTicketPrices(tid, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
async uncacheAndSaveTicket(tid: number) {
|
|
||||||
return this.repo.uncacheAndSaveTicket(tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCheckoutUrlByRefOId(id: number) {
|
|
||||||
return this.repo.getCheckoutUrlByRefOId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRefOIdsForTicket(tid: number, oids: number[]) {
|
|
||||||
return this.repo.setRefOIdsForTicket(tid, oids);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketById(id: number) {
|
|
||||||
return this.repo.getTicketById(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTC() {
|
|
||||||
const ds = new ScrapedTicketsDataSource("", "");
|
|
||||||
return new TicketController(new TicketRepository(db, ds));
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { createTRPCRouter } from "$lib/trpc/t";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { publicProcedure } from "$lib/server/trpc/t";
|
|
||||||
import { getTC } from "./controller";
|
|
||||||
import { Logger } from "@pkg/logger";
|
|
||||||
import {
|
|
||||||
flightPriceDetailsModel,
|
|
||||||
flightTicketModel,
|
|
||||||
ticketSearchPayloadModel,
|
|
||||||
} from "../data/entities/index";
|
|
||||||
import type { Result } from "@pkg/result";
|
|
||||||
import { getCKUseCases } from "$lib/domains/ckflow/domain/usecases";
|
|
||||||
import { CouponRepository } from "@pkg/logic/domains/coupon/repository";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
|
|
||||||
export const ticketRouter = createTRPCRouter({
|
|
||||||
searchAirports: publicProcedure
|
|
||||||
.input(z.object({ query: z.string() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
const { query } = input;
|
|
||||||
const tc = getTC();
|
|
||||||
Logger.info(`Fetching airports with query: ${query}`);
|
|
||||||
return await tc.searchAirports(query);
|
|
||||||
}),
|
|
||||||
|
|
||||||
ping: publicProcedure
|
|
||||||
.input(z.object({ tid: z.number(), refOIds: z.array(z.number()) }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
console.log("Pinged");
|
|
||||||
console.log(input);
|
|
||||||
const ckflowUC = getCKUseCases();
|
|
||||||
const out = await ckflowUC.areCheckoutAlreadyLiveForOrder(
|
|
||||||
input.refOIds,
|
|
||||||
);
|
|
||||||
return { data: out } as Result<boolean>;
|
|
||||||
}),
|
|
||||||
|
|
||||||
getAirportByCode: publicProcedure
|
|
||||||
.input(z.object({ code: z.string() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return await getTC().getAirportByCode(input.code);
|
|
||||||
}),
|
|
||||||
|
|
||||||
searchTickets: publicProcedure
|
|
||||||
.input(ticketSearchPayloadModel)
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
const cr = new CouponRepository(db);
|
|
||||||
const coupon = await cr.getBestActiveCoupon();
|
|
||||||
Logger.info(`Got coupon? :: ${coupon.data?.code}`);
|
|
||||||
return getTC().searchForTickets(input, coupon.data);
|
|
||||||
}),
|
|
||||||
|
|
||||||
cacheTicket: publicProcedure
|
|
||||||
.input(z.object({ sid: z.string(), payload: flightTicketModel }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await getTC().cacheTicket(input.sid, input.payload);
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateTicketPrices: publicProcedure
|
|
||||||
.input(z.object({ tid: z.number(), payload: flightPriceDetailsModel }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await getTC().updateTicketPrices(input.tid, input.payload);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getTicketById: publicProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return await getTC().getTicketById(input.id);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
import type { FlightTicket } from "../data/entities";
|
|
||||||
import { Logger } from "@pkg/logger";
|
|
||||||
import { round } from "$lib/core/num.utils";
|
|
||||||
import { type Result } from "@pkg/result";
|
|
||||||
import { type LimitedOrderWithTicketInfoModel } from "$lib/domains/order/data/entities";
|
|
||||||
import { getJustDateString } from "@pkg/logic/core/date.utils";
|
|
||||||
|
|
||||||
type OrderWithUsageInfo = {
|
|
||||||
order: LimitedOrderWithTicketInfoModel;
|
|
||||||
usePartialAmount: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TicketWithOrderSkiddaddler {
|
|
||||||
THRESHOLD_DIFF_PERCENTAGE = 15;
|
|
||||||
ELEVATED_THRESHOLD_DIFF_PERCENTAGE = 50;
|
|
||||||
tickets: FlightTicket[];
|
|
||||||
|
|
||||||
private reservedOrders: number[];
|
|
||||||
private allActiveOrders: LimitedOrderWithTicketInfoModel[];
|
|
||||||
private urgentActiveOrders: LimitedOrderWithTicketInfoModel[];
|
|
||||||
// Stores oids that have their amount divided into smaller amounts for being reused for multiple tickets
|
|
||||||
// this record keeps track of the oid, with how much remainder is left to be divided
|
|
||||||
private reservedPartialOrders: Record<number, number>;
|
|
||||||
private minTicketPrice: number;
|
|
||||||
private maxTicketPrice: number;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
tickets: FlightTicket[],
|
|
||||||
allActiveOrders: LimitedOrderWithTicketInfoModel[],
|
|
||||||
) {
|
|
||||||
this.tickets = tickets;
|
|
||||||
this.reservedOrders = [];
|
|
||||||
this.reservedPartialOrders = {};
|
|
||||||
this.minTicketPrice = 0;
|
|
||||||
this.maxTicketPrice = 0;
|
|
||||||
this.allActiveOrders = [];
|
|
||||||
this.urgentActiveOrders = [];
|
|
||||||
|
|
||||||
this.loadupActiveOrders(allActiveOrders);
|
|
||||||
}
|
|
||||||
|
|
||||||
async magic(): Promise<Result<boolean>> {
|
|
||||||
// Sorting the orders by price in ascending order
|
|
||||||
this.allActiveOrders.sort((a, b) => b.basePrice - a.basePrice);
|
|
||||||
|
|
||||||
this.loadMinMaxPrices();
|
|
||||||
|
|
||||||
for (const ticket of this.tickets) {
|
|
||||||
if (this.areAllOrdersUsedUp()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suitableOrders = this.getSuitableOrders(ticket);
|
|
||||||
if (!suitableOrders) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log("--------- suitable orders ---------");
|
|
||||||
console.log(suitableOrders);
|
|
||||||
console.log("-----------------------------------");
|
|
||||||
const [discountAmt, newDisplayPrice] = this.calculateNewAmounts(
|
|
||||||
ticket,
|
|
||||||
suitableOrders,
|
|
||||||
);
|
|
||||||
ticket.priceDetails.discountAmount = discountAmt;
|
|
||||||
ticket.priceDetails.displayPrice = newDisplayPrice;
|
|
||||||
|
|
||||||
const oids = Array.from(
|
|
||||||
new Set(suitableOrders.map((o) => o.order.id)),
|
|
||||||
).toSorted();
|
|
||||||
ticket.refOIds = oids;
|
|
||||||
this.reservedOrders.push(...oids);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`Assigned ${this.reservedOrders.length} orders to tickets`);
|
|
||||||
return { data: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadupActiveOrders(data: LimitedOrderWithTicketInfoModel[]) {
|
|
||||||
const now = new Date();
|
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
||||||
|
|
||||||
// Removing all orders which have tickets with departure date of yesterday and older
|
|
||||||
this.allActiveOrders = data.filter((o) => {
|
|
||||||
return (
|
|
||||||
new Date(
|
|
||||||
getJustDateString(new Date(o.flightTicketInfo.departureDate)),
|
|
||||||
) >= today
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.urgentActiveOrders = [];
|
|
||||||
const threeDaysFromNowMs = 3 * 24 * 3600 * 1000;
|
|
||||||
for (const order of this.allActiveOrders) {
|
|
||||||
const depDate = new Date(order.flightTicketInfo.departureDate);
|
|
||||||
if (now.getTime() + threeDaysFromNowMs > depDate.getTime()) {
|
|
||||||
this.urgentActiveOrders.push(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`Found ${this.allActiveOrders.length} active orders`);
|
|
||||||
Logger.info(`We got ${this.urgentActiveOrders.length} urgent orders`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadMinMaxPrices() {
|
|
||||||
this.minTicketPrice = 100000;
|
|
||||||
this.maxTicketPrice = 0;
|
|
||||||
|
|
||||||
for (const ticket of this.tickets) {
|
|
||||||
const _dPrice = ticket.priceDetails.displayPrice;
|
|
||||||
if (_dPrice < this.minTicketPrice) {
|
|
||||||
this.minTicketPrice = _dPrice;
|
|
||||||
}
|
|
||||||
if (_dPrice > this.maxTicketPrice) {
|
|
||||||
this.maxTicketPrice = _dPrice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private areAllOrdersUsedUp() {
|
|
||||||
if (this.urgentActiveOrders.length === 0) {
|
|
||||||
return this.reservedOrders.length >= this.allActiveOrders.length;
|
|
||||||
}
|
|
||||||
const areUrgentOrdersUsedUp =
|
|
||||||
this.reservedOrders.length >= this.urgentActiveOrders.length;
|
|
||||||
return areUrgentOrdersUsedUp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An order is used up in 2 cases
|
|
||||||
* 1. it's not being partially fulfulled and it's found in the used orders list
|
|
||||||
* 2. it's being partially fulfilled and it's found in both the used orders list and sub chunked orders list
|
|
||||||
*/
|
|
||||||
private isOrderUsedUp(oid: number, displayPrice: number) {
|
|
||||||
if (this.reservedPartialOrders.hasOwnProperty(oid)) {
|
|
||||||
return this.reservedPartialOrders[oid] < displayPrice;
|
|
||||||
}
|
|
||||||
return this.reservedOrders.includes(oid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateNewAmounts(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
orders: OrderWithUsageInfo[],
|
|
||||||
) {
|
|
||||||
if (orders.length < 1) {
|
|
||||||
return [
|
|
||||||
ticket.priceDetails.discountAmount,
|
|
||||||
ticket.priceDetails.displayPrice,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalAmount = orders.reduce((sum, { order, usePartialAmount }) => {
|
|
||||||
return (
|
|
||||||
sum +
|
|
||||||
(usePartialAmount ? order.pricePerPassenger : order.displayPrice)
|
|
||||||
);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const discountAmt = round(ticket.priceDetails.displayPrice - totalAmount);
|
|
||||||
return [discountAmt, totalAmount];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suitable orders are those orders that are ideally within the threshold and are not already reserved. So there's a handful of cases, which other sub methods are handling
|
|
||||||
*/
|
|
||||||
private getSuitableOrders(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
): OrderWithUsageInfo[] | undefined {
|
|
||||||
const activeOrders =
|
|
||||||
this.urgentActiveOrders.length > 0
|
|
||||||
? this.urgentActiveOrders
|
|
||||||
: this.allActiveOrders;
|
|
||||||
const cases = [
|
|
||||||
(t: any, a: any) => {
|
|
||||||
return this.findFirstSuitableDirectOId(t, a);
|
|
||||||
},
|
|
||||||
(t: any, a: any) => {
|
|
||||||
return this.findFirstSuitablePartialOId(t, a);
|
|
||||||
},
|
|
||||||
(t: any, a: any) => {
|
|
||||||
return this.findManySuitableOIds(t, a);
|
|
||||||
},
|
|
||||||
(t: any, a: any) => {
|
|
||||||
return this.findFirstSuitableDirectOIdElevated(t, a);
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let c = 1;
|
|
||||||
for (const each of cases) {
|
|
||||||
const out = each(ticket, activeOrders);
|
|
||||||
if (out) {
|
|
||||||
console.log(`Case ${c} worked, returning it's response`);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
c++;
|
|
||||||
}
|
|
||||||
console.log("NO CASE WORKED, WUT, yee");
|
|
||||||
}
|
|
||||||
|
|
||||||
private findFirstSuitableDirectOId(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
activeOrders: LimitedOrderWithTicketInfoModel[],
|
|
||||||
): OrderWithUsageInfo[] | undefined {
|
|
||||||
let ord: LimitedOrderWithTicketInfoModel | undefined;
|
|
||||||
for (const o of activeOrders) {
|
|
||||||
const [op, tp] = [o.displayPrice, ticket.priceDetails.displayPrice];
|
|
||||||
const diff = Math.abs(op - tp);
|
|
||||||
const diffPtage = (diff / tp) * 100;
|
|
||||||
if (this.isOrderSuitable(o.id, op, tp, diffPtage)) {
|
|
||||||
ord = o;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ord) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return [{ order: ord, usePartialAmount: false }];
|
|
||||||
}
|
|
||||||
|
|
||||||
private findFirstSuitablePartialOId(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
activeOrders: LimitedOrderWithTicketInfoModel[],
|
|
||||||
): OrderWithUsageInfo[] | undefined {
|
|
||||||
const tp = ticket.priceDetails.displayPrice;
|
|
||||||
|
|
||||||
let ord: LimitedOrderWithTicketInfoModel | undefined;
|
|
||||||
for (const o of activeOrders) {
|
|
||||||
const op = o.pricePerPassenger;
|
|
||||||
const diff = op - tp;
|
|
||||||
const diffPtage = (diff / tp) * 100;
|
|
||||||
if (this.isOrderSuitable(o.id, op, tp, diffPtage)) {
|
|
||||||
ord = o;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ord) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.upsertPartialOrderToCache(
|
|
||||||
ord.id,
|
|
||||||
ord.pricePerPassenger,
|
|
||||||
ord.fullfilledPrice,
|
|
||||||
);
|
|
||||||
return [{ order: ord, usePartialAmount: true }];
|
|
||||||
}
|
|
||||||
|
|
||||||
private findManySuitableOIds(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
activeOrders: LimitedOrderWithTicketInfoModel[],
|
|
||||||
): OrderWithUsageInfo[] | undefined {
|
|
||||||
const targetPrice = ticket.priceDetails.displayPrice;
|
|
||||||
|
|
||||||
const validOrderOptions = activeOrders.flatMap((order) => {
|
|
||||||
const options: OrderWithUsageInfo[] = [];
|
|
||||||
|
|
||||||
// Add full price option if suitable
|
|
||||||
if (
|
|
||||||
!this.isOrderUsedUp(order.id, order.displayPrice) &&
|
|
||||||
order.displayPrice <= targetPrice
|
|
||||||
) {
|
|
||||||
options.push({ order, usePartialAmount: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add partial price option if suitable
|
|
||||||
if (
|
|
||||||
!this.isOrderUsedUp(order.id, order.pricePerPassenger) &&
|
|
||||||
order.pricePerPassenger <= targetPrice
|
|
||||||
) {
|
|
||||||
options.push({ order, usePartialAmount: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (validOrderOptions.length === 0) return undefined;
|
|
||||||
// Helper function to get the effective price of an order
|
|
||||||
const getEffectivePrice = (ordInfo: {
|
|
||||||
order: LimitedOrderWithTicketInfoModel;
|
|
||||||
usePerPassenger: boolean;
|
|
||||||
}) => {
|
|
||||||
return ordInfo.usePerPassenger
|
|
||||||
? ordInfo.order.pricePerPassenger
|
|
||||||
: ordInfo.order.displayPrice;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sumCombination = (combo: OrderWithUsageInfo[]): number => {
|
|
||||||
return combo.reduce(
|
|
||||||
(sum, { order, usePartialAmount }) =>
|
|
||||||
sum +
|
|
||||||
(usePartialAmount
|
|
||||||
? order.pricePerPassenger
|
|
||||||
: order.displayPrice),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let bestCombination: OrderWithUsageInfo[] = [];
|
|
||||||
let bestDiff = targetPrice;
|
|
||||||
|
|
||||||
// Try combinations of different sizes
|
|
||||||
for (
|
|
||||||
let size = 1;
|
|
||||||
size <= Math.min(validOrderOptions.length, 3);
|
|
||||||
size++
|
|
||||||
) {
|
|
||||||
const combinations = this.getCombinations(validOrderOptions, size);
|
|
||||||
|
|
||||||
for (const combo of combinations) {
|
|
||||||
const sum = sumCombination(combo);
|
|
||||||
const diff = targetPrice - sum;
|
|
||||||
|
|
||||||
if (
|
|
||||||
diff >= 0 &&
|
|
||||||
(diff / targetPrice) * 100 <=
|
|
||||||
this.THRESHOLD_DIFF_PERCENTAGE &&
|
|
||||||
diff < bestDiff
|
|
||||||
) {
|
|
||||||
// Verify we're not using the same order twice
|
|
||||||
const orderIds = new Set(combo.map((c) => c.order.id));
|
|
||||||
if (orderIds.size === combo.length) {
|
|
||||||
bestDiff = diff;
|
|
||||||
bestCombination = combo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestCombination.length === 0) return undefined;
|
|
||||||
|
|
||||||
// Update partial orders cache
|
|
||||||
bestCombination.forEach(({ order, usePartialAmount }) => {
|
|
||||||
if (usePartialAmount) {
|
|
||||||
this.upsertPartialOrderToCache(
|
|
||||||
order.id,
|
|
||||||
order.pricePerPassenger,
|
|
||||||
order.fullfilledPrice,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return bestCombination;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In case no other cases worked, but we have an order with far lower price amount,
|
|
||||||
* then we juss use it but bump up it's amount to match the order
|
|
||||||
*/
|
|
||||||
private findFirstSuitableDirectOIdElevated(
|
|
||||||
ticket: FlightTicket,
|
|
||||||
activeOrders: LimitedOrderWithTicketInfoModel[],
|
|
||||||
): OrderWithUsageInfo[] | undefined {
|
|
||||||
const targetPrice = ticket.priceDetails.displayPrice;
|
|
||||||
|
|
||||||
// Find the first unreserved order with the smallest price difference
|
|
||||||
let bestOrder: LimitedOrderWithTicketInfoModel | undefined;
|
|
||||||
let smallestPriceDiff = Number.MAX_VALUE;
|
|
||||||
|
|
||||||
for (const order of activeOrders) {
|
|
||||||
if (this.isOrderUsedUp(order.id, order.displayPrice)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check both regular price and per-passenger price
|
|
||||||
const prices = [order.displayPrice];
|
|
||||||
if (order.pricePerPassenger) {
|
|
||||||
prices.push(order.pricePerPassenger);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const price of prices) {
|
|
||||||
const diff = Math.abs(targetPrice - price);
|
|
||||||
if (diff < smallestPriceDiff) {
|
|
||||||
smallestPriceDiff = diff;
|
|
||||||
bestOrder = order;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bestOrder) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate if we should use partial amount
|
|
||||||
const usePartial =
|
|
||||||
bestOrder.pricePerPassenger &&
|
|
||||||
Math.abs(targetPrice - bestOrder.pricePerPassenger) <
|
|
||||||
Math.abs(targetPrice - bestOrder.displayPrice);
|
|
||||||
|
|
||||||
// The price will be elevated to match the ticket price
|
|
||||||
// We'll use the original price ratio to determine the new display price
|
|
||||||
const originalPrice = usePartial
|
|
||||||
? bestOrder.pricePerPassenger
|
|
||||||
: bestOrder.displayPrice;
|
|
||||||
|
|
||||||
// Only elevate if the price difference is reasonable
|
|
||||||
const priceDiffPercentage =
|
|
||||||
(Math.abs(targetPrice - originalPrice) / targetPrice) * 100;
|
|
||||||
if (priceDiffPercentage > this.ELEVATED_THRESHOLD_DIFF_PERCENTAGE) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (usePartial) {
|
|
||||||
// bestOrder.pricePerPassenger =
|
|
||||||
// } else {
|
|
||||||
// }
|
|
||||||
|
|
||||||
return [{ order: bestOrder, usePartialAmount: !!usePartial }];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCombinations<T>(arr: T[], size: number): T[][] {
|
|
||||||
if (size === 0) return [[]];
|
|
||||||
if (arr.length === 0) return [];
|
|
||||||
|
|
||||||
const first = arr[0];
|
|
||||||
const rest = arr.slice(1);
|
|
||||||
|
|
||||||
const combosWithoutFirst = this.getCombinations(rest, size);
|
|
||||||
const combosWithFirst = this.getCombinations(rest, size - 1).map(
|
|
||||||
(combo) => [first, ...combo],
|
|
||||||
);
|
|
||||||
return [...combosWithoutFirst, ...combosWithFirst];
|
|
||||||
}
|
|
||||||
|
|
||||||
private isOrderSuitable(
|
|
||||||
orderId: number,
|
|
||||||
orderPrice: number,
|
|
||||||
ticketPrice: number,
|
|
||||||
diffPercentage: number,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
diffPercentage > 0 &&
|
|
||||||
diffPercentage <= this.THRESHOLD_DIFF_PERCENTAGE &&
|
|
||||||
orderPrice <= ticketPrice &&
|
|
||||||
!this.isOrderUsedUp(orderId, orderPrice)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private upsertPartialOrderToCache(
|
|
||||||
oid: number,
|
|
||||||
price: number,
|
|
||||||
_defaultPrice: number = 0,
|
|
||||||
) {
|
|
||||||
if (!this.reservedPartialOrders[oid]) {
|
|
||||||
this.reservedPartialOrders[oid] = _defaultPrice;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.reservedPartialOrders[oid] += price;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
import { goto, replaceState } from "$app/navigation";
|
|
||||||
import { page } from "$app/state";
|
|
||||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
|
||||||
import {
|
|
||||||
type FlightPriceDetails,
|
|
||||||
type FlightTicket,
|
|
||||||
ticketSearchPayloadModel,
|
|
||||||
} from "$lib/domains/ticket/data/entities/index";
|
|
||||||
import { trpcApiStore } from "$lib/stores/api";
|
|
||||||
import type { Result } from "@pkg/result";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { toast } from "svelte-sonner";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import { flightTicketStore, ticketSearchStore } from "../data/store";
|
|
||||||
import { checkoutVM } from "./checkout/checkout.vm.svelte";
|
|
||||||
import { paymentInfoVM } from "./checkout/payment-info-section/payment.info.vm.svelte";
|
|
||||||
import {
|
|
||||||
MaxStops,
|
|
||||||
SortOption,
|
|
||||||
ticketFiltersStore,
|
|
||||||
} from "./ticket-filters.vm.svelte";
|
|
||||||
|
|
||||||
export class FlightTicketViewModel {
|
|
||||||
searching = $state(false);
|
|
||||||
tickets = $state<FlightTicket[]>([]);
|
|
||||||
renderedTickets = $state<FlightTicket[]>([]);
|
|
||||||
|
|
||||||
updatingPrices = $state(false);
|
|
||||||
|
|
||||||
beginSearch() {
|
|
||||||
const info = get(ticketSearchStore);
|
|
||||||
if (!info) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
info.passengerCounts.adults < 1 &&
|
|
||||||
info.passengerCounts.children < 1
|
|
||||||
) {
|
|
||||||
toast.error("Please enter at least one adult and one child");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sum = info.passengerCounts.adults + info.passengerCounts.children;
|
|
||||||
if (sum > 10) {
|
|
||||||
toast.error("Please enter no more than 10 passengers");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = this.formatURLParams();
|
|
||||||
goto(`/search?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadStore(urlParams: URLSearchParams) {
|
|
||||||
console.log("Meta parameter: ", urlParams.get("meta"));
|
|
||||||
|
|
||||||
ticketSearchStore.update((prev) => {
|
|
||||||
return {
|
|
||||||
sessionId: prev.sessionId ?? "",
|
|
||||||
ticketType: urlParams.get("ticketType") ?? prev.ticketType,
|
|
||||||
cabinClass: urlParams.get("cabinClass") ?? prev.cabinClass,
|
|
||||||
passengerCounts: {
|
|
||||||
adults: Number(
|
|
||||||
urlParams.get("adults") ?? prev.passengerCounts.adults,
|
|
||||||
),
|
|
||||||
children: Number(
|
|
||||||
urlParams.get("children") ??
|
|
||||||
prev.passengerCounts.children,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
departure: urlParams.get("departure") ?? prev.departure,
|
|
||||||
arrival: urlParams.get("arrival") ?? prev.arrival,
|
|
||||||
departureDate:
|
|
||||||
urlParams.get("departureDate") ?? prev.departureDate,
|
|
||||||
returnDate: urlParams.get("returnDate") ?? prev.returnDate,
|
|
||||||
loadMore: prev.loadMore ?? false,
|
|
||||||
meta: (() => {
|
|
||||||
const metaStr = urlParams.get("meta");
|
|
||||||
if (!metaStr) return prev.meta;
|
|
||||||
try {
|
|
||||||
return JSON.parse(metaStr);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse meta parameter:", e);
|
|
||||||
return prev.meta;
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetCachedCheckoutData() {
|
|
||||||
// @ts-ignore
|
|
||||||
flightTicketStore.set(undefined);
|
|
||||||
passengerInfoVM.reset();
|
|
||||||
checkoutVM.reset();
|
|
||||||
paymentInfoVM.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(loadMore = false) {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast.error("Please try again by reloading the page", {
|
|
||||||
description: "Page state not properly initialized",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let payload = get(ticketSearchStore);
|
|
||||||
if (!payload) {
|
|
||||||
return toast.error(
|
|
||||||
"Could not search for tickets due to invalid payload",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = ticketSearchPayloadModel.safeParse(payload);
|
|
||||||
if (!parsed.success) {
|
|
||||||
console.log("Enter some parameters to search for tickets");
|
|
||||||
this.searching = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload = parsed.data;
|
|
||||||
|
|
||||||
if (loadMore) {
|
|
||||||
payload.loadMore = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searching = true;
|
|
||||||
const out = await api.ticket.searchTickets.query(payload);
|
|
||||||
this.searching = false;
|
|
||||||
|
|
||||||
console.log(out);
|
|
||||||
|
|
||||||
if (out.error) {
|
|
||||||
return toast.error(out.error.message, {
|
|
||||||
description: out.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!out.data) {
|
|
||||||
this.tickets = [];
|
|
||||||
return toast.error("No search results", {
|
|
||||||
description: "Please try again with different parameters",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tickets = out.data;
|
|
||||||
this.applyFilters();
|
|
||||||
this.resetCachedCheckoutData();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters() {
|
|
||||||
this.searching = true;
|
|
||||||
const filters = get(ticketFiltersStore);
|
|
||||||
const filteredTickets = this.tickets.filter((ticket) => {
|
|
||||||
// Price filter
|
|
||||||
if (filters.priceRange.max > 0) {
|
|
||||||
if (
|
|
||||||
ticket.priceDetails.displayPrice < filters.priceRange.min ||
|
|
||||||
ticket.priceDetails.displayPrice > filters.priceRange.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.maxStops !== MaxStops.Any) {
|
|
||||||
// Calculate stops for outbound flight
|
|
||||||
const outboundStops =
|
|
||||||
ticket.flightIteneraries.outbound.length - 1;
|
|
||||||
// Calculate stops for inbound flight
|
|
||||||
const inboundStops = ticket.flightIteneraries.inbound.length - 1;
|
|
||||||
|
|
||||||
// Get the maximum number of stops between outbound and inbound
|
|
||||||
const maxStopsInJourney = Math.max(outboundStops, inboundStops);
|
|
||||||
|
|
||||||
switch (filters.maxStops) {
|
|
||||||
case MaxStops.Direct:
|
|
||||||
if (maxStopsInJourney > 0) return false;
|
|
||||||
break;
|
|
||||||
case MaxStops.One:
|
|
||||||
if (maxStopsInJourney > 1) return false;
|
|
||||||
break;
|
|
||||||
case MaxStops.Two:
|
|
||||||
if (maxStopsInJourney > 2) return false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time range filters
|
|
||||||
if (filters.time.departure.max > 0 || filters.time.arrival.max > 0) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
for (const itinerary of allItineraries) {
|
|
||||||
const departureHour = new Date(
|
|
||||||
itinerary.departure.utcTime,
|
|
||||||
).getHours();
|
|
||||||
const arrivalHour = new Date(
|
|
||||||
itinerary.destination.utcTime,
|
|
||||||
).getHours();
|
|
||||||
|
|
||||||
if (filters.time.departure.max > 0) {
|
|
||||||
if (
|
|
||||||
departureHour < filters.time.departure.min ||
|
|
||||||
departureHour > filters.time.departure.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.time.arrival.max > 0) {
|
|
||||||
if (
|
|
||||||
arrivalHour < filters.time.arrival.min ||
|
|
||||||
arrivalHour > filters.time.arrival.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration filter
|
|
||||||
if (filters.duration.max > 0) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
const totalDuration = allItineraries.reduce(
|
|
||||||
(sum, itinerary) => sum + itinerary.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const durationHours = totalDuration / 3600;
|
|
||||||
|
|
||||||
if (
|
|
||||||
durationHours < filters.duration.min ||
|
|
||||||
durationHours > filters.duration.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overnight filter
|
|
||||||
if (!filters.allowOvernight) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
const hasOvernightFlight = allItineraries.some((itinerary) => {
|
|
||||||
const departureHour = new Date(
|
|
||||||
itinerary.departure.utcTime,
|
|
||||||
).getHours();
|
|
||||||
const arrivalHour = new Date(
|
|
||||||
itinerary.destination.utcTime,
|
|
||||||
).getHours();
|
|
||||||
|
|
||||||
// Consider a flight overnight if it departs between 8 PM (20) and 6 AM (6)
|
|
||||||
return (
|
|
||||||
departureHour >= 20 ||
|
|
||||||
departureHour <= 6 ||
|
|
||||||
arrivalHour >= 20 ||
|
|
||||||
arrivalHour <= 6
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (hasOvernightFlight) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredTickets.sort((a, b) => {
|
|
||||||
switch (filters.sortBy) {
|
|
||||||
case SortOption.PriceLowToHigh:
|
|
||||||
return (
|
|
||||||
a.priceDetails.displayPrice - b.priceDetails.displayPrice
|
|
||||||
);
|
|
||||||
case SortOption.PriceHighToLow:
|
|
||||||
return (
|
|
||||||
b.priceDetails.displayPrice - a.priceDetails.displayPrice
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderedTickets = filteredTickets;
|
|
||||||
this.searching = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cacheTicketAndGotoCheckout(id: number) {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast.error("Please try again by reloading the page", {
|
|
||||||
description: "Page state not properly initialized",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const targetTicket = this.tickets.find((ticket) => ticket.id === id);
|
|
||||||
if (!targetTicket) {
|
|
||||||
return toast.error("Ticket not found", {
|
|
||||||
description:
|
|
||||||
"Please try again with different parameters or refresh page to try again",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const sid = get(ticketSearchStore).sessionId;
|
|
||||||
console.log("sid", sid);
|
|
||||||
const out = await api.ticket.cacheTicket.mutate({
|
|
||||||
sid,
|
|
||||||
payload: targetTicket,
|
|
||||||
});
|
|
||||||
if (out.error) {
|
|
||||||
return toast.error(out.error.message, {
|
|
||||||
description: out.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!out.data) {
|
|
||||||
return toast.error("Failed to proceed to checkout", {
|
|
||||||
description: "Please refresh the page to try again",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
goto(`/checkout/${sid}/${out.data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectToSearchPage() {
|
|
||||||
const params = this.formatURLParams();
|
|
||||||
goto(`/search?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setURLParams(): Result<boolean> {
|
|
||||||
ticketSearchStore.update((prev) => {
|
|
||||||
return { ...prev, sessionId: nanoid() };
|
|
||||||
});
|
|
||||||
const newParams = this.formatURLParams();
|
|
||||||
|
|
||||||
const url = new URL(page.url.href);
|
|
||||||
|
|
||||||
for (const [key, value] of newParams.entries()) {
|
|
||||||
url.searchParams.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stripped = page.url.href.includes("?")
|
|
||||||
? page.url.href.split("?")[0]
|
|
||||||
: page.url.href;
|
|
||||||
|
|
||||||
replaceState(
|
|
||||||
new URL(stripped + "?" + new URLSearchParams(newParams)).toString(),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatURLParams() {
|
|
||||||
const info = get(ticketSearchStore);
|
|
||||||
let out = new URLSearchParams();
|
|
||||||
if (!info) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
out.append("ticketType", info.ticketType);
|
|
||||||
out.append("cabinClass", info.cabinClass);
|
|
||||||
out.append("adults", info.passengerCounts.adults.toString());
|
|
||||||
out.append("children", info.passengerCounts.children.toString());
|
|
||||||
out.append("departureDate", info.departureDate);
|
|
||||||
out.append("returnDate", info.returnDate);
|
|
||||||
out.append("meta", JSON.stringify(info.meta));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTicketPrices(updated: FlightPriceDetails) {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tid = get(flightTicketStore).id;
|
|
||||||
this.updatingPrices = true;
|
|
||||||
const out = await api.ticket.updateTicketPrices.mutate({
|
|
||||||
tid,
|
|
||||||
payload: updated,
|
|
||||||
});
|
|
||||||
this.updatingPrices = false;
|
|
||||||
console.log("new shit");
|
|
||||||
console.log(out);
|
|
||||||
if (out.error) {
|
|
||||||
toast.error(out.error.message, {
|
|
||||||
description: out.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!out.data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
flightTicketStore.update((prev) => {
|
|
||||||
return { ...prev, priceDetails: out.data! };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flightTicketVM = new FlightTicketViewModel();
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import BackpackIcon from "~icons/solar/backpack-broken";
|
|
||||||
import BagCheckIcon from "~icons/solar/suitcase-broken";
|
|
||||||
import PersonalBagIcon from "~icons/solar/bag-broken";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import type { FlightTicket } from "../../data/entities";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if data}
|
|
||||||
<Title size="h5" color="black">Baggage Info</Title>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<!-- Personal Item - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={PersonalBagIcon} cls="w-6 h-6" />
|
|
||||||
<span>Personal Item</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.bagsInfo.includedPersonalBags} included</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cabin Baggage - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={BackpackIcon} cls="w-6 h-6" />
|
|
||||||
<span>Cabin Baggage</span>
|
|
||||||
</div>
|
|
||||||
{#if data.bagsInfo.hasHandBagsSupport}
|
|
||||||
<span>{data.bagsInfo.includedHandBags} included</span>
|
|
||||||
{:else}
|
|
||||||
<span class="text-gray-500">Not available</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Checked Baggage - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={BagCheckIcon} cls="w-6 h-6" />
|
|
||||||
<span>Checked Baggage</span>
|
|
||||||
</div>
|
|
||||||
{#if data.bagsInfo.hasCheckedBagsSupport}
|
|
||||||
<span>{data.bagsInfo.includedCheckedBags} included</span>
|
|
||||||
{:else}
|
|
||||||
<span class="text-gray-500">Not available</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import Button, {
|
|
||||||
buttonVariants,
|
|
||||||
} from "$lib/components/ui/button/button.svelte";
|
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
||||||
import { TRANSITION_ALL } from "$lib/core/constants";
|
|
||||||
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
|
||||||
import { cn } from "$lib/utils";
|
|
||||||
import { type FlightTicket } from "../../data/entities";
|
|
||||||
import { flightTicketVM } from "../ticket.vm.svelte";
|
|
||||||
import TicketDetailsModal from "./ticket-details-modal.svelte";
|
|
||||||
import TicketLegsOverview from "./ticket-legs-overview.svelte";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
|
|
||||||
async function proceedToCheckout() {
|
|
||||||
await flightTicketVM.cacheTicketAndGotoCheckout(data.id);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row">
|
|
||||||
<TicketDetailsModal {data} onCheckoutBtnClick={proceedToCheckout}>
|
|
||||||
<Dialog.Trigger
|
|
||||||
class={cn(
|
|
||||||
"flex w-full flex-col justify-center gap-4 rounded-lg border-x-0 border-t-2 border-gray-200 bg-white p-6 shadow-md hover:bg-gray-50 md:border-y-2 md:border-l-2 md:border-r-2",
|
|
||||||
TRANSITION_ALL,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TicketLegsOverview {data} />
|
|
||||||
<div class="flex items-center gap-2"></div>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
</TicketDetailsModal>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
"flex w-full flex-col items-center justify-center gap-4 rounded-lg border-x-0 border-b-2 border-gray-200 bg-white p-6 shadow-md md:max-w-xs md:border-y-2 md:border-l-0 md:border-r-2",
|
|
||||||
TRANSITION_ALL,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<!-- Add price comparison logic here -->
|
|
||||||
{#if data.priceDetails.basePrice !== data.priceDetails.displayPrice}
|
|
||||||
{@const discountPercentage = Math.round(
|
|
||||||
(1 -
|
|
||||||
data.priceDetails.displayPrice /
|
|
||||||
data.priceDetails.basePrice) *
|
|
||||||
100,
|
|
||||||
)}
|
|
||||||
<div class="flex flex-col items-center gap-1">
|
|
||||||
<Badge variant="destructive">
|
|
||||||
<span>{discountPercentage}% OFF</span>
|
|
||||||
</Badge>
|
|
||||||
<div class="text-gray-500 line-through">
|
|
||||||
{convertAndFormatCurrency(data.priceDetails.basePrice)}
|
|
||||||
</div>
|
|
||||||
<Title center size="h4" weight="medium" color="black">
|
|
||||||
{convertAndFormatCurrency(data.priceDetails.displayPrice)}
|
|
||||||
</Title>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Title center size="h4" weight="medium" color="black">
|
|
||||||
{convertAndFormatCurrency(data.priceDetails.displayPrice)}
|
|
||||||
</Title>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex w-full flex-col gap-1">
|
|
||||||
<TicketDetailsModal {data} onCheckoutBtnClick={proceedToCheckout}>
|
|
||||||
<Dialog.Trigger
|
|
||||||
class={cn(buttonVariants({ variant: "secondary" }))}
|
|
||||||
>
|
|
||||||
Flight Info
|
|
||||||
</Dialog.Trigger>
|
|
||||||
</TicketDetailsModal>
|
|
||||||
<Button onclick={() => proceedToCheckout()}>Checkout</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
||||||
import type { FlightTicket } from "../../data/entities";
|
|
||||||
import TicketDetails from "./ticket-details.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
data,
|
|
||||||
onCheckoutBtnClick,
|
|
||||||
hideCheckoutBtn,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
data: FlightTicket;
|
|
||||||
children: any;
|
|
||||||
hideCheckoutBtn?: boolean;
|
|
||||||
onCheckoutBtnClick: (tid: number) => void;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root>
|
|
||||||
{@render children()}
|
|
||||||
|
|
||||||
<Dialog.Content class="w-full max-w-2xl">
|
|
||||||
<div class="flex max-h-[80vh] w-full flex-col gap-4 overflow-y-auto">
|
|
||||||
<TicketDetails {data} {hideCheckoutBtn} {onCheckoutBtnClick} />
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
|
||||||
import { convertAndFormatCurrency } from "$lib/domains/currency/view/currency.vm.svelte";
|
|
||||||
import { type FlightTicket } from "../../data/entities";
|
|
||||||
import BaggageInfo from "./baggage-info.svelte";
|
|
||||||
import TripDetails from "./trip-details.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
data,
|
|
||||||
hideCheckoutBtn,
|
|
||||||
onCheckoutBtnClick,
|
|
||||||
}: {
|
|
||||||
data: FlightTicket;
|
|
||||||
onCheckoutBtnClick: (tid: number) => void;
|
|
||||||
hideCheckoutBtn?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TripDetails {data} />
|
|
||||||
|
|
||||||
<BaggageInfo {data} />
|
|
||||||
|
|
||||||
{#if !hideCheckoutBtn}
|
|
||||||
<div class="mt-4 flex items-center justify-end gap-4">
|
|
||||||
<Title center size="h5" color="black">
|
|
||||||
{convertAndFormatCurrency(data.priceDetails.displayPrice)}
|
|
||||||
</Title>
|
|
||||||
<Button onclick={() => onCheckoutBtnClick(data.id)}>Checkout</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import { Plane } from "@lucide/svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
departure: string;
|
|
||||||
destination: string;
|
|
||||||
durationSeconds: number;
|
|
||||||
departureTime: string;
|
|
||||||
arrivalTime: string;
|
|
||||||
departureDate: string;
|
|
||||||
arrivalDate: string;
|
|
||||||
airlineName?: string;
|
|
||||||
stops?: number;
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
departure,
|
|
||||||
destination,
|
|
||||||
durationSeconds,
|
|
||||||
airlineName,
|
|
||||||
stops,
|
|
||||||
departureDate,
|
|
||||||
departureTime,
|
|
||||||
arrivalDate,
|
|
||||||
arrivalTime,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const dH = Math.floor(durationSeconds / 3600);
|
|
||||||
const dM = Math.floor((durationSeconds % 3600) / 60);
|
|
||||||
const durationFormatted = `${dH}h ${dM}m`;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex w-full flex-col gap-4 p-2">
|
|
||||||
<div class="flex w-full items-start justify-between gap-4">
|
|
||||||
<div class="flex flex-col items-start text-start">
|
|
||||||
<Title color="black" size="h5">{departure}</Title>
|
|
||||||
<Title color="black" size="p" weight="normal">
|
|
||||||
{departureTime}
|
|
||||||
</Title>
|
|
||||||
<span class="text-xs text-gray-500 sm:text-sm">{departureDate}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-2 pt-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="w-20 border-t-2 border-gray-300 md:w-40"></div>
|
|
||||||
<Plane
|
|
||||||
class="absolute -top-[10px] left-1/2 -translate-x-1/2 transform text-primary"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="text-sm font-medium text-gray-600">
|
|
||||||
{durationFormatted}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-1">
|
|
||||||
{#if stops !== undefined}
|
|
||||||
<Badge
|
|
||||||
variant={stops > 0 ? "secondary" : "outline"}
|
|
||||||
class="font-medium"
|
|
||||||
>
|
|
||||||
{#if stops > 0}
|
|
||||||
{stops} {stops === 1 ? "Stop" : "Stops"}
|
|
||||||
{:else}
|
|
||||||
Direct Flight
|
|
||||||
{/if}
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
{#if airlineName}
|
|
||||||
<span class="text-xs text-gray-500">{airlineName}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-end text-end">
|
|
||||||
<Title color="black" size="h5">{destination}</Title>
|
|
||||||
<Title color="black" size="p" weight="normal">
|
|
||||||
{arrivalTime}
|
|
||||||
</Title>
|
|
||||||
<span class="text-xs text-gray-500 sm:text-sm">{arrivalDate}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { TicketType, type FlightTicket } from "../../data/entities";
|
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import TicketItinerary from "./ticket-itinerary.svelte";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
|
|
||||||
const outboundDuration = data.flightIteneraries.outbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const inboundDuration = data.flightIteneraries.inbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReturnTicket =
|
|
||||||
data.flightIteneraries.inbound.length > 0 &&
|
|
||||||
data.flightType === TicketType.Return;
|
|
||||||
|
|
||||||
function formatDateTime(dateTimeStr: string) {
|
|
||||||
const date = new Date(dateTimeStr);
|
|
||||||
return {
|
|
||||||
time: date.toLocaleTimeString("en-US", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
}),
|
|
||||||
date: date.toLocaleDateString("en-US", {
|
|
||||||
weekday: "short",
|
|
||||||
day: "2-digit",
|
|
||||||
month: "short",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const outboundFirst = data.flightIteneraries.outbound[0];
|
|
||||||
const outboundLast =
|
|
||||||
data.flightIteneraries.outbound[
|
|
||||||
data.flightIteneraries.outbound.length - 1
|
|
||||||
];
|
|
||||||
|
|
||||||
const inboundFirst = isReturnTicket
|
|
||||||
? data.flightIteneraries.inbound[0]
|
|
||||||
: null;
|
|
||||||
const inboundLast = isReturnTicket
|
|
||||||
? data.flightIteneraries.inbound[
|
|
||||||
data.flightIteneraries.inbound.length - 1
|
|
||||||
]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const outboundDeparture = formatDateTime(outboundFirst.departure.localTime);
|
|
||||||
const outboundArrival = formatDateTime(outboundLast.destination.localTime);
|
|
||||||
|
|
||||||
const inboundDeparture = inboundFirst
|
|
||||||
? formatDateTime(inboundFirst.departure.localTime)
|
|
||||||
: null;
|
|
||||||
const inboundArrival = inboundLast
|
|
||||||
? formatDateTime(inboundLast.destination.localTime)
|
|
||||||
: null;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<Badge variant="outline" class="w-max">
|
|
||||||
<span>Outbound</span>
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
<TicketItinerary
|
|
||||||
departure={data.departure}
|
|
||||||
destination={data.arrival}
|
|
||||||
durationSeconds={outboundDuration}
|
|
||||||
stops={data.flightIteneraries.outbound.length - 1}
|
|
||||||
departureTime={outboundDeparture.time}
|
|
||||||
departureDate={outboundDeparture.date}
|
|
||||||
arrivalTime={outboundArrival.time}
|
|
||||||
arrivalDate={outboundArrival.date}
|
|
||||||
airlineName={outboundFirst.airline.name}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<Badge variant="outline" class="w-max">
|
|
||||||
<span>Inbound</span>
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<TicketItinerary
|
|
||||||
departure={data.arrival}
|
|
||||||
destination={data.departure}
|
|
||||||
durationSeconds={inboundDuration}
|
|
||||||
stops={data.flightIteneraries.inbound.length - 1}
|
|
||||||
departureTime={inboundDeparture?.time || ""}
|
|
||||||
departureDate={inboundDeparture?.date || ""}
|
|
||||||
arrivalTime={inboundArrival?.time || ""}
|
|
||||||
arrivalDate={inboundArrival?.date || ""}
|
|
||||||
airlineName={inboundFirst?.airline.name}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import { TicketType, type FlightTicket } from "../../data/entities";
|
|
||||||
import * as Accordion from "$lib/components/ui/accordion";
|
|
||||||
import { Badge } from "$lib/components/ui/badge";
|
|
||||||
import { formatDateTime } from "@pkg/logic/core/date.utils";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
|
|
||||||
const isReturnTicket = data.flightType === TicketType.Return;
|
|
||||||
|
|
||||||
function formatDuration(seconds: number) {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Title size="h4" color="black">Trip Details</Title>
|
|
||||||
|
|
||||||
<Accordion.Root type="single" class="flex w-full flex-col gap-4">
|
|
||||||
<Accordion.Item
|
|
||||||
value="outbound"
|
|
||||||
class="rounded-lg border-2 border-gray-200 bg-white px-4 shadow-md"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger class="w-full">
|
|
||||||
<div class="flex w-full flex-col gap-2">
|
|
||||||
<Badge variant="outline" class="w-max">Outbound</Badge>
|
|
||||||
<div class="flex w-full items-center justify-between">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium">
|
|
||||||
{data.flightIteneraries.outbound[0].departure
|
|
||||||
.station.code}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-500">→</span>
|
|
||||||
<span class="font-medium">
|
|
||||||
{data.flightIteneraries.outbound[
|
|
||||||
data.flightIteneraries.outbound.length - 1
|
|
||||||
].destination.station.code}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-gray-600">
|
|
||||||
{data.flightIteneraries.outbound[0].airline.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{formatDuration(
|
|
||||||
data.flightIteneraries.outbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion.Trigger>
|
|
||||||
<Accordion.Content>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-4 border-t-2 border-dashed border-gray-200 p-4"
|
|
||||||
>
|
|
||||||
{#each data.flightIteneraries.outbound as flight, index}
|
|
||||||
{#if index > 0}
|
|
||||||
<div class="my-2 border-t border-dashed"></div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>Flight {flight.flightNumber}</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
• {flight.airline.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>{formatDuration(flight.durationSeconds)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-sm font-semibold">
|
|
||||||
{formatDateTime(flight.departure.localTime)
|
|
||||||
.time}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
{formatDateTime(flight.departure.localTime)
|
|
||||||
.date}
|
|
||||||
</span>
|
|
||||||
<span class="mt-1 text-sm text-gray-600">
|
|
||||||
{flight.departure.station.city} ({flight
|
|
||||||
.departure.station.code})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<span class="text-sm font-semibold">
|
|
||||||
{formatDateTime(flight.destination.localTime)
|
|
||||||
.time}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
{formatDateTime(flight.destination.localTime)
|
|
||||||
.date}
|
|
||||||
</span>
|
|
||||||
<span class="mt-1 text-sm text-gray-600">
|
|
||||||
{flight.destination.station.city} ({flight
|
|
||||||
.destination.station.code})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
|
|
||||||
{#if isReturnTicket && data.flightIteneraries.inbound.length > 0}
|
|
||||||
<Accordion.Item
|
|
||||||
value="inbound"
|
|
||||||
class="rounded-lg border-2 border-gray-200 bg-white px-4 shadow-md"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger class="w-full">
|
|
||||||
<div class="flex w-full flex-col gap-2">
|
|
||||||
<Badge variant="outline" class="w-max">Inbound</Badge>
|
|
||||||
<div class="flex w-full items-center justify-between">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium">
|
|
||||||
{data.flightIteneraries.inbound[0].departure
|
|
||||||
.station.code}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-500">→</span>
|
|
||||||
<span class="font-medium">
|
|
||||||
{data.flightIteneraries.inbound[
|
|
||||||
data.flightIteneraries.inbound.length - 1
|
|
||||||
].destination.station.code}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-gray-600">
|
|
||||||
{data.flightIteneraries.inbound[0].airline.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{formatDuration(
|
|
||||||
data.flightIteneraries.inbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion.Trigger>
|
|
||||||
|
|
||||||
<Accordion.Content>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-4 border-t-2 border-dashed border-gray-200 p-4"
|
|
||||||
>
|
|
||||||
{#each data.flightIteneraries.inbound as flight, index}
|
|
||||||
{#if index > 0}
|
|
||||||
<div class="my-2 border-t border-dashed"></div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>Flight {flight.flightNumber}</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
• {flight.airline.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
>{formatDuration(
|
|
||||||
flight.durationSeconds,
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-sm font-semibold">
|
|
||||||
{formatDateTime(
|
|
||||||
flight.departure.localTime,
|
|
||||||
).time}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
{formatDateTime(
|
|
||||||
flight.departure.localTime,
|
|
||||||
).date}
|
|
||||||
</span>
|
|
||||||
<span class="mt-1 text-sm text-gray-600">
|
|
||||||
{flight.departure.station.city} ({flight
|
|
||||||
.departure.station.code})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<span class="text-sm font-semibold">
|
|
||||||
{formatDateTime(
|
|
||||||
flight.destination.localTime,
|
|
||||||
).time}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
{formatDateTime(
|
|
||||||
flight.destination.localTime,
|
|
||||||
).date}
|
|
||||||
</span>
|
|
||||||
<span class="mt-1 text-sm text-gray-600">
|
|
||||||
{flight.destination.station.city} ({flight
|
|
||||||
.destination.station.code})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
{/if}
|
|
||||||
</Accordion.Root>
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { getProductUseCases } from "$lib/domains/product/usecases";
|
||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
return await getProductUseCases().getProductByLinkId(
|
||||||
|
Number(params.plid ?? "-1"),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
import PaymentVerificationSection from "$lib/domains/checkout/payment-verification-section.svelte";
|
import PaymentVerificationSection from "$lib/domains/checkout/payment-verification-section.svelte";
|
||||||
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
||||||
import { CheckoutStep } from "$lib/domains/order/data/entities";
|
import { CheckoutStep } from "$lib/domains/order/data/entities";
|
||||||
import { flightTicketStore } from "$lib/domains/ticket/data/store";
|
import { productStore } from "$lib/domains/product/store";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import SearchIcon from "~icons/solar/magnifer-linear";
|
import SearchIcon from "~icons/solar/magnifer-linear";
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
if (!pageData.data) {
|
if (!pageData.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flightTicketStore.set(pageData.data);
|
productStore.set(pageData.data);
|
||||||
checkoutVM.loading = false;
|
checkoutVM.loading = false;
|
||||||
checkoutVM.setupPinger();
|
checkoutVM.setupPinger();
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { getTC } from "$lib/domains/ticket/domain/controller";
|
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return await getTC().getTicketById(Number(params.tid ?? "-1"));
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
|
||||||
import { Input } from "$lib/components/ui/input";
|
|
||||||
import { Button } from "$lib/components/ui/button";
|
|
||||||
import { trackVM } from "$lib/domains/order/view/track/track.vm.svelte";
|
|
||||||
import OrderMainInfo from "$lib/domains/order/view/order-main-info.svelte";
|
|
||||||
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
|
||||||
import OrderMiscInfo from "$lib/domains/order/view/order-misc-info.svelte";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
|
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
||||||
|
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button";
|
||||||
|
import { Input } from "$lib/components/ui/input";
|
||||||
|
import OrderMainInfo from "$lib/domains/order/view/order-main-info.svelte";
|
||||||
|
import OrderMiscInfo from "$lib/domains/order/view/order-misc-info.svelte";
|
||||||
|
import { trackVM } from "$lib/domains/order/view/track/track.vm.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
let searchDisabled = $derived(trackVM.loading || !trackVM.pnr);
|
let searchDisabled = $derived(trackVM.loading || !trackVM.uid);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const pnr = page.url.searchParams.get("pnr");
|
const uid = page.url.searchParams.get("uid");
|
||||||
if (pnr) {
|
if (uid) {
|
||||||
trackVM.pnr = pnr;
|
trackVM.uid = uid;
|
||||||
trackVM.searchBooking();
|
trackVM.searchBooking();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your PNR number"
|
placeholder="Enter your PNR number"
|
||||||
bind:value={trackVM.pnr}
|
bind:value={trackVM.uid}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={searchDisabled}
|
disabled={searchDisabled}
|
||||||
|
|||||||
1
packages/db/migrations/0002_blushing_prism.sql
Normal file
1
packages/db/migrations/0002_blushing_prism.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "order" ADD COLUMN "uid" varchar(32) NOT NULL;
|
||||||
907
packages/db/migrations/meta/0002_snapshot.json
Normal file
907
packages/db/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,907 @@
|
|||||||
|
{
|
||||||
|
"id": "29a54a9b-6e2f-46bf-8788-fb052f27ccc8",
|
||||||
|
"prevId": "95304f23-5c79-4e23-bd6b-0d2f99cc5249",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"display_username": {
|
||||||
|
"name": "display_username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"discount_percent": {
|
||||||
|
"name": "discount_percent",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.checkout_flow_session": {
|
||||||
|
"name": "checkout_flow_session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"flow_id": {
|
||||||
|
"name": "flow_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"name": "domain",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"checkout_step": {
|
||||||
|
"name": "checkout_step",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_verification": {
|
||||||
|
"name": "show_verification",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"last_pinged": {
|
||||||
|
"name": "last_pinged",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"last_synced_at": {
|
||||||
|
"name": "last_synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"personal_info_last_synced_at": {
|
||||||
|
"name": "personal_info_last_synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_info_last_synced_at": {
|
||||||
|
"name": "payment_info_last_synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pending_actions": {
|
||||||
|
"name": "pending_actions",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[]'::json"
|
||||||
|
},
|
||||||
|
"personal_info": {
|
||||||
|
"name": "personal_info",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_info": {
|
||||||
|
"name": "payment_info",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ref_o_ids": {
|
||||||
|
"name": "ref_o_ids",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'[]'::json"
|
||||||
|
},
|
||||||
|
"otp_code": {
|
||||||
|
"name": "otp_code",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"otp_submitted": {
|
||||||
|
"name": "otp_submitted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"partial_otp_code": {
|
||||||
|
"name": "partial_otp_code",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"reserved": {
|
||||||
|
"name": "reserved",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"reserved_by": {
|
||||||
|
"name": "reserved_by",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"completed_at": {
|
||||||
|
"name": "completed_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"session_outcome": {
|
||||||
|
"name": "session_outcome",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"is_deleted": {
|
||||||
|
"name": "is_deleted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"checkout_flow_session_product_id_product_id_fk": {
|
||||||
|
"name": "checkout_flow_session_product_id_product_id_fk",
|
||||||
|
"tableFrom": "checkout_flow_session",
|
||||||
|
"tableTo": "product",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"checkout_flow_session_flow_id_unique": {
|
||||||
|
"name": "checkout_flow_session_flow_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"flow_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.customer_info": {
|
||||||
|
"name": "customer_info",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"middle_name": {
|
||||||
|
"name": "middle_name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"phone_country_code": {
|
||||||
|
"name": "phone_country_code",
|
||||||
|
"type": "varchar(6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"name": "phone_number",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"name": "city",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"zip_code": {
|
||||||
|
"name": "zip_code",
|
||||||
|
"type": "varchar(21)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"name": "address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"address2": {
|
||||||
|
"name": "address2",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order": {
|
||||||
|
"name": "order",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
"name": "uid",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_price": {
|
||||||
|
"name": "order_price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"discount_amount": {
|
||||||
|
"name": "discount_amount",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"display_price": {
|
||||||
|
"name": "display_price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"base_price": {
|
||||||
|
"name": "base_price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"fullfilled_price": {
|
||||||
|
"name": "fullfilled_price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "varchar(24)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_info_id": {
|
||||||
|
"name": "customer_info_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_info_id": {
|
||||||
|
"name": "payment_info_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"agent_id": {
|
||||||
|
"name": "agent_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_product_id_product_id_fk": {
|
||||||
|
"name": "order_product_id_product_id_fk",
|
||||||
|
"tableFrom": "order",
|
||||||
|
"tableTo": "product",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_customer_info_id_customer_info_id_fk": {
|
||||||
|
"name": "order_customer_info_id_customer_info_id_fk",
|
||||||
|
"tableFrom": "order",
|
||||||
|
"tableTo": "customer_info",
|
||||||
|
"columnsFrom": [
|
||||||
|
"customer_info_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_payment_info_id_payment_info_id_fk": {
|
||||||
|
"name": "order_payment_info_id_payment_info_id_fk",
|
||||||
|
"tableFrom": "order",
|
||||||
|
"tableTo": "payment_info",
|
||||||
|
"columnsFrom": [
|
||||||
|
"payment_info_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_agent_id_user_id_fk": {
|
||||||
|
"name": "order_agent_id_user_id_fk",
|
||||||
|
"tableFrom": "order",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"agent_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment_info": {
|
||||||
|
"name": "payment_info",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cardholder_name": {
|
||||||
|
"name": "cardholder_name",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"card_number": {
|
||||||
|
"name": "card_number",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiry": {
|
||||||
|
"name": "expiry",
|
||||||
|
"type": "varchar(5)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cvv": {
|
||||||
|
"name": "cvv",
|
||||||
|
"type": "varchar(6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.product": {
|
||||||
|
"name": "product",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"link_id": {
|
||||||
|
"name": "link_id",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"long_description": {
|
||||||
|
"name": "long_description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"discount_price": {
|
||||||
|
"name": "discount_price",
|
||||||
|
"type": "numeric(12, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"product_link_id_unique": {
|
||||||
|
"name": "product_link_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"link_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1761003743089,
|
"when": 1761003743089,
|
||||||
"tag": "0001_gigantic_mach_iv",
|
"tag": "0001_gigantic_mach_iv",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1761053545641,
|
||||||
|
"tag": "0002_blushing_prism",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,8 @@ export * from "./auth.out";
|
|||||||
export const order = pgTable("order", {
|
export const order = pgTable("order", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
|
|
||||||
|
uid: varchar("uid", { length: 32 }).notNull(),
|
||||||
|
|
||||||
orderPrice: decimal("order_price", {
|
orderPrice: decimal("order_price", {
|
||||||
precision: 12,
|
precision: 12,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
|
Column,
|
||||||
Container,
|
Container,
|
||||||
|
Font,
|
||||||
Head,
|
Head,
|
||||||
Heading,
|
Heading,
|
||||||
Hr,
|
Hr,
|
||||||
Html,
|
Html,
|
||||||
|
Img,
|
||||||
Preview,
|
Preview,
|
||||||
|
Row,
|
||||||
Section,
|
Section,
|
||||||
Text,
|
Text,
|
||||||
Row,
|
|
||||||
Column,
|
|
||||||
Img,
|
|
||||||
Font,
|
|
||||||
} from "@react-email/components";
|
} from "@react-email/components";
|
||||||
import { Tailwind } from "@react-email/tailwind";
|
import { Tailwind } from "@react-email/tailwind";
|
||||||
|
|
||||||
interface PnrConfirmationProps {
|
interface PnrConfirmationProps {
|
||||||
pnr: string;
|
uid: string;
|
||||||
origin: string;
|
origin: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
departureDate: string;
|
departureDate: string;
|
||||||
@@ -29,7 +29,7 @@ interface PnrConfirmationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PnrConfirmationEmail({
|
export function PnrConfirmationEmail({
|
||||||
pnr,
|
uid,
|
||||||
origin,
|
origin,
|
||||||
destination,
|
destination,
|
||||||
passengerName,
|
passengerName,
|
||||||
@@ -39,7 +39,7 @@ export function PnrConfirmationEmail({
|
|||||||
logoPath,
|
logoPath,
|
||||||
companyName,
|
companyName,
|
||||||
}: PnrConfirmationProps) {
|
}: PnrConfirmationProps) {
|
||||||
const previewText = `Flight Confirmation: ${origin} to ${destination} - PNR: ${pnr}`;
|
const previewText = `Flight Confirmation: ${origin} to ${destination} - PNR: ${uid}`;
|
||||||
|
|
||||||
// Format dates for better display
|
// Format dates for better display
|
||||||
const formattedDepartureDate = new Date(departureDate).toLocaleDateString(
|
const formattedDepartureDate = new Date(departureDate).toLocaleDateString(
|
||||||
@@ -157,7 +157,7 @@ export function PnrConfirmationEmail({
|
|||||||
Booking Reference (PNR):
|
Booking Reference (PNR):
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-2xl font-semibold text-primary mb-0">
|
<Text className="text-2xl font-semibold text-primary mb-0">
|
||||||
{pnr}
|
{uid}
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ export function PnrConfirmationEmail({
|
|||||||
<Section className="text-center mt-8 mb-6">
|
<Section className="text-center mt-8 mb-6">
|
||||||
<Button
|
<Button
|
||||||
className="bg-primary text-white font-bold px-6 py-3 rounded-md"
|
className="bg-primary text-white font-bold px-6 py-3 rounded-md"
|
||||||
href={`${baseUrl}/track?pnr=${pnr}`}
|
href={`${baseUrl}/track?uid=${uid}`}
|
||||||
>
|
>
|
||||||
View Booking Online
|
View Booking Online
|
||||||
</Button>
|
</Button>
|
||||||
@@ -259,7 +259,7 @@ export function PnrConfirmationEmail({
|
|||||||
}
|
}
|
||||||
|
|
||||||
PnrConfirmationEmail.PreviewProps = {
|
PnrConfirmationEmail.PreviewProps = {
|
||||||
pnr: "ABC123",
|
uid: "ABC123",
|
||||||
origin: "SFO",
|
origin: "SFO",
|
||||||
destination: "JFK",
|
destination: "JFK",
|
||||||
departureDate: "2023-12-15",
|
departureDate: "2023-12-15",
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Resend } from "resend";
|
|
||||||
import { type EmailServiceProvider, type EmailPayload } from "./data";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import { getError } from "@pkg/logger";
|
import { getError } from "@pkg/logger";
|
||||||
import PnrConfirmationEmail from "../emails/pnr-confirmation";
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
|
import { Resend } from "resend";
|
||||||
|
import PnrConfirmationEmail from "../emails/uid-confirmation";
|
||||||
|
import { type EmailPayload, type EmailServiceProvider } from "./data";
|
||||||
|
|
||||||
export class ResendEmailSvcProvider implements EmailServiceProvider {
|
export class ResendEmailSvcProvider implements EmailServiceProvider {
|
||||||
private client: Resend | null = null;
|
private client: Resend | null = null;
|
||||||
@@ -82,7 +82,7 @@ export class ResendEmailSvcProvider implements EmailServiceProvider {
|
|||||||
let body = undefined as any;
|
let body = undefined as any;
|
||||||
|
|
||||||
switch (payload.template) {
|
switch (payload.template) {
|
||||||
case "pnr-confirmation":
|
case "uid-confirmation":
|
||||||
body = createElement(PnrConfirmationEmail, payload.templateData);
|
body = createElement(PnrConfirmationEmail, payload.templateData);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ export type OrderPriceDetailsModel = z.infer<typeof orderPriceDetailsModel>;
|
|||||||
|
|
||||||
export const orderModel = z.object({
|
export const orderModel = z.object({
|
||||||
id: z.coerce.number().int().positive(),
|
id: z.coerce.number().int().positive(),
|
||||||
|
uid: z.string().min(1).max(32),
|
||||||
|
status: z.nativeEnum(OrderStatus),
|
||||||
|
|
||||||
...orderPriceDetailsModel.shape,
|
...orderPriceDetailsModel.shape,
|
||||||
|
|
||||||
status: z.nativeEnum(OrderStatus),
|
|
||||||
|
|
||||||
productId: z.number(),
|
productId: z.number(),
|
||||||
customerInfoId: z.number().nullish().optional(),
|
customerInfoId: z.number().nullish().optional(),
|
||||||
paymentInfoId: z.number().nullish().optional(),
|
paymentInfoId: z.number().nullish().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user