diff --git a/apps/admin/src/lib/domains/customerinfo/view/customer-details-card.svelte b/apps/admin/src/lib/domains/customerinfo/view/customer-details-card.svelte index cad5ceb..517caab 100644 --- a/apps/admin/src/lib/domains/customerinfo/view/customer-details-card.svelte +++ b/apps/admin/src/lib/domains/customerinfo/view/customer-details-card.svelte @@ -1,7 +1,7 @@ - +
Full Name @@ -54,4 +54,4 @@ {/if}
-
+ diff --git a/apps/frontend/src/lib/domains/customerinfo/controller.ts b/apps/frontend/src/lib/domains/customerinfo/controller.ts index dcd117d..11b24d6 100644 --- a/apps/frontend/src/lib/domains/customerinfo/controller.ts +++ b/apps/frontend/src/lib/domains/customerinfo/controller.ts @@ -45,3 +45,7 @@ export class CustomerInfoController { return this.repo.getAllCustomerInfo(); } } + +export function getCustomerInfoController() { + return new CustomerInfoController(new CustomerInfoRepository(db)); +} diff --git a/apps/frontend/src/lib/domains/order/data/repository.ts b/apps/frontend/src/lib/domains/order/data/repository.ts index 896c975..afafccd 100644 --- a/apps/frontend/src/lib/domains/order/data/repository.ts +++ b/apps/frontend/src/lib/domains/order/data/repository.ts @@ -1,14 +1,14 @@ import { ERROR_CODES, type Result } from "$lib/core/data.types"; 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 { nanoid } from "nanoid"; import { fullOrderModel, - limitedOrderWithTicketInfoModel, + limitedOrderWithProductModel, OrderStatus, type FullOrderModel, - type LimitedOrderWithTicketInfoModel, + type LimitedOrderWithProductModel, type NewOrderModel, } from "./entities"; @@ -19,10 +19,10 @@ export class OrderRepository { this.db = db; } - async listActiveOrders(): Promise> { + async listActiveOrders(): Promise> { const conditions = [ or( - eq(order.status, OrderStatus.PENDING_FULLFILLMENT), + eq(order.status, OrderStatus.PENDING_FULFILLMENT), eq(order.status, OrderStatus.PARTIALLY_FULFILLED), ), isNotNull(order.agentId), @@ -34,29 +34,29 @@ export class OrderRepository { displayPrice: true, basePrice: true, discountAmount: true, - pricePerPassenger: true, + orderPrice: true, fullfilledPrice: true, status: true, }, with: { - flightTicketInfo: { + product: true, + customerInfo: { columns: { - id: true, - departure: true, - arrival: true, - departureDate: true, - returnDate: true, - flightType: true, - passengerCounts: true, + firstName: true, + middleName: true, + lastName: true, + email: true, + country: true, + state: true, }, }, }, }); - const out = [] as LimitedOrderWithTicketInfoModel[]; + const out = [] as LimitedOrderWithProductModel[]; for (const order of qRes) { - const parsed = limitedOrderWithTicketInfoModel.safeParse({ + const parsed = limitedOrderWithProductModel.safeParse({ ...order, }); if (!parsed.success) { @@ -78,21 +78,13 @@ export class OrderRepository { return { data: out }; } - async getOrderByPNR(pnr: string): Promise> { + async getOrderByUID(uid: string): Promise> { const out = await this.db.query.order.findFirst({ - where: eq(order.pnr, pnr), - with: { flightTicketInfo: true }, + where: eq(order.uid, uid), + with: { customerInfo: true, product: true }, }); if (!out) return {}; - const relatedPassengerInfos = await this.db.query.passengerInfo.findMany({ - where: eq(passengerInfo.orderId, out.id), - with: { passengerPii: true }, - }); - const parsed = fullOrderModel.safeParse({ - ...out, - emailAccount: undefined, - passengerInfos: relatedPassengerInfos, - }); + const parsed = fullOrderModel.safeParse({ ...out }); if (!parsed.success) { return { error: getError( @@ -111,21 +103,25 @@ export class OrderRepository { async createOrder( payload: NewOrderModel, - ): Promise> { - const pnr = nanoid(9).toUpperCase(); + ): Promise> { + const uid = nanoid(12).toUpperCase(); try { const out = await this.db .insert(order) .values({ + uid, displayPrice: payload.displayPrice.toFixed(3), basePrice: payload.basePrice.toFixed(3), discountAmount: payload.discountAmount.toFixed(3), + fullfilledPrice: payload.fullfilledPrice.toFixed(3), + orderPrice: payload.orderPrice.toFixed(3), - flightTicketInfoId: payload.flightTicketInfoId, paymentInfoId: payload.paymentInfoId, - status: OrderStatus.PENDING_FULLFILLMENT, - pnr, + status: OrderStatus.PENDING_FULFILLMENT, + + customerInfoId: payload.customerInfoId, + productId: payload.productId, createdAt: new Date(), updatedAt: new Date(), @@ -133,7 +129,7 @@ export class OrderRepository { .returning({ id: order.id }) .execute(); - return { data: { id: out[0]?.id, pnr } }; + return { data: { id: out[0]?.id, uid } }; } catch (e) { return { error: getError( diff --git a/apps/frontend/src/lib/domains/order/domain/controller.ts b/apps/frontend/src/lib/domains/order/domain/controller.ts index d1710ee..1d1e989 100644 --- a/apps/frontend/src/lib/domains/order/domain/controller.ts +++ b/apps/frontend/src/lib/domains/order/domain/controller.ts @@ -16,8 +16,8 @@ export class OrderController { return this.repo.createOrder(payload); } - async getOrderByPNR(pnr: string) { - return this.repo.getOrderByPNR(pnr); + async getOrderByUID(uid: string) { + return this.repo.getOrderByUID(uid); } async markOrdersAsFulfilled(oids: number[]) { diff --git a/apps/frontend/src/lib/domains/order/domain/router.ts b/apps/frontend/src/lib/domains/order/domain/router.ts index 3c40f6d..fd615f5 100644 --- a/apps/frontend/src/lib/domains/order/domain/router.ts +++ b/apps/frontend/src/lib/domains/order/domain/router.ts @@ -1,13 +1,14 @@ import { SessionOutcome } from "$lib/domains/ckflow/data/entities"; import { getCKUseCases } from "$lib/domains/ckflow/domain/usecases"; +import { getCustomerInfoController } from "$lib/domains/customerinfo/controller"; import { EmailerUseCases } from "$lib/domains/email/domain/usecases"; -import { createOrderPayloadModel } from "$lib/domains/order/data/entities"; -import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository"; -import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller"; +import { + CheckoutStep, + createOrderPayloadModel, +} from "$lib/domains/order/data/entities"; import { PaymentInfoRepository } from "$lib/domains/paymentinfo/data/repository"; import { PaymentInfoUseCases } from "$lib/domains/paymentinfo/domain/usecases"; -import { CheckoutStep } from "$lib/domains/ticket/data/entities"; -import { getTC } from "$lib/domains/ticket/domain/controller"; +import { getProductUseCases } from "$lib/domains/product/usecases"; import { createTRPCRouter, publicProcedure } from "$lib/trpc/t"; import { db } from "@pkg/db"; import { getError, Logger } from "@pkg/logger"; @@ -22,10 +23,8 @@ export const orderRouter = createTRPCRouter({ .mutation(async ({ input }) => { const pduc = new PaymentInfoUseCases(new PaymentInfoRepository(db)); const oc = new OrderController(new OrderRepository(db)); - const pc = new PassengerInfoController( - new PassengerInfoRepository(db), - ); - const tc = getTC(); + const cc = getCustomerInfoController(); + const puc = getProductUseCases(); const emailUC = new EmailerUseCases(); 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; } try { // Get order details for email - const orderDetails = await oc.getOrderByPNR(pnr); + const orderDetails = await oc.getOrderByPNR(uid); if (!orderDetails.data) { return out; @@ -134,10 +133,10 @@ export const orderRouter = createTRPCRouter({ // Send the email with React component directly const emailResult = await emailUC.sendEmailWithTemplate({ to: passengerEmail, - subject: `Flight Confirmation: ${ticketInfo.departure} to ${ticketInfo.arrival} - PNR: ${pnr}`, - template: "pnr-confirmation", + subject: `Flight Confirmation: ${ticketInfo.departure} to ${ticketInfo.arrival} - PNR: ${uid}`, + template: "uid-confirmation", templateData: { - pnr: pnr, + uid: uid, origin: ticketInfo.departure, destination: ticketInfo.arrival, departureDate: new Date( @@ -175,10 +174,10 @@ export const orderRouter = createTRPCRouter({ return out; }), - findByPNR: publicProcedure - .input(z.object({ pnr: z.string() })) + findByUID: publicProcedure + .input(z.object({ uid: z.string() })) .query(async ({ input }) => { const oc = new OrderController(new OrderRepository(db)); - return oc.getOrderByPNR(input.pnr); + return oc.getOrderByUID(input.uid); }), }); diff --git a/apps/frontend/src/lib/domains/order/view/order-main-info.svelte b/apps/frontend/src/lib/domains/order/view/order-main-info.svelte index 77da108..bca25f4 100644 --- a/apps/frontend/src/lib/domains/order/view/order-main-info.svelte +++ b/apps/frontend/src/lib/domains/order/view/order-main-info.svelte @@ -1,13 +1,9 @@
- {#if order.emailAccount} - -
-
- - Account Information -
-

{order.emailAccount.email}

-
- {/if} - - -
-
-
- - Flight Details -
- -
- - {order.flightTicketInfo.flightType} - - - {order.flightTicketInfo.cabinClass} - -
-
- - -
+ TODO: SHOW PRODUCT DETSIL INFO HERE
diff --git a/apps/frontend/src/lib/domains/order/view/track/track.vm.svelte.ts b/apps/frontend/src/lib/domains/order/view/track/track.vm.svelte.ts index 5d87bf5..a6b5a3c 100644 --- a/apps/frontend/src/lib/domains/order/view/track/track.vm.svelte.ts +++ b/apps/frontend/src/lib/domains/order/view/track/track.vm.svelte.ts @@ -1,16 +1,16 @@ -import type { FullOrderModel } from "@pkg/logic/domains/order/data/entities"; 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 { get } from "svelte/store"; export class TrackViewModel { - pnr = $state(""); + uid = $state(""); loading = $state(false); bookingData = $state(undefined); error = $state(null); async searchBooking() { - if (!this.pnr) { + if (!this.uid) { this.error = "Please enter a PNR number"; return; } @@ -24,7 +24,7 @@ export class TrackViewModel { this.loading = true; 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) { this.error = result.error.message; diff --git a/apps/frontend/src/lib/domains/ticket/data/entities/create.entities.ts b/apps/frontend/src/lib/domains/ticket/data/entities/create.entities.ts deleted file mode 100644 index 4195cb3..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/entities/create.entities.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "$lib/domains/passengerinfo/data/entities"; diff --git a/apps/frontend/src/lib/domains/ticket/data/entities/enums.ts b/apps/frontend/src/lib/domains/ticket/data/entities/enums.ts deleted file mode 100644 index 39067c5..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/entities/enums.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@pkg/logic/domains/ticket/data/entities/enums"; diff --git a/apps/frontend/src/lib/domains/ticket/data/entities/index.ts b/apps/frontend/src/lib/domains/ticket/data/entities/index.ts deleted file mode 100644 index 9c9f302..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@pkg/logic/domains/ticket/data/entities/index"; diff --git a/apps/frontend/src/lib/domains/ticket/data/repository.ts b/apps/frontend/src/lib/domains/ticket/data/repository.ts deleted file mode 100644 index 9fecfa0..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/repository.ts +++ /dev/null @@ -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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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 { - return this.ckUseCases.areCheckoutAlreadyLiveForOrder(refOids); - } -} diff --git a/apps/frontend/src/lib/domains/ticket/data/scrape.data.source.ts b/apps/frontend/src/lib/domains/ticket/data/scrape.data.source.ts deleted file mode 100644 index 6d1af60..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/scrape.data.source.ts +++ /dev/null @@ -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> { - const { data, error } = await betterFetch>( - `${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 ?? [] }; - } -} diff --git a/apps/frontend/src/lib/domains/ticket/data/store.ts b/apps/frontend/src/lib/domains/ticket/data/store.ts deleted file mode 100644 index ee8fb8a..0000000 --- a/apps/frontend/src/lib/domains/ticket/data/store.ts +++ /dev/null @@ -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(); - -export const ticketSearchStore = writable({ - loadMore: false, - sessionId: nanoid(), - ticketType: TicketType.Return, - cabinClass: CabinClass.Economy, - passengerCounts: { adults: 1, children: 0 }, - departure: "", - arrival: "", - departureDate: "", - returnDate: "", - meta: {}, -}); diff --git a/apps/frontend/src/lib/domains/ticket/domain/controller.ts b/apps/frontend/src/lib/domains/ticket/domain/controller.ts deleted file mode 100644 index 02dadda..0000000 --- a/apps/frontend/src/lib/domains/ticket/domain/controller.ts +++ /dev/null @@ -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> { - 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)); -} diff --git a/apps/frontend/src/lib/domains/ticket/domain/router.ts b/apps/frontend/src/lib/domains/ticket/domain/router.ts deleted file mode 100644 index 11fcba8..0000000 --- a/apps/frontend/src/lib/domains/ticket/domain/router.ts +++ /dev/null @@ -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; - }), - - 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); - }), -}); diff --git a/apps/frontend/src/lib/domains/ticket/domain/ticket.n.order.skiddaddler.ts b/apps/frontend/src/lib/domains/ticket/domain/ticket.n.order.skiddaddler.ts deleted file mode 100644 index 8182247..0000000 --- a/apps/frontend/src/lib/domains/ticket/domain/ticket.n.order.skiddaddler.ts +++ /dev/null @@ -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; - 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> { - // 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(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; - } -} diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket.vm.svelte.ts b/apps/frontend/src/lib/domains/ticket/view/ticket.vm.svelte.ts deleted file mode 100644 index 342508e..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket.vm.svelte.ts +++ /dev/null @@ -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([]); - renderedTickets = $state([]); - - 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 { - 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(); diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/baggage-info.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/baggage-info.svelte deleted file mode 100644 index b9a21fc..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/baggage-info.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - -{#if data} - Baggage Info -
- -
-
- - Personal Item -
- {data.bagsInfo.includedPersonalBags} included -
- - -
-
- - Cabin Baggage -
- {#if data.bagsInfo.hasHandBagsSupport} - {data.bagsInfo.includedHandBags} included - {:else} - Not available - {/if} -
- - -
-
- - Checked Baggage -
- {#if data.bagsInfo.hasCheckedBagsSupport} - {data.bagsInfo.includedCheckedBags} included - {:else} - Not available - {/if} -
-
-{/if} diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-card.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-card.svelte deleted file mode 100644 index fd7f05e..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-card.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
- - - -
-
-
- -
- - {#if data.priceDetails.basePrice !== data.priceDetails.displayPrice} - {@const discountPercentage = Math.round( - (1 - - data.priceDetails.displayPrice / - data.priceDetails.basePrice) * - 100, - )} -
- - {discountPercentage}% OFF - -
- {convertAndFormatCurrency(data.priceDetails.basePrice)} -
- - {convertAndFormatCurrency(data.priceDetails.displayPrice)} - -
- {:else} - - {convertAndFormatCurrency(data.priceDetails.displayPrice)} - - {/if} - -
- - - Flight Info - - - -
-
-
diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details-modal.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details-modal.svelte deleted file mode 100644 index a84a75f..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details-modal.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - {@render children()} - - -
- -
-
-
diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details.svelte deleted file mode 100644 index 5ee3307..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-details.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - -{#if !hideCheckoutBtn} -
- - {convertAndFormatCurrency(data.priceDetails.displayPrice)} - - -
-{/if} diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-itinerary.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-itinerary.svelte deleted file mode 100644 index 1385d95..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-itinerary.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -
-
-
- {departure} - - {departureTime} - - {departureDate} -
- -
-
-
-
-
- -
-
-
- - - {durationFormatted} - - -
- {#if stops !== undefined} - 0 ? "secondary" : "outline"} - class="font-medium" - > - {#if stops > 0} - {stops} {stops === 1 ? "Stop" : "Stops"} - {:else} - Direct Flight - {/if} - - {/if} - {#if airlineName} - {airlineName} - {/if} -
-
- -
- {destination} - - {arrivalTime} - - {arrivalDate} -
-
-
diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-legs-overview.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-legs-overview.svelte deleted file mode 100644 index f634e83..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/ticket-legs-overview.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - -{#if isReturnTicket} - - Outbound - -{/if} - - -{#if isReturnTicket} -
- - {#if isReturnTicket} - - Inbound - - {/if} - - -{:else} -
-{/if} diff --git a/apps/frontend/src/lib/domains/ticket/view/ticket/trip-details.svelte b/apps/frontend/src/lib/domains/ticket/view/ticket/trip-details.svelte deleted file mode 100644 index cbb28dc..0000000 --- a/apps/frontend/src/lib/domains/ticket/view/ticket/trip-details.svelte +++ /dev/null @@ -1,212 +0,0 @@ - - -Trip Details - - - - -
- Outbound -
-
-
- - {data.flightIteneraries.outbound[0].departure - .station.code} - - - - {data.flightIteneraries.outbound[ - data.flightIteneraries.outbound.length - 1 - ].destination.station.code} - -
- - {data.flightIteneraries.outbound[0].airline.name} - -
- - {formatDuration( - data.flightIteneraries.outbound.reduce( - (acc, curr) => acc + curr.durationSeconds, - 0, - ), - )} - -
-
-
- -
- {#each data.flightIteneraries.outbound as flight, index} - {#if index > 0} -
- {/if} -
-
-
- Flight {flight.flightNumber} - - • {flight.airline.name} - -
- {formatDuration(flight.durationSeconds)} -
-
-
- - {formatDateTime(flight.departure.localTime) - .time} - - - {formatDateTime(flight.departure.localTime) - .date} - - - {flight.departure.station.city} ({flight - .departure.station.code}) - -
-
- - {formatDateTime(flight.destination.localTime) - .time} - - - {formatDateTime(flight.destination.localTime) - .date} - - - {flight.destination.station.city} ({flight - .destination.station.code}) - -
-
-
- {/each} -
-
-
- - {#if isReturnTicket && data.flightIteneraries.inbound.length > 0} - - -
- Inbound -
-
-
- - {data.flightIteneraries.inbound[0].departure - .station.code} - - - - {data.flightIteneraries.inbound[ - data.flightIteneraries.inbound.length - 1 - ].destination.station.code} - -
- - {data.flightIteneraries.inbound[0].airline.name} - -
- - {formatDuration( - data.flightIteneraries.inbound.reduce( - (acc, curr) => acc + curr.durationSeconds, - 0, - ), - )} - -
-
-
- - -
- {#each data.flightIteneraries.inbound as flight, index} - {#if index > 0} -
- {/if} -
-
-
- Flight {flight.flightNumber} - - • {flight.airline.name} - -
- {formatDuration( - flight.durationSeconds, - )} -
-
-
- - {formatDateTime( - flight.departure.localTime, - ).time} - - - {formatDateTime( - flight.departure.localTime, - ).date} - - - {flight.departure.station.city} ({flight - .departure.station.code}) - -
-
- - {formatDateTime( - flight.destination.localTime, - ).time} - - - {formatDateTime( - flight.destination.localTime, - ).date} - - - {flight.destination.station.city} ({flight - .destination.station.code}) - -
-
-
- {/each} -
-
-
- {/if} -
diff --git a/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts new file mode 100644 index 0000000..06415eb --- /dev/null +++ b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.server.ts @@ -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"), + ); +}; diff --git a/apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.svelte b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte similarity index 96% rename from apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.svelte rename to apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte index 630533e..4e15bab 100644 --- a/apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.svelte +++ b/apps/frontend/src/routes/(main)/checkout/[sid]/[plid]/+page.svelte @@ -13,7 +13,7 @@ import PaymentVerificationSection from "$lib/domains/checkout/payment-verification-section.svelte"; import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte"; 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 { toast } from "svelte-sonner"; import SearchIcon from "~icons/solar/magnifer-linear"; @@ -31,7 +31,7 @@ if (!pageData.data) { return; } - flightTicketStore.set(pageData.data); + productStore.set(pageData.data); checkoutVM.loading = false; checkoutVM.setupPinger(); diff --git a/apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.server.ts b/apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.server.ts deleted file mode 100644 index 7153a1f..0000000 --- a/apps/frontend/src/routes/(main)/checkout/[sid]/[tid]/+page.server.ts +++ /dev/null @@ -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")); -}; diff --git a/apps/frontend/src/routes/(main)/track/+page.svelte b/apps/frontend/src/routes/(main)/track/+page.svelte index dc22e55..0cd0fdc 100644 --- a/apps/frontend/src/routes/(main)/track/+page.svelte +++ b/apps/frontend/src/routes/(main)/track/+page.svelte @@ -1,20 +1,20 @@