Files
edr-platform/apps/edr-passenger-api/test/pricing-fare-engine.e2e-spec.ts

134 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
});
});