mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
Adding all the tests and fixes to the passengers app
This commit is contained in:
@@ -2,12 +2,14 @@ import { test, expect } from "@playwright/test";
|
||||
import { resultsUrl } from "../../fixtures/data";
|
||||
|
||||
/**
|
||||
* UA-1b — the first money-chain break. For a non-Ethiopian (USD) search the results card shows the
|
||||
* USD-converted `displayAmountMinor`, but the internal `baseFareMinor` stays in raw ETB minor. The
|
||||
* two diverge exactly 100× (the USD→ETB rate). The portal stores `Math.min(baseFareMinor)` on select,
|
||||
* so the ETB value — not the USD one the passenger saw — is what flows downstream.
|
||||
* UA-1b ✅ — for a non-Ethiopian (USD) search the results card shows the USD-converted
|
||||
* `displayAmountMinor`, and the internal `baseFareMinor` is the ETB source it was converted from
|
||||
* (exactly the USD→ETB rate apart — a correct conversion, not a mislabel). The passenger sees and
|
||||
* carries forward the USD value; the ETB basis is stored honestly on the booking as `currency: ETB`
|
||||
* (proven end-to-end by UA-2). This pins the display layer so a regression that shows the raw ETB
|
||||
* number, or drops the conversion, is caught.
|
||||
*/
|
||||
test("UA-1b: USD card display fare diverges 100x from the internal baseFareMinor", async ({ page }) => {
|
||||
test("UA-1b: USD card shows the USD fare, correctly converted from the internal ETB base", async ({ page }) => {
|
||||
const searchDone = page.waitForResponse(
|
||||
(r) => r.url().includes("/search") && r.request().method() === "POST",
|
||||
);
|
||||
@@ -16,11 +18,12 @@ test("UA-1b: USD card display fare diverges 100x from the internal baseFareMinor
|
||||
const cls = out?.faresByClass?.[0];
|
||||
|
||||
expect(out.displayCurrency).toBe("USD");
|
||||
// The display fare is the converted USD amount; baseFareMinor is the untouched ETB minor.
|
||||
// The USD display fare is the ETB base converted at the USD→ETB rate (100×), not a parity mislabel.
|
||||
expect(cls.displayAmountMinor).toBeGreaterThan(0);
|
||||
expect(cls.displayAmountMinor).toBeLessThan(cls.baseFareMinor);
|
||||
expect(cls.baseFareMinor).toBe(cls.displayAmountMinor * 100);
|
||||
|
||||
// The DOM shows the USD value (formatFare divides by 100, 2dp) — e.g. "USD 12.50".
|
||||
// The DOM shows the USD value the passenger pays (formatFare divides by 100, 2dp) — e.g. "USD 12.50".
|
||||
const usdMajor = (cls.displayAmountMinor / 100).toFixed(2);
|
||||
await expect(page.getByText(new RegExp(`USD\\s*${usdMajor.replace(".", "\\.")}`)).first()).toBeVisible({
|
||||
timeout: 20_000,
|
||||
|
||||
@@ -8,13 +8,12 @@ test.afterAll(async () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* UA-2 🔴 — full one-way USD booking (Other nationality, INTERNATIONAL class, WALLET). This surfaces
|
||||
* the money-chain incoherence for non-ETB currencies: the browser sends reviewedTotalMinor = the raw
|
||||
* ETB baseFareMinor (125000), but the server stores Booking.totalMinor = the USD display value (1250).
|
||||
* The two differ 100× — the client-computed "reviewed total" the C-1 path trusts is in the wrong
|
||||
* currency, yet the stored/charged total is the display one. We pin both so a regression is caught.
|
||||
* UA-2 ✅ — full one-way USD booking (Other nationality, INTERNATIONAL class, WALLET). The money chain
|
||||
* is now COHERENT: the passenger sees and agrees to a USD amount (displayCurrency/displayTotalMinor),
|
||||
* while the stored charge basis is honestly labeled ETB (currency/totalMinor). The two are the same
|
||||
* fare at the USD→ETB rate — no longer a mislabeled 100× divergence.
|
||||
*/
|
||||
test("UA-2: USD booking — reviewed total (ETB) and stored total (USD) diverge 100x", async ({ page }) => {
|
||||
test("UA-2: USD booking — passenger amount in USD, charge basis stored coherently in ETB", async ({ page }) => {
|
||||
const r = await bookTrip(page, { nationality: "Other", paymentMethod: "WALLET" });
|
||||
expect(r.displayCurrency).toBe("USD");
|
||||
expect(r.confirmed).toBe(true);
|
||||
@@ -22,12 +21,17 @@ test("UA-2: USD booking — reviewed total (ETB) and stored total (USD) diverge
|
||||
const booking = await prisma.booking.findUniqueOrThrow({ where: { id: r.bookingId } });
|
||||
const intent = await prisma.paymentIntent.findUniqueOrThrow({ where: { bookingId: r.bookingId } });
|
||||
|
||||
// The browser sent the USD display value the passenger saw as the reviewed total…
|
||||
expect(r.reviewedTotalMinor).toBe(r.cardDisplayMinor);
|
||||
// …but the booking stored the raw ETB base fare (100× larger, un-converted).
|
||||
expect(booking.totalMinor).toBe(r.cardBaseFareMinor);
|
||||
expect(booking.totalMinor).toBe(r.reviewedTotalMinor * 100); // 🔴 the divergence
|
||||
expect(intent.amountMinor).toBe(booking.totalMinor);
|
||||
// Passenger-facing: the USD amount they saw and agreed to (what the browser reviewed).
|
||||
expect(booking.displayCurrency).toBe("USD");
|
||||
expect(booking.displayTotalMinor).toBe(r.reviewedTotalMinor);
|
||||
expect(r.reviewedTotalMinor).toBe(r.cardDisplayMinor);
|
||||
|
||||
// Stored charge basis: ETB, coherently labeled (no more USD mislabel).
|
||||
expect(booking.currency).toBe("ETB");
|
||||
expect(booking.totalMinor).toBe(r.cardBaseFareMinor); // the ETB fare
|
||||
expect(booking.totalMinor).toBe(r.reviewedTotalMinor * 100); // ETB == USD display × rate
|
||||
|
||||
// The charge/intent moves the ETB amount; booking is confirmed.
|
||||
expect(intent.amountMinor).toBe(booking.totalMinor);
|
||||
expect(booking.status).toBe("CONFIRMED");
|
||||
});
|
||||
|
||||
@@ -8,20 +8,22 @@ test.afterAll(async () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* UA-3w — Djiboutian/DJF, WALLET. DJF is a zero-decimal currency, but the portal renders every fare
|
||||
* through formatFare (always /100, 2dp) → "DJF x.yy". WALLET short-circuits past the charge-currency
|
||||
* conversion, so here we can only assert the display currency is stamped DJF and the chain is intact.
|
||||
* UA-3w ✅ — Djiboutian/DJF, WALLET. The money chain is now COHERENT: the passenger sees and agrees
|
||||
* to a DJF amount (displayCurrency/displayTotalMinor), while the stored charge basis is honestly
|
||||
* labeled ETB (currency/totalMinor). Same fare, two correctly-labeled currencies — no mislabel.
|
||||
*/
|
||||
test("UA-3w: DJF WALLET booking is stamped DJF; reviewed (DJF) vs stored (ETB) diverge", async ({ page }) => {
|
||||
test("UA-3w: DJF WALLET booking — passenger amount in DJF, charge basis stored coherently in ETB", async ({ page }) => {
|
||||
const r = await bookTrip(page, { nationality: "Djiboutian", paymentMethod: "WALLET" });
|
||||
expect(r.displayCurrency).toBe("DJF");
|
||||
expect(r.confirmed).toBe(true);
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({ where: { id: r.bookingId } });
|
||||
// Passenger-facing: the DJF amount they saw and agreed to.
|
||||
expect(booking.displayCurrency).toBe("DJF");
|
||||
// Mirror image of UA-2: here the browser sent the DJF display value as the reviewed total…
|
||||
expect(booking.displayTotalMinor).toBe(r.reviewedTotalMinor);
|
||||
expect(r.reviewedTotalMinor).toBe(r.cardDisplayMinor);
|
||||
// …while the booking stored the raw ETB base. The chain uses different currencies at each hop.
|
||||
// Stored charge basis: ETB, coherently labeled (no more DJF mislabel).
|
||||
expect(booking.currency).toBe("ETB");
|
||||
expect(booking.totalMinor).toBe(r.cardBaseFareMinor);
|
||||
expect(booking.status).toBe("CONFIRMED");
|
||||
});
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { resultsUrl } from "../../fixtures/data";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { bookTrip } from "../../fixtures/booking-flow";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
test.afterAll(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
/**
|
||||
* UA-6 🔴 — round-trip return leg is UNBOOKABLE. A ROUND_TRIP search returns an inbound (C→A) leg
|
||||
* that reports seat availability, but its `coachTypes`/`faresByClass` come back EMPTY: the fare engine
|
||||
* cannot price the reverse direction on this route (the segment runs high→low stop sequence, so the
|
||||
* distance resolves non-positive and every class is dropped). With no priced coach the portal renders
|
||||
* no coach option, so the round-trip wizard cannot advance past inbound selection — the full-UI
|
||||
* round-trip booking flow is blocked. This test pins the gap through the portal's own search call.
|
||||
*
|
||||
* When reverse-leg pricing is fixed (priced inbound coachTypes), replace this with the full
|
||||
* two-leg booking flow: `bookTrip(page, { tripType: "ROUND_TRIP" })` asserting total = 2× base fare
|
||||
* and one seat per leg.
|
||||
* UA-6 ✅ — round-trip books BOTH legs. The return leg (C→A) traverses the seeded route high→low; the
|
||||
* fare engine now prices the reverse direction by absolute distance (previously it threw "origin must
|
||||
* come before destination" and dropped every class, leaving the inbound leg with seats but no priced
|
||||
* coach — unbookable). The full two-leg wizard now completes: outbound + return seat, and a total of
|
||||
* 2× the one-way fare.
|
||||
*/
|
||||
test("UA-6: round-trip inbound leg has availability but no priced coach (booking blocked)", async ({
|
||||
test("UA-6: round-trip books both legs — return leg priced, one seat per leg, total = 2× one-way fare", async ({
|
||||
page,
|
||||
}) => {
|
||||
const searchDone = page.waitForResponse(
|
||||
(r) => r.url().includes("/search") && r.request().method() === "POST",
|
||||
);
|
||||
await page.goto(resultsUrl({ tripType: "ROUND_TRIP" }));
|
||||
const data = (await (await searchDone).json())?.data;
|
||||
const r = await bookTrip(page, { tripType: "ROUND_TRIP", paymentMethod: "WALLET" });
|
||||
expect(r.confirmed).toBe(true);
|
||||
|
||||
expect(data.journeyType).toBe("ROUND_TRIP");
|
||||
const inbound = data.inbound?.[0];
|
||||
expect(inbound).toBeTruthy();
|
||||
const booking = await prisma.booking.findUniqueOrThrow({ where: { id: r.bookingId } });
|
||||
expect(booking.bookingType).toBe("ROUND_TRIP");
|
||||
expect(booking.status).toBe("CONFIRMED");
|
||||
|
||||
// The return leg exists and shows availability…
|
||||
expect(inbound.hasAvailability).toBe(true);
|
||||
expect(Object.values(inbound.availabilityByClass ?? {}).some((n) => Number(n) > 0)).toBe(true);
|
||||
// …but no coach type is priced, so nothing is selectable in the UI. 🔴
|
||||
expect(inbound.coachTypes ?? []).toHaveLength(0);
|
||||
expect(inbound.faresByClass ?? []).toHaveLength(0);
|
||||
// One seat per leg (leg 1 outbound + leg 2 return) for a single passenger.
|
||||
const seats = await prisma.bookingSeat.findMany({ where: { bookingId: r.bookingId } });
|
||||
expect(seats.length).toBe(2);
|
||||
expect(new Set(seats.map((s) => s.leg)).size).toBe(2);
|
||||
|
||||
// Both legs cover the same A↔C distance, so the round-trip total is 2× the one-way base fare (ETB).
|
||||
expect(r.cardBaseFareMinor).toBeGreaterThan(0);
|
||||
expect(booking.totalMinor).toBe(r.cardBaseFareMinor * 2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user