From 296e3af7ddb5e2f973b8d1fd98b51929b31d8a1b Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Mon, 20 Jul 2026 23:08:32 +0300 Subject: [PATCH] Fix package price --- .../src/modules/payments/payments.service.ts | 36 +++++++++------ .../src/modules/reports/reports.service.ts | 9 ++-- .../backoffice/src/app/routes/page.tsx | 18 ++++---- .../backoffice/src/app/schedules/page.tsx | 3 ++ .../src/app/booking/confirmation/page.tsx | 6 ++- .../portal/src/app/booking/review/page.tsx | 5 ++- .../portal/src/lib/generate-voucher.ts | 45 ++++++++++++++----- 7 files changed, 81 insertions(+), 41 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index e57d94643..ac8c7ef66 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -162,29 +162,39 @@ export class PaymentsService { } /** - * Returns the correct totalMinor for a booking, accounting for package round-trip bookings - * where totalMinor may have been stored as a single-leg amount before the server fix. - * A package round-trip booking has packageId set, bookingType ROUND_TRIP, and - * totalMinor equal to a single-leg fare (i.e. seats split evenly across 2 legs). + * Returns the correct totalMinor (in ETB) for a booking, accounting for package round-trip + * bookings where totalMinor may have been stored as a single-leg amount before the server fix. */ - private async resolveBookingTotal(booking: { id: string; totalMinor: number; bookingType: string; packageId?: string | null; priceTierId?: string | null }): Promise { + private async resolveBookingTotal(booking: { + id: string; + totalMinor: number; + bookingType: string; + packageId?: string | null; + priceTierId?: string | null; + displayTotalMinor?: number | null; + }): Promise { if (!booking.packageId || !booking.priceTierId || booking.bookingType !== 'ROUND_TRIP') { return booking.totalMinor; } - // For package round-trip bookings, recompute from the tier price to handle - // bookings created before the server fix stored the full round-trip total. + // New bookings store displayTotalMinor from the frontend's reviewedTotalMinor; their + // totalMinor was already computed in ETB at creation time — no recomputation needed. + if (booking.displayTotalMinor != null && booking.displayTotalMinor > 0) { + return booking.totalMinor; + } + // Legacy path: old bookings may have stored a single-leg totalMinor — recompute from tier. const tier = await this.prisma.packagePriceTier.findUnique({ where: { id: booking.priceTierId } }); if (!tier) return booking.totalMinor; - // Count adults and children from booking seats const seats = await this.prisma.bookingSeat.findMany({ where: { bookingId: booking.id, leg: 1 }, select: { passengerCategory: true } }); const adultCount = seats.filter(s => s.passengerCategory === 'ADULT').length || 1; const childCount = seats.filter(s => s.passengerCategory === 'CHILD').length; - const adultFareMinor = tier.priceMinor * 2; // round-trip = 2 legs + // tier.priceMinor may be in a non-ETB currency — convert to ETB so the result is + // always in the same units as totalMinor (which is always the ETB canonical). + const rawFare = tier.priceMinor * 2; + const adultFareMinor = tier.currency && (tier.currency as string) !== 'ETB' + ? await this.currencyService.convertAmount(rawFare, tier.currency as any, 'ETB' as any) + : rawFare; const childFareMinor = Math.round(adultFareMinor * 0.1); - const correctTotal = adultCount * adultFareMinor + childCount * childFareMinor; - // If stored total already matches the correct round-trip total, use it as-is. - // If it's roughly half (single-leg), use the recomputed value. - return correctTotal; + return adultCount * adultFareMinor + childCount * childFareMinor; } async initiatePayment( diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 9067ffe56..078748929 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -684,10 +684,11 @@ export class ReportsService { const paidMinor = pi.amountMinor; const paidCurrency = pi.currency; - // b.totalMinor is always in ETB. Convert the paid amount to ETB for an - // apples-to-apples comparison regardless of which currency was used at checkout. + // b.totalMinor is always in ETB minor. pi.amountMinor is the charge MAJOR amount + // (the gateway receives major units — displayMinorToChargeMajor divides by 100 before + // sending). Multiply by 100 to convert back to minor before the ETB comparison. const owedEtb = b.totalMinor; - const paidEtb = toEtbMinor(paidMinor, paidCurrency); + const paidEtb = toEtbMinor(paidMinor * 100, paidCurrency); const balanceMinor = owedEtb - paidEtb; const balanceCurrency = 'ETB'; @@ -795,7 +796,7 @@ export class ReportsService { const paidCurrency = pi?.currency ?? b.currency; const owedEtb = b.totalMinor; - const paidEtb = toEtbMinor(paidMinor, paidCurrency); + const paidEtb = toEtbMinor(paidMinor * 100, paidCurrency); const balanceMinor = owedEtb - paidEtb; const balanceCurrency = 'ETB'; diff --git a/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx b/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx index 5fa655a5e..52b875433 100644 --- a/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx @@ -787,14 +787,6 @@ export default function RoutesPage() { title="Check-in cutoff override (minutes) for this stop" /> -
- updateStop(index, 'plannedDepartureTime', v)} - placeholder="Dep time" - label="Planned Departure" - /> -
+
+ updateStop(index, 'plannedDepartureTime', v)} + placeholder="Dep time" + label="Planned Departure" + /> +