mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
Adding all the tests and fixes to the passengers app
This commit is contained in:
@@ -226,6 +226,20 @@ Two structural facts frame everything:
|
||||
`getExchangeRate(USD,ETB)=1.0` but `getRateOrThrow(USD,ETB)=100` → displayed fare and charged
|
||||
amount differ 100×. Matrix C2/C5.
|
||||
- **Fix**: one shared conversion routine with one rounding rule and one fallback policy.
|
||||
- **Resolution (booking-record coherence, UA-1b/UA-2/UA-3w)** ✅ — the stored booking record no longer
|
||||
mislabels its amount. Every `booking.create` path (`bookings.service.ts` one-way/round-trip/
|
||||
transit/round-trip-transit + the guest equivalents) now stores `currency: Currency.ETB` (the actual
|
||||
currency of `totalMinor`/the ETB charge basis) instead of the display currency. The passenger-facing
|
||||
amount stays in `displayCurrency`/`displayTotalMinor` (Birr for Ethiopian, DJF for Djiboutian, USD
|
||||
for Other), and every read endpoint already prefers those. The portal `results/page.tsx` on-select
|
||||
now carries the passenger-currency fare (`displayAmountMinor`) forward, aligning with the seats
|
||||
page's already-`displayAmountMinor` fare logic. Net effect (agreed model **A**): the passenger sees
|
||||
and is charged in their own currency; the internal charge basis stays ETB (the unit every downstream
|
||||
calc — wallet debit, loyalty, refund, gateway conversion — already assumes), now honestly labeled.
|
||||
Proven by `e2e-ui/specs/portal/ua2-usd-booking.spec.ts` and `ua3-djf.spec.ts` (UA-3w): `currency`
|
||||
is `ETB` while `displayCurrency`/`displayTotalMinor` carry USD/DJF — red before the fix
|
||||
(`currency` was `USD`/`DJF`), green after; UA-1 (ETB) unchanged. The deeper H-3 (unify
|
||||
`getExchangeRate`/`getRateOrThrow`) and H-4 (branded Minor/Major units) refactors remain open.
|
||||
|
||||
### H-4 ✅ Conversion routines return different UNITS for the same money
|
||||
- **Where**: `displayMinorToChargeMajor`/`convertMinorToChargeMajor` return **major** units;
|
||||
@@ -379,6 +393,13 @@ Two structural facts frame everything:
|
||||
`:124-132` blocks only same-train+same-route+same-day, so the same train can run two routes at
|
||||
overlapping times. Matrix H3/H4.
|
||||
- **Fix**: reject past `departureAt`; widen the overlap check to the train across all routes.
|
||||
- **Resolution (past departure)** ✅ — `schedules.service.ts` `createSchedule` now rejects a
|
||||
`departureAt` in the past (`dep.getTime() < Date.now()` → 400) alongside the existing
|
||||
`arrivalAt > departureAt` check. Proven by `e2e-ui/specs/backoffice/config-validation.spec.ts`
|
||||
(BC-10): a 2020 departure → 400 while a future schedule still creates (red before the guard, green
|
||||
after). Scoped to creation (an admin may still need to edit metadata on an already-departed
|
||||
schedule via `updateSchedule`). The cross-route train double-booking overlap widening remains a
|
||||
follow-up (not exercised by BC-10).
|
||||
|
||||
### M-5 🔎 Deletes ignore referencing bookings; one cascade is non-transactional
|
||||
- **Where**: station delete ignores bookings (`stations.service.ts:110-137`); seat-class delete
|
||||
|
||||
@@ -78,13 +78,13 @@ Axes: booking type {one-way, round-trip} × pax mix {1A, 2A, 1A+1C-free, 1A+2C (
|
||||
| ID | Scenario | Key inputs | Price cross-check expectation | Finding tie-in |
|
||||
|---|---|---|---|---|
|
||||
| **UA-1** | One-way, 1 adult, Economy Regular, Ethiopian/ETB, WALLET, no promo | ETB LOCAL regular class | Baseline green: card price == fare-breakdown == reviewedTotalMinor == Booking.totalMinor == PaymentIntent.amountMinor == wallet DEBIT. All equal, 2dp. (Optionally assert `LoyaltyAccount.pointsBalance` accrual here if the accrual link is kept.) | matrix A6 (baseline) |
|
||||
| **UA-1b** | One-way, 1 adult, **Other/USD** — assert card `displayAmountMinor` vs on-select stored `baseFareMinor` | nationality OTHER → USD; INTL class | **First chain break:** assert the value stored on select (`Math.min(baseFareMinor)`, results:320) equals what flows to review, and flag if it differs from the card's `displayAmountMinor` (results:821) | div #1 (new); H-3/H-4 |
|
||||
| **UA-2** | One-way, 1 adult, Other/USD, INTERNATIONAL Regular, WALLET | OTHER → displayCurrency USD | Chain equal in USD; assert USD 2dp; verify INTL 2× surcharge NOT silently dropped on seat-class base path | H-3/H-4, matrix B3 |
|
||||
| **UA-1b** | One-way, 1 adult, **Other/USD** — assert card `displayAmountMinor` vs internal `baseFareMinor` | nationality OTHER → USD; INTL class | ✅ FIXED — the card shows the USD fare (`displayAmountMinor`), the internal `baseFareMinor` is the ETB source it was converted from (rate apart, coherent). The portal now carries the USD value forward (`results/page.tsx` on-select uses `displayAmountMinor`, aligning with the already-USD seats-page logic). | div #1; H-3/H-4 |
|
||||
| **UA-2** | One-way, 1 adult, Other/USD, INTERNATIONAL Regular, WALLET | OTHER → displayCurrency USD | ✅ FIXED — money chain COHERENT: `displayCurrency=USD`/`displayTotalMinor` = what the passenger saw (=`reviewedTotalMinor`); `currency=ETB`/`totalMinor` = the ETB charge basis (=`displayTotalMinor × rate`). The prior `currency:USD`-on-an-ETB-amount mislabel is gone. | H-3/H-4, matrix B3 |
|
||||
| **UA-3** | One-way, 1 adult, **Djiboutian/DJF**, **forged-telebirr** | DJIBOUTIAN → DJF; gateway path | **DJF displayed 2dp (`DJF x.yy`) but charged whole-franc** — assert DOM-2dp vs settled `amount_minor` (0-decimal). Must be a **gateway** row (WALLET bypasses the charge conversion). | **L-1** ✅, matrix C6/K3 |
|
||||
| **UA-3w** | One-way, 1 adult, Djiboutian/DJF, WALLET (shape-only) | DJF, WALLET | Assert only the DOM-2dp vs `booking-amount`-major **shape** mismatch (no settle-side rounding on WALLET). | L-1 (partial) |
|
||||
| **UA-4** | One-way, **1A + 1 child ≤5yr (free)**, ETB, WALLET | childCount 1, DOB<5yr | Child shows "CHILD - FREE"; free child excluded from total; `fare-breakdown.isFree==true` agrees with client `fare-utils.isFirstChild` | **H-12**, matrix B6 |
|
||||
| **UA-5** | One-way, **1A + 2 children** (first free, second paid), ETB, WALLET | childCount 2 | Second child paid; client reduce (review:587) == breakdown sum; assert booking vs quote free-child count agree (quote uses min(child,adult); booking uses child-1) | **H-12**, matrix B6 |
|
||||
| **UA-6** | Round-trip, 1 adult, Economy Regular, ETB, WALLET | ROUND_TRIP, outbound+inbound holds | Total == outbound+inbound; per-leg split `Math.round(fareMinor/2)` (review:481) reconciles to `outbound/inboundSeatFareMinor`; both `holdId` + `returnHoldId` present | div #6; matrix A3 |
|
||||
| **UA-6** | Round-trip, 1 adult, Economy Regular, ETB, WALLET | ROUND_TRIP, outbound+inbound holds | ✅ FIXED — the fare engine now prices the reverse (C→A) leg by absolute distance (was: threw "origin must come before destination", leaving the inbound leg with seats but no priced coach → unbookable). Full two-leg flow completes: 2 seats (one per leg), total = 2× the one-way fare. | div #6; matrix A3 |
|
||||
| **UA-7** | Round-trip, 2 adults, **Economy Bed / berth**, INTERNATIONAL/USD, WALLET | bed seat-class; berth seats (`bedPosition`) | Berth priced as separate class; `getSeatFare` bedPosition match (seats:433) == breakdown; INTL berth surcharge consistent | requires **berth seed** (§5); matrix B3 |
|
||||
| **UA-8** | One-way, 1 adult, ETB, **valid % promo via `?promoCode=`**, WALLET | valid `percentOff:10` in URL | ✅ FIXED — the browser still sends the undiscounted `reviewedTotalMinor`, but the authed `bookings.service` recomputes the authoritative fare and applies the promo, so `Booking.totalMinor` = `subtotal − discount`. | H-13 fixed & guarded; matrix D |
|
||||
| **UA-11** | One-way, 1 adult, ETB, **expired promo** (validUntil past, active:true) via URL, WALLET | expired code | Promo rejected/ignored; total unaffected; UI shows no discount (consistent with UA-8 drop). | matrix D5 |
|
||||
@@ -142,7 +142,7 @@ Each row: change made in backoffice UI (:5184) → API write → portal read (:5
|
||||
- **BC-7** ✅ FIXED: `PATCH /seat-classes` with `basePrice:-500` is now rejected with **400** — `CreateSeatClassDto.basePrice` (and `insuranceFeeMinor`) carry `@Min(0)`, applied to updates via `PartialType`. **M-1**, matrix H1/H2.
|
||||
- **BC-8** ✅ FIXED: `POST /promos` with `percentOff:200` is now rejected with **400** — `CreatePromotionDto.percentOff` carries `@Min(0) @Max(100)` (and `amountOffMinor` `@Min(0)`). A valid ≤100% promo still succeeds. **M-2**, matrix H4.
|
||||
- **BC-9** ✅ FIXED: `PATCH /config {seat_hold_duration_minutes:"-1"}` is now rejected with **400** — a whitelisted `UpdateSystemConfigDto` coerces each known key to a positive integer (`seat_hold_duration_minutes` bounded 1..60). A sane value still stores. **M-3**, matrix H7.
|
||||
- **BC-10**: `/schedules` → past `departureAt`. Client blocks only `arrival<=departure`, no past-date block. Assert schedule created in the past. **M-4**, matrix H3.
|
||||
- **BC-10** ✅ FIXED: `POST /schedules` with a past `departureAt` is now rejected with **400** — `schedules.service.createSchedule` guards `departureAt >= now` alongside the existing `arrival > departure` check. A future schedule still creates. **M-4**, matrix H3.
|
||||
- **BC-11** ✅ FIXED: `PUT/PATCH /fare-engine/exchange-rates` now carry `@PassengerAdmin()` (as DELETE already did). Anon → 401, regular passenger → **403 forbidden**, staff admin → 200. **C-8**, matrix J1.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user