mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -294,8 +294,12 @@ export class GuestBookingService {
|
|||||||
|
|
||||||
let displayTotalMinor: number;
|
let displayTotalMinor: number;
|
||||||
let resolvedTotalMinor: 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) {
|
if (allFaresProvided && !isPackageOneway) {
|
||||||
displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0);
|
displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0);
|
||||||
|
usedClientSubtotal = true;
|
||||||
} else if (isPackageOneway && dto.reviewedTotalMinor != null) {
|
} else if (isPackageOneway && dto.reviewedTotalMinor != null) {
|
||||||
displayTotalMinor = dto.reviewedTotalMinor;
|
displayTotalMinor = dto.reviewedTotalMinor;
|
||||||
if (seatedPassengers.length > 0) {
|
if (seatedPassengers.length > 0) {
|
||||||
@@ -306,6 +310,7 @@ export class GuestBookingService {
|
|||||||
}
|
}
|
||||||
} else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
|
} else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
|
||||||
displayTotalMinor = dto.reviewedTotalMinor;
|
displayTotalMinor = dto.reviewedTotalMinor;
|
||||||
|
usedClientSubtotal = true;
|
||||||
} else {
|
} else {
|
||||||
// fare engine returns ETB — convert forward to display currency
|
// fare engine returns ETB — convert forward to display currency
|
||||||
const etbTotal = Math.max(0, totalBaseFareMinor - discountMinor);
|
const etbTotal = Math.max(0, totalBaseFareMinor - discountMinor);
|
||||||
@@ -313,6 +318,18 @@ export class GuestBookingService {
|
|||||||
? await this.currencyService.convertAmount(etbTotal, Currency.ETB, displayCurrency)
|
? await this.currencyService.convertAmount(etbTotal, Currency.ETB, displayCurrency)
|
||||||
: etbTotal;
|
: 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
|
resolvedTotalMinor = displayCurrency !== Currency.ETB
|
||||||
? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
|
? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
|
||||||
: displayTotalMinor;
|
: displayTotalMinor;
|
||||||
@@ -834,17 +851,16 @@ export class GuestBookingService {
|
|||||||
const allRTFaresProvided = rtSeatedPassengers.length > 0 &&
|
const allRTFaresProvided = rtSeatedPassengers.length > 0 &&
|
||||||
rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null);
|
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) {
|
if (allRTFaresProvided && !isPackageRoundTrip) {
|
||||||
// Server-computed sum is authoritative — prevents race-condition under-count.
|
// Server-computed sum is authoritative — prevents race-condition under-count.
|
||||||
displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0);
|
displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0);
|
||||||
totalMinor = displayCurrency !== Currency.ETB
|
usedClientSubtotal = true;
|
||||||
? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
|
|
||||||
: displayTotalMinor;
|
|
||||||
} else if (isPackageRoundTrip && dto.reviewedTotalMinor != null) {
|
} else if (isPackageRoundTrip && dto.reviewedTotalMinor != null) {
|
||||||
displayTotalMinor = dto.reviewedTotalMinor;
|
displayTotalMinor = dto.reviewedTotalMinor;
|
||||||
totalMinor = displayCurrency !== Currency.ETB
|
|
||||||
? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
|
|
||||||
: displayTotalMinor;
|
|
||||||
const seatedCount = passengersData.filter(p => p.seatId).length;
|
const seatedCount = passengersData.filter(p => p.seatId).length;
|
||||||
if (seatedCount > 0) {
|
if (seatedCount > 0) {
|
||||||
const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2));
|
const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2));
|
||||||
@@ -855,11 +871,24 @@ export class GuestBookingService {
|
|||||||
}
|
}
|
||||||
} else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
|
} else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
|
||||||
displayTotalMinor = dto.reviewedTotalMinor;
|
displayTotalMinor = dto.reviewedTotalMinor;
|
||||||
totalMinor = displayCurrency !== Currency.ETB
|
usedClientSubtotal = true;
|
||||||
? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
|
|
||||||
: displayTotalMinor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// C-1 guard: never charge less than the server-recomputed authoritative round-trip fare.
|
||||||
this.assertTotalNotUnderAuthoritative(totalMinor, authoritativeTotalMinor, 'createGuestRoundTripBooking');
|
this.assertTotalNotUnderAuthoritative(totalMinor, authoritativeTotalMinor, 'createGuestRoundTripBooking');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user