mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Hermetic E2E harness targeting pricing integrity and backoffice config: - e2e/ docker Postgres (5544) + prepare.sh/run.sh one-command runner + HTML report - 6 suites / 23 tests reproducing pricing, FX, wallet, refund, config and auth defects (see docs/ISSUES.md); docs/e2e-test-matrix.md documents the matrix - two-tier harness (slim module boot + direct service instantiation) to work around the IAM/RabbitMQ/file-type boot wall - .env.test.example tracked; loader falls back to it for fresh checkouts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3.8 KiB
TypeScript
82 lines
3.8 KiB
TypeScript
/**
|
|
* Currency / FX suite (matrix Suite C). Exercises CurrencyService directly.
|
|
* C2 🔴 missing rate: getExchangeRate() silently returns 1.0 while getRateOrThrow() throws —
|
|
* the display path degrades but the charge path errors on the SAME condition (divergence).
|
|
* C3 🔴 a future-dated rate is applied immediately (no `effectiveDate <= now` filter).
|
|
* C5 🔴 conversion routines disagree on units: displayMinorToChargeMajor / convertMinorToChargeMajor
|
|
* return MAJOR units, convertEtbMinorToChargeMinor returns MINOR — a 100x unit landmine both
|
|
* written into fields named `amountMinor` at their call sites.
|
|
*/
|
|
import { CurrencyService } from "../src/modules/currency/currency.service";
|
|
import { createServiceHarness, ServiceHarness } from "./setup/slim-app";
|
|
import { resetAndSeedCore, USD_TO_ETB } from "./fixtures/seed-core";
|
|
|
|
describe("Pricing — CurrencyService (Suite C)", () => {
|
|
let harness: ServiceHarness;
|
|
let currency: CurrencyService;
|
|
|
|
beforeAll(async () => {
|
|
harness = await createServiceHarness();
|
|
currency = harness.moduleRef.get(CurrencyService);
|
|
});
|
|
afterAll(async () => {
|
|
await harness?.close();
|
|
});
|
|
beforeEach(async () => {
|
|
await resetAndSeedCore(harness.prisma);
|
|
});
|
|
|
|
it("C2 🔴 same DB state, 100x divergence: getExchangeRate → 1.0, getRateOrThrow → 100 (via inverse)", async () => {
|
|
// Remove only the DIRECT USD→ETB row; the inverse ETB→USD (0.01) from the fixture stays.
|
|
await harness.prisma.currencyExchangeRate.deleteMany({
|
|
where: { fromCurrency: "USD", toCurrency: "ETB" },
|
|
});
|
|
|
|
// Display/fare path (getExchangeRate) has NO inverse fallback → silently returns 1.0 (wrong).
|
|
await expect(currency.getExchangeRate("USD" as any, "ETB" as any)).resolves.toBe(1.0);
|
|
|
|
// Charge path (getRateOrThrow) DOES fall back to the inverse → 1 / 0.01 = 100 (correct).
|
|
await expect(
|
|
currency.getRateOrThrow("USD" as any, "ETB" as any),
|
|
).resolves.toBe(USD_TO_ETB);
|
|
// → the display fare and the charged amount for the same trip differ by 100x.
|
|
});
|
|
|
|
it("C2b 🔴 truly-missing pair: getExchangeRate → 1.0 (silent), getRateOrThrow → throws", async () => {
|
|
await harness.prisma.currencyExchangeRate.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ fromCurrency: "USD", toCurrency: "ETB" },
|
|
{ fromCurrency: "ETB", toCurrency: "USD" },
|
|
],
|
|
},
|
|
});
|
|
await expect(currency.getExchangeRate("USD" as any, "ETB" as any)).resolves.toBe(1.0);
|
|
await expect(
|
|
currency.getRateOrThrow("USD" as any, "ETB" as any),
|
|
).rejects.toThrow(/No exchange rate/i);
|
|
});
|
|
|
|
it("C3 🔴 a future-dated rate is used right now (no effective-date gate)", async () => {
|
|
const future = new Date(Date.now() + 365 * 24 * 3600 * 1000);
|
|
await harness.prisma.currencyExchangeRate.create({
|
|
data: { fromCurrency: "ETB", toCurrency: "USD", rate: 999, effectiveDate: future },
|
|
});
|
|
|
|
// Correct behavior: ignore not-yet-effective rates. Actual: latest-by-date wins immediately.
|
|
const rate = await currency.getExchangeRate("ETB" as any, "USD" as any);
|
|
expect(rate).toBe(999);
|
|
});
|
|
|
|
it("C5 🔴 conversion routines return different UNITS for the same money (100x apart)", async () => {
|
|
// 100000 ETB minor = 1000.00 ETB. With ETB→USD = 1/100:
|
|
const asMajor = await currency.convertMinorToChargeMajor(100000, "ETB", "USD"); // → 10.00 (major)
|
|
const asMinor = await currency.convertEtbMinorToChargeMinor(100000, "USD"); // → 1000 (minor)
|
|
|
|
expect(asMajor).toBeCloseTo(1000 / USD_TO_ETB, 2); // 10.00
|
|
expect(asMinor).toBe(Math.round((100000 * 1) / USD_TO_ETB)); // 1000
|
|
// Same amount, but the two results differ by 100x — and both feed fields named `amountMinor`.
|
|
expect(asMinor).toBe(asMajor * 100);
|
|
});
|
|
});
|