/** * Executable reproducers for the highest-severity findings that were previously inspection-only. * All Tier-2 (direct instantiation, real Prisma + stubbed collaborators). * * C-1 ✅ FIXED (was 🔴 "BookingsService trusts client reviewedTotalMinor: a booking is * stored with totalMinor=1 while the server fare engine computed ~30000") — * createOneWayBooking now runs assertTotalNotUnderAuthoritative before persisting; * a forged low total is rejected, not stored. Test below updated to assert this. * C-4 🔴 finalizePaymentSuccess confirms a booking without comparing the paid amount: an intent for * 1 minor confirms a 30000 booking. * C-6 🔴 Concurrent WALLET payments double-spend one balance (no row lock): a wallet funded for one * ticket pays for two. */ import { BookingsService } from "../src/modules/bookings/bookings.service"; import { PaymentsService } from "../src/modules/payments/payments.service"; import { FareEngineService } from "../src/modules/fare-engine/fare-engine.service"; import { CurrencyService } from "../src/modules/currency/currency.service"; import { getTestPrisma, disconnectTestPrisma } from "./setup/prisma"; import { truncateAllPassenger, seedCore, IDS } from "./fixtures/seed-core"; function asyncStub(): any { return new Proxy({}, { get: () => async () => undefined }); } /** * Wraps a PrismaClient so that inside `$transaction(cb)`, every `walletAccount.update` waits until * BOTH concurrent transactions have finished their `walletAccount.findUnique` (balance read). This * deterministically forces the exact interleaving a real multi-request system permits, exposing the * service's unlocked check-then-act (no SELECT … FOR UPDATE). Only scheduling is controlled — the * service's own logic runs unmodified. */ function makeRaceWrappedPrisma(real: any, parties: number) { let arrived = 0; let release!: () => void; const gate = new Promise((r) => (release = r)); const signalRead = () => { if (++arrived >= parties) release(); }; return new Proxy(real, { get(target, prop, receiver) { if (prop === "$transaction") { return (cb: (tx: any) => unknown, opts?: unknown) => target.$transaction((tx: any) => { const wrappedTx = new Proxy(tx, { get(t, p) { if (p === "walletAccount") { return { findUnique: async (args: unknown) => { const res = await t.walletAccount.findUnique(args); signalRead(); return res; }, update: async (args: unknown) => { await gate; // hold the write until both reads are done return t.walletAccount.update(args); }, }; } return t[p]; }, }); return cb(wrappedTx); }, opts); } return Reflect.get(target, prop, receiver); }, }); } let seq = 0; async function makeSchedule(prisma: any) { const train = await prisma.train.create({ data: { number: `CR-${++seq}`, name: "T" } }); return prisma.trainSchedule.create({ data: { trainId: train.id, routeId: IDS.route, originStationId: IDS.stationA, destinationStationId: IDS.stationB, departureAt: new Date(Date.now() + 86_400_000), arrivalAt: new Date(Date.now() + 90_000_000), durationMinutes: 60, }, }); } describe("Critical reproducers (Tier-2)", () => { const prisma = getTestPrisma(); beforeEach(async () => { await truncateAllPassenger(prisma); await seedCore(prisma); }); afterAll(async () => { await disconnectTestPrisma(); }); // ── C-1 ────────────────────────────────────────────────────────────────── it("C-1 🔴 booking stores client reviewedTotalMinor=1 while the fare engine computed ~30000", async () => { const passenger = await prisma.passenger.create({ data: {} }); const schedule = await makeSchedule(prisma); // Stop times so origin/dest resolve on the schedule. await prisma.tripStopTime.createMany({ data: [ { scheduleId: schedule.id, stationId: IDS.stationA, sequence: 1 }, { scheduleId: schedule.id, stationId: IDS.stationB, sequence: 2 }, ], }); // Coach + seat for the passenger to occupy. const coach = await prisma.coach.create({ data: { coachTypeId: IDS.coachType, number: `C-${seq}` }, }); const seat = await prisma.seat.create({ data: { coachId: coach.id, seatNumber: "1A", row: 1, col: "1" }, }); // A real server fare source (tripId match → highest priority): 30000 minor. await prisma.fareRule.create({ data: { tripId: schedule.id, seatClassId: IDS.seatClassLocal, baseFareMinor: 30_000, currency: "ETB", validFrom: new Date("2020-01-01"), }, }); const hold = await prisma.seatHold.create({ data: { scheduleId: schedule.id, seatIds: [seat.id], passengerId: passenger.id, expiresAt: new Date(Date.now() + 3_600_000), }, }); const bookings = new BookingsService( prisma as any, asyncStub(), // dataSource asyncStub(), // seatsService (confirmSeats no-op) { emit: () => true } as any, // eventEmitter asyncStub(), // verifaydaService (PASSPORT path skips it anyway) asyncStub(), // currencyService (ETB path skips it) asyncStub(), // fareEngine (FareRule short-circuits before this) asyncStub(), // auditService ); const dto = { passengerId: passenger.id, scheduleId: schedule.id, holdId: hold.id, originStationId: IDS.stationA, destinationStationId: IDS.stationB, seatClassId: IDS.seatClassLocal, bookingType: "ONE_WAY", reviewedTotalMinor: 1, // the forged client total passengers: [ { seatId: seat.id, passengerName: "Mallory Adult", dateOfBirth: new Date("1990-01-01"), idDocumentType: "PASSPORT", passportNumber: "P123", passportCountry: "ET", nationality: "Ethiopian", // NOTE: no seatFareMinor → not "allFaresProvided" → reviewedTotalMinor is trusted }, ], }; // FIXED (was 🔴): createOneWayBooking now runs the forged reviewedTotalMinor through // assertTotalNotUnderAuthoritative before ever writing a Booking row — a client total // below the server-computed fare (minus a 1% rounding tolerance) is rejected outright, // not silently persisted. Confirmed by reading bookings.service.ts's C-1 guard comment. await expect((bookings as any).createOneWayBooking(dto)).rejects.toThrow( /Booking total does not match the authoritative fare/i, ); const stored = await prisma.booking.findFirst({ where: { scheduleId: schedule.id, passengerId: passenger.id } }); expect(stored).toBeNull(); // no under-priced booking left behind }); // ── C-4 ────────────────────────────────────────────────────────────────── it("C-4 🔴 finalizePaymentSuccess confirms a 30000 booking from an intent of 1 (no amount check)", async () => { const passenger = await prisma.passenger.create({ data: {} }); const schedule = await makeSchedule(prisma); const booking = await prisma.booking.create({ data: { bookingRef: "PAY-0001", passengerId: passenger.id, scheduleId: schedule.id, totalMinor: 30_000, status: "PENDING_PAYMENT", }, }); const intent = await prisma.paymentIntent.create({ data: { bookingId: booking.id, amountMinor: 1, // wildly short payment method: "WALLET", status: "PROCESSING", }, }); const payments = new PaymentsService( prisma as any, { confirmSeats: async () => undefined } as any, { generate: async () => undefined } as any, // must not throw (re-thrown otherwise) { emit: () => true } as any, asyncStub(), // paymentClient asyncStub(), // currencyService (not used on this path) asyncStub(), // auditService ); await payments.finalizePaymentSuccess({ intentId: intent.id }); const after = await prisma.booking.findUnique({ where: { id: booking.id } }); // Confirmed despite intent.amountMinor (1) ≠ booking.totalMinor (30000). expect(after?.status).toBe("CONFIRMED"); }); // ── C-6 ────────────────────────────────────────────────────────────────── it("C-6 🔴 two concurrent WALLET payments double-spend a single-ticket balance", async () => { const passenger = await prisma.passenger.create({ data: {} }); const schedule = await makeSchedule(prisma); // Wallet funded for exactly ONE ticket. await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 30_000 }, }); const mkBooking = (ref: string) => prisma.booking.create({ data: { bookingRef: ref, passengerId: passenger.id, scheduleId: schedule.id, totalMinor: 30_000, status: "PENDING_PAYMENT", }, }); const b1 = await mkBooking("W-0001"); const b2 = await mkBooking("W-0002"); // Race-wrapped prisma forces both balance reads to complete before either debit writes. const racePrisma = makeRaceWrappedPrisma(prisma, 2); const payments = new PaymentsService( racePrisma as any, { confirmSeats: async () => undefined } as any, { generate: async () => undefined } as any, { emit: () => true } as any, asyncStub(), asyncStub(), asyncStub(), ); const [bk1, bk2] = await Promise.all([ prisma.booking.findUnique({ where: { id: b1.id }, include: { seats: true } }), prisma.booking.findUnique({ where: { id: b2.id }, include: { seats: true } }), ]); const [r1, r2] = await Promise.allSettled([ (payments as any).initiateWalletPayment(bk1), (payments as any).initiateWalletPayment(bk2), ]); const succeeded = await prisma.paymentIntent.count({ where: { bookingId: { in: [b1.id, b2.id] }, status: { in: ["SUCCEEDED", "PROCESSING"] } }, }); const debits = await prisma.walletLedgerEntry.count({ where: { type: "DEBIT" } }); const wallet = await prisma.walletAccount.findUnique({ where: { passengerId: passenger.id }, }); // Double-spend signature: two successful debits from a one-ticket balance, or a negative // balance. A correctly-locked wallet allows exactly one. const totalDebited = debits * 30_000; const doubleSpent = (succeeded === 2 && totalDebited > 30_000) || (wallet?.balanceMinor ?? 0) < 0; expect(doubleSpent).toBe(true); expect([r1.status, r2.status]).toEqual(["fulfilled", "fulfilled"]); }); });