Files
domain-wall/apps/frontend/src/lib/domains/ticket/domain/ticket.n.order.skiddaddler.ts
2025-10-20 17:07:41 +03:00

446 lines
12 KiB
TypeScript

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;
}
}