mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
/**
|
||
* Reference pricing suite — proves the slim harness boots and exercises FareEngineService directly.
|
||
* Also confirms matrix findings against the running engine:
|
||
* D1 — a promo with percentOff > 100 drives the total NEGATIVE (no clamp at 0).
|
||
* C1 ✅ FIXED (was 🔴 "missing USD→ETB FX rate is silently substituted with 1.0, fares
|
||
* collapse ~100x"): CurrencyService.getExchangeRate() now fails closed on a missing
|
||
* rate (see the "H-2: fail closed" comment in currency.service.ts, and
|
||
* pricing-currency.e2e-spec.ts's C2/C2b) — FareEngineService.calculate() calls it
|
||
* internally, so a missing rate now correctly rejects the fare calculation instead of
|
||
* silently underpricing it. Updated below to assert the current, correct behavior.
|
||
*/
|
||
import { FareEngineService } from "../src/modules/fare-engine/fare-engine.service";
|
||
import {
|
||
createServiceHarness,
|
||
ServiceHarness,
|
||
} from "./setup/slim-app";
|
||
import {
|
||
IDS,
|
||
resetAndSeedCore,
|
||
DISTANCE,
|
||
USD_TO_ETB,
|
||
} from "./fixtures/seed-core";
|
||
|
||
describe("Pricing — FareEngineService (slim harness)", () => {
|
||
let harness: ServiceHarness;
|
||
let fareEngine: FareEngineService;
|
||
|
||
beforeAll(async () => {
|
||
harness = await createServiceHarness();
|
||
fareEngine = harness.moduleRef.get(FareEngineService);
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await harness?.close();
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await resetAndSeedCore(harness.prisma);
|
||
});
|
||
|
||
// nationality 'Ethiopian' → LOCAL seat class (3.00 ETB/km) and ETB billing (rate 1).
|
||
const baseDto = () => ({
|
||
routeId: IDS.route,
|
||
originStationId: IDS.stationA,
|
||
destinationStationId: IDS.stationB,
|
||
seatClassId: IDS.seatClassLocal,
|
||
nationality: "Ethiopian",
|
||
adultCount: 1,
|
||
childCount: 0,
|
||
});
|
||
|
||
it("boots and computes a positive baseline fare (A→B, local, 1 adult)", async () => {
|
||
const result = await fareEngine.calculate(baseDto() as any);
|
||
// 100km × 3.00 ETB/km × 1 × USD_TO_ETB(100) = 30000 minor (see seat-class formula).
|
||
expect(result.totalMinor).toBeGreaterThan(0);
|
||
expect(result.totalMinor).toBe(DISTANCE.B * 3 * USD_TO_ETB);
|
||
});
|
||
|
||
it("D1 🔴 promo percentOff=150 produces a NEGATIVE total (no floor at 0)", async () => {
|
||
await harness.prisma.promotion.create({
|
||
data: {
|
||
title: "Overshoot",
|
||
code: "OVER150",
|
||
percentOff: 150,
|
||
validUntil: new Date(Date.now() + 86_400_000),
|
||
active: true,
|
||
},
|
||
});
|
||
|
||
const result = await fareEngine.calculate({
|
||
...baseDto(),
|
||
promoCode: "OVER150",
|
||
} as any);
|
||
|
||
// Expected (correct) behavior: total clamped at >= 0. Actual: negative.
|
||
expect(result.totalMinor).toBeLessThan(0);
|
||
});
|
||
|
||
it("D2 🔴 fixed amountOffMinor larger than subtotal drives total NEGATIVE", async () => {
|
||
const base = await fareEngine.calculate(baseDto() as any); // 30000 minor
|
||
await harness.prisma.promotion.create({
|
||
data: {
|
||
title: "Huge fixed",
|
||
code: "FIXEDBIG",
|
||
amountOffMinor: base.totalMinor + 10_000,
|
||
validUntil: new Date(Date.now() + 86_400_000),
|
||
active: true,
|
||
},
|
||
});
|
||
|
||
const result = await fareEngine.calculate({
|
||
...baseDto(),
|
||
promoCode: "FIXEDBIG",
|
||
} as any);
|
||
expect(result.totalMinor).toBeLessThan(0);
|
||
});
|
||
|
||
it("D4 🔴 promo with percentOff=0 is treated as FIXED (0 is falsy) and applies amountOffMinor", async () => {
|
||
// A promo intended as '0% off' but also carrying a stray fixed amount: the falsy check
|
||
// `promo.percentOff ? percent : amountOffMinor` wrongly applies the fixed discount.
|
||
await harness.prisma.promotion.create({
|
||
data: {
|
||
title: "Zero percent",
|
||
code: "ZERO0",
|
||
percentOff: 0,
|
||
amountOffMinor: 5000,
|
||
validUntil: new Date(Date.now() + 86_400_000),
|
||
active: true,
|
||
},
|
||
});
|
||
|
||
const base = await fareEngine.calculate(baseDto() as any);
|
||
const withPromo = await fareEngine.calculate({
|
||
...baseDto(),
|
||
promoCode: "ZERO0",
|
||
} as any);
|
||
|
||
// A true 0% promo should not change the price; here it deducts the fixed 5000.
|
||
expect(withPromo.totalMinor).toBe(base.totalMinor - 5000);
|
||
});
|
||
|
||
it("C1 ✅ a missing USD→ETB rate now rejects the fare calculation instead of silently pricing at parity", async () => {
|
||
const withRate = await fareEngine.calculate(baseDto() as any);
|
||
expect(withRate.totalMinor).toBeGreaterThan(0);
|
||
|
||
// Remove the USD→ETB rate the seat-class formula multiplies by.
|
||
await harness.prisma.currencyExchangeRate.deleteMany({
|
||
where: { fromCurrency: "USD", toCurrency: "ETB" },
|
||
});
|
||
|
||
await expect(fareEngine.calculate(baseDto() as any)).rejects.toThrow(/No exchange rate configured/i);
|
||
});
|
||
});
|