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>
129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
/**
|
|
* Deterministic core fixtures for the pricing E2E suites.
|
|
*
|
|
* The repo's `prisma/seed.ts` is entirely commented out (every step disabled), so the harness
|
|
* builds its own minimal, fully-controlled graph: coach type → seat classes → stations → route
|
|
* with distance-bearing stops → FX rates. Fixed UUIDs let specs reference entities directly.
|
|
*
|
|
* Uses a bare PrismaClient (not the Nest PrismaService) so it can run in jest globalSetup or
|
|
* inside a spec without booting the app. Reads DATABASE_URL from process.env (load-env sets it).
|
|
*/
|
|
import { PrismaClient } from "@prisma/client";
|
|
|
|
export const IDS = {
|
|
coachType: "00000000-0000-4000-8000-000000000001",
|
|
seatClassLocal: "00000000-0000-4000-8000-000000000010",
|
|
seatClassIntl: "00000000-0000-4000-8000-000000000011",
|
|
stationA: "00000000-0000-4000-8000-000000000020",
|
|
stationB: "00000000-0000-4000-8000-000000000021",
|
|
stationC: "00000000-0000-4000-8000-000000000022",
|
|
route: "00000000-0000-4000-8000-000000000030",
|
|
} as const;
|
|
|
|
/** Route stop distances (km from origin). A=0, B=100, C=250 → A→B is 100km, A→C is 250km. */
|
|
export const DISTANCE = { A: 0, B: 100, C: 250 } as const;
|
|
|
|
/**
|
|
* FX rate chosen so the seat-class distance formula (which multiplies an ETB/km rate by the
|
|
* USD→ETB rate — see fare-engine.service.ts:157) yields whole ETB-minor amounts. 100 makes the
|
|
* major→minor scaling line up; a realistic rate (e.g. 132) would visibly distort domestic fares,
|
|
* which is itself a finding the suites probe.
|
|
*/
|
|
export const USD_TO_ETB = 100;
|
|
export const ETB_TO_DJF = 1.8;
|
|
|
|
/** TRUNCATE every table in the `passenger` schema (except Prisma's migration bookkeeping). */
|
|
export async function truncateAllPassenger(prisma: PrismaClient): Promise<void> {
|
|
const rows = await prisma.$queryRawUnsafe<Array<{ tablename: string }>>(
|
|
`SELECT tablename FROM pg_tables WHERE schemaname = 'passenger' AND tablename <> '_prisma_migrations'`,
|
|
);
|
|
if (rows.length === 0) return;
|
|
const list = rows.map((r) => `passenger."${r.tablename}"`).join(", ");
|
|
await prisma.$executeRawUnsafe(
|
|
`TRUNCATE ${list} RESTART IDENTITY CASCADE`,
|
|
);
|
|
}
|
|
|
|
/** Insert the deterministic core graph. Call after truncateAllPassenger. */
|
|
export async function seedCore(prisma: PrismaClient): Promise<void> {
|
|
const past = new Date("2020-01-01T00:00:00.000Z");
|
|
|
|
await prisma.coachType.create({
|
|
data: {
|
|
id: IDS.coachType,
|
|
code: "STD",
|
|
name: "Standard Coach",
|
|
type: "passenger",
|
|
},
|
|
});
|
|
|
|
// LOCAL and INTERNATIONAL seat classes share the coach type + bedPosition (null = regular seat),
|
|
// which is exactly how fare-engine picks the nationality-matched class (findFirst on those keys).
|
|
await prisma.seatClass.createMany({
|
|
data: [
|
|
{
|
|
id: IDS.seatClassLocal,
|
|
coachTypeId: IDS.coachType,
|
|
name: "Local Standard",
|
|
nationalityType: "LOCAL",
|
|
bedPosition: null,
|
|
baseFareMinor: 300, // 3.00 ETB/km
|
|
premiumMinor: 0,
|
|
insuranceFeeMinor: 0,
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: IDS.seatClassIntl,
|
|
coachTypeId: IDS.coachType,
|
|
name: "Intl Standard",
|
|
nationalityType: "INTERNATIONAL",
|
|
bedPosition: null,
|
|
baseFareMinor: 500, // 5.00 ETB/km
|
|
premiumMinor: 0,
|
|
insuranceFeeMinor: 0,
|
|
isActive: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
await prisma.station.createMany({
|
|
data: [
|
|
{ id: IDS.stationA, code: "AAA", name: "Alpha", city: "Alpha City", sequence: 1 },
|
|
{ id: IDS.stationB, code: "BBB", name: "Bravo", city: "Bravo City", sequence: 2 },
|
|
{ id: IDS.stationC, code: "CCC", name: "Charlie", city: "Charlie City", sequence: 3 },
|
|
],
|
|
});
|
|
|
|
await prisma.route.create({
|
|
data: {
|
|
id: IDS.route,
|
|
code: "RT-MAIN",
|
|
name: "Main Line",
|
|
effectiveFrom: past,
|
|
active: true,
|
|
stops: {
|
|
create: [
|
|
{ stationId: IDS.stationA, sequence: 1, distanceKm: DISTANCE.A },
|
|
{ stationId: IDS.stationB, sequence: 2, distanceKm: DISTANCE.B },
|
|
{ stationId: IDS.stationC, sequence: 3, distanceKm: DISTANCE.C },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
await prisma.currencyExchangeRate.createMany({
|
|
data: [
|
|
{ fromCurrency: "USD", toCurrency: "ETB", rate: USD_TO_ETB, effectiveDate: new Date() },
|
|
{ fromCurrency: "ETB", toCurrency: "USD", rate: 1 / USD_TO_ETB, effectiveDate: new Date() },
|
|
{ fromCurrency: "ETB", toCurrency: "DJF", rate: ETB_TO_DJF, effectiveDate: new Date() },
|
|
{ fromCurrency: "DJF", toCurrency: "ETB", rate: 1 / ETB_TO_DJF, effectiveDate: new Date() },
|
|
],
|
|
});
|
|
}
|
|
|
|
/** Convenience: reset + seed in one call. */
|
|
export async function resetAndSeedCore(prisma: PrismaClient): Promise<void> {
|
|
await truncateAllPassenger(prisma);
|
|
await seedCore(prisma);
|
|
}
|