/** * 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; /** * Optional per-stop check-in-cutoff/travel-time overrides, keyed by station label (A/B/C). * Lets a spec seed a distinct `checkinMinutesBefore` override and/or `travelMinutesToStop` * per stop without changing the zero-arg call sites the other specs rely on. */ export interface RouteStopOverrides { A?: { checkinMinutesBefore?: number; travelMinutesToStop?: number }; B?: { checkinMinutesBefore?: number; travelMinutesToStop?: number }; C?: { checkinMinutesBefore?: number; travelMinutesToStop?: number }; } /** * 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 { const rows = await prisma.$queryRawUnsafe>( `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, stopOverrides: RouteStopOverrides = {}): Promise { 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, ...stopOverrides.A }, { stationId: IDS.stationB, sequence: 2, distanceKm: DISTANCE.B, ...stopOverrides.B }, { stationId: IDS.stationC, sequence: 3, distanceKm: DISTANCE.C, ...stopOverrides.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, stopOverrides: RouteStopOverrides = {}): Promise { await truncateAllPassenger(prisma); await seedCore(prisma, stopOverrides); }