From acd4b37d598a5dcdb83b1bf8d45672d21bc17aec Mon Sep 17 00:00:00 2001 From: Muluhabt Date: Tue, 28 Jul 2026 09:42:27 +0300 Subject: [PATCH] fix(bookings): apply promo discount to client-subtotal totals in guest/authenticated booking createGuestBooking (ONE_WAY) and createGuestRoundTripBooking overwrote the authoritative discounted total with an undiscounted client-provided subtotal (per-seat sum or reviewedTotalMinor) whenever one was present, silently dropping a valid promos discount at booking-creation time, even though the same promo was correctly applied in the fare-breakdown/quote step. Only the no-client-subtotal fallback branch subtracted discountMinor. Mirrors the existing usedClientSubtotal fix already present in BookingsService.createOneWayBooking (H-13): subtract discountMinor from the client subtotal before storing it, in ETB and display currency. TRANSIT and ROUND_TRIP_TRANSIT were unaffected since neither overrides with a client subtotal. Co-Authored-By: Claude Sonnet 5 --- .../modules/bookings/guest-booking.service.ts | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 238d58d82..c8061184f 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -294,8 +294,12 @@ export class GuestBookingService { let displayTotalMinor: number; let resolvedTotalMinor: number; + // True when the total came from a client-summed subtotal (per-seat sum or reviewedTotalMinor), + // which the portal computes UNDISCOUNTED — the promo must still be applied to it (H-13). + let usedClientSubtotal = false; if (allFaresProvided && !isPackageOneway) { displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0); + usedClientSubtotal = true; } else if (isPackageOneway && dto.reviewedTotalMinor != null) { displayTotalMinor = dto.reviewedTotalMinor; if (seatedPassengers.length > 0) { @@ -306,6 +310,7 @@ export class GuestBookingService { } } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { displayTotalMinor = dto.reviewedTotalMinor; + usedClientSubtotal = true; } else { // fare engine returns ETB — convert forward to display currency const etbTotal = Math.max(0, totalBaseFareMinor - discountMinor); @@ -313,6 +318,18 @@ export class GuestBookingService { ? await this.currencyService.convertAmount(etbTotal, Currency.ETB, displayCurrency) : etbTotal; } + + // H-13: the portal sums UNDISCOUNTED per-passenger fares into a client subtotal, silently + // dropping the promo the fare engine recognized. Apply the authoritative discount now so the + // customer is charged the discounted price. The non-client-subtotal branch above already + // nets the discount out of etbTotal, so it's excluded here to avoid double-subtracting. + if (usedClientSubtotal && discountMinor > 0) { + const discountDisplayMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(discountMinor, Currency.ETB, displayCurrency) + : discountMinor; + displayTotalMinor = Math.max(0, displayTotalMinor - discountDisplayMinor); + } + resolvedTotalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) : displayTotalMinor; @@ -834,17 +851,16 @@ export class GuestBookingService { const allRTFaresProvided = rtSeatedPassengers.length > 0 && rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null); + // True when the total came from a client-summed subtotal (per-seat sum or reviewedTotalMinor), + // which the portal computes UNDISCOUNTED — the promo must still be applied to it (H-13). + let usedClientSubtotal = false; + if (allRTFaresProvided && !isPackageRoundTrip) { // Server-computed sum is authoritative — prevents race-condition under-count. displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0); - totalMinor = displayCurrency !== Currency.ETB - ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) - : displayTotalMinor; + usedClientSubtotal = true; } else if (isPackageRoundTrip && dto.reviewedTotalMinor != null) { displayTotalMinor = dto.reviewedTotalMinor; - totalMinor = displayCurrency !== Currency.ETB - ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) - : displayTotalMinor; const seatedCount = passengersData.filter(p => p.seatId).length; if (seatedCount > 0) { const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2)); @@ -855,11 +871,24 @@ export class GuestBookingService { } } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { displayTotalMinor = dto.reviewedTotalMinor; - totalMinor = displayCurrency !== Currency.ETB - ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) - : displayTotalMinor; + usedClientSubtotal = true; } + // H-13: the portal sums UNDISCOUNTED per-passenger fares into a client subtotal, silently + // dropping the promo the fare engine recognized. Apply the authoritative discount now so the + // customer is charged the discounted price. The no-override case above already starts from a + // discounted displayTotalMinor, so it's excluded here to avoid double-subtracting. + if (usedClientSubtotal && discountMinor > 0) { + const discountDisplayMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(discountMinor, Currency.ETB, displayCurrency) + : discountMinor; + displayTotalMinor = Math.max(0, displayTotalMinor - discountDisplayMinor); + } + + totalMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) + : displayTotalMinor; + // C-1 guard: never charge less than the server-recomputed authoritative round-trip fare. this.assertTotalNotUnderAuthoritative(totalMinor, authoritativeTotalMinor, 'createGuestRoundTripBooking');