Package passenger numbers and pricing updates

This commit is contained in:
Stephanos A
2026-07-05 18:14:42 +03:00
parent 9e77ca7865
commit afd30c36a0
20 changed files with 537 additions and 105 deletions

View File

@@ -341,8 +341,9 @@ export class BookingsService {
totalMinor: b.totalMinor, currency: b.currency || 'ETB',
displayCurrency: b.displayCurrency, displayTotalMinor: b.displayTotalMinor,
contactEmail: b.contactEmail, contactPhone: b.contactPhone,
bookingType: 'PACKAGE', packageId: b.packageId, isPackageBooking: true,
packageName: b.package?.name, packageCode: b.package?.code,
bookingType: 'PACKAGE', packageId: b.packageId, priceTierId: b.priceTierId,
isPackageBooking: true, packageName: b.package?.name, packageCode: b.package?.code,
tierLabel: b.priceTier?.label ?? null,
returnLegStatus: null, adultCount: b.passengerCount, childCount: 0,
createdAt: b.createdAt, passenger: null,
passengerNames: b.passengers?.map((p: any) => p.passengerName) ?? [],
@@ -372,6 +373,8 @@ export class BookingsService {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
paymentIntent: true,
seats: { include: { seat: true } },
package: { select: { id: true, name: true, code: true } },
priceTier: { select: { id: true, label: true } },
},
}),
this.prisma.booking.count({ where }),
@@ -415,6 +418,10 @@ export class BookingsService {
contactPhone: booking.contactPhone,
bookingType: booking.bookingType,
packageId: booking.packageId ?? null,
priceTierId: (booking as any).priceTierId ?? null,
packageName: (booking as any).package?.name ?? null,
packageCode: (booking as any).package?.code ?? null,
tierLabel: (booking as any).priceTier?.label ?? null,
isPackageBooking: !!booking.packageId,
returnLegStatus: (booking as any).returnLegStatus ?? null,
adultCount: booking.adultCount,
@@ -446,9 +453,11 @@ export class BookingsService {
contactPhone: b.contactPhone,
bookingType: 'PACKAGE',
packageId: b.packageId,
priceTierId: b.priceTierId,
isPackageBooking: true,
packageName: b.package?.name,
packageCode: b.package?.code,
tierLabel: b.priceTier?.label ?? null,
returnLegStatus: null,
adultCount: b.passengerCount,
childCount: 0,
@@ -518,20 +527,18 @@ export class BookingsService {
displayTotalMinor = await this.currencyService.convertAmount(fareCalculation.totalMinor, Currency.ETB, displayCurrency);
}
// Track which child gets free fare (first child encountered)
// Track per-seat fare. For package bookings children pay 10% of adult fare;
// for regular bookings the first child is free.
let freeChildUsed = false;
const passengersWithFares = passengersData.map(p => {
let fareMinor: number;
if (p.category === PassengerCategory.ADULT) {
fareMinor = fareCalculation.baseFareMinor;
} else if (dto.packageId) {
fareMinor = Math.round(fareCalculation.baseFareMinor * 0.1);
} else {
// Child: first child is free, subsequent children pay full fare
if (!freeChildUsed) {
fareMinor = 0;
freeChildUsed = true;
} else {
fareMinor = fareCalculation.baseFareMinor;
}
if (!freeChildUsed) { fareMinor = 0; freeChildUsed = true; }
else fareMinor = fareCalculation.baseFareMinor;
}
return { ...p, fareMinor };
});
@@ -661,34 +668,27 @@ export class BookingsService {
displayTotalMinor = await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency);
}
// Track which child gets free fare for outbound and return legs
// Track per-seat fare. For package bookings children pay 10% of adult fare;
// for regular bookings the first child is free per leg.
let outboundFreeChildUsed = false;
let returnFreeChildUsed = false;
const passengersWithFares = passengersData.map(p => {
let outboundFareMinor: number;
let returnFareMinor: number;
if (p.category === PassengerCategory.ADULT) {
outboundFareMinor = outboundFare.baseFareMinor;
returnFareMinor = returnFare.baseFareMinor;
} else if (dto.packageId) {
outboundFareMinor = Math.round(outboundFare.baseFareMinor * 0.1);
returnFareMinor = Math.round(returnFare.baseFareMinor * 0.1);
} else {
// Child fare for outbound
if (!outboundFreeChildUsed) {
outboundFareMinor = 0;
outboundFreeChildUsed = true;
} else {
outboundFareMinor = outboundFare.baseFareMinor;
}
// Child fare for return
if (!returnFreeChildUsed) {
returnFareMinor = 0;
returnFreeChildUsed = true;
} else {
returnFareMinor = returnFare.baseFareMinor;
}
if (!outboundFreeChildUsed) { outboundFareMinor = 0; outboundFreeChildUsed = true; }
else outboundFareMinor = outboundFare.baseFareMinor;
if (!returnFreeChildUsed) { returnFareMinor = 0; returnFreeChildUsed = true; }
else returnFareMinor = returnFare.baseFareMinor;
}
return { ...p, outboundFareMinor, returnFareMinor };
});
@@ -1231,16 +1231,20 @@ export class BookingsService {
childCount: number,
) {
const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: priceTierId } });
const passengerCount = adultCount + childCount;
const totalBaseFareMinor = tier.priceMinor * passengerCount;
// For round-trip packages the caller splits the tier price across legs, so
// priceMinor here is already the per-leg amount. Children pay 10% of adult fare.
const childFareMinor = Math.round(tier.priceMinor * 0.1);
const adultFareMinor = tier.priceMinor * adultCount;
const childTotalMinor = childFareMinor * childCount;
const totalBaseFareMinor = adultFareMinor + childTotalMinor;
return {
baseFareMinor: tier.priceMinor,
adultCount,
adultFareMinor: tier.priceMinor * adultCount,
adultFareMinor,
childCount,
freeChildrenCount: 0,
paidChildrenCount: childCount,
childFareMinor: tier.priceMinor * childCount,
childFareMinor: childTotalMinor,
totalBaseFareMinor,
discountMinor: 0,
loyaltyRedemptionMinor: 0,