Package payment amount fixes

This commit is contained in:
Stephanos A
2026-07-06 07:37:00 +03:00
parent dd4f2dcba9
commit 76bf81eec3
4 changed files with 170 additions and 52 deletions

View File

@@ -89,7 +89,19 @@ export class PaymentsService {
const [items, total] = await Promise.all([
this.prisma.paymentIntent.findMany({
where,
include: { booking: true },
include: {
booking: {
select: {
bookingRef: true,
bookingType: true,
packageId: true,
priceTierId: true,
adultCount: true,
childCount: true,
priceTier: { select: { priceMinor: true } },
},
},
},
skip,
take: pageSize,
orderBy: { createdAt: "desc" },
@@ -98,24 +110,65 @@ export class PaymentsService {
]);
return {
items: items.map((item) => ({
id: item.id,
reference: item.id.substring(0, 8),
bookingId: item.bookingId,
booking: { bookingRef: item.booking?.bookingRef },
amountMinor: item.amountMinor,
currency: item.currency,
method: item.method,
status: item.status,
createdAt: item.createdAt,
paidAt: item.paidAt,
})),
items: items.map((item) => {
const b = item.booking as any;
// For package round-trip bookings the stored amountMinor may be the single-leg
// amount. Recompute from the tier price when applicable.
let amountMinor = item.amountMinor;
if (b?.packageId && b?.bookingType === 'ROUND_TRIP' && b?.priceTier?.priceMinor) {
const adultFare = b.priceTier.priceMinor * 2;
const childFare = Math.round(adultFare * 0.1);
const correctMinor = (b.adultCount || 1) * adultFare + (b.childCount || 0) * childFare;
// Convert to the charge currency ratio: stored amountMinor is in charge currency
// (may be DJF/USD), but correctMinor is in ETB minor. Only override when the
// currency is ETB (most common case); for foreign currencies keep stored value.
if (item.currency === 'ETB') amountMinor = correctMinor;
}
return {
id: item.id,
reference: item.id.substring(0, 8),
bookingId: item.bookingId,
booking: { bookingRef: b?.bookingRef },
amountMinor,
currency: item.currency,
method: item.method,
status: item.status,
createdAt: item.createdAt,
paidAt: item.paidAt,
};
}),
total,
page,
pageSize,
};
}
/**
* 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).
*/
private async resolveBookingTotal(booking: { id: string; totalMinor: number; bookingType: string; packageId?: string | null; priceTierId?: string | null }): Promise<number> {
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.
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
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;
}
async initiatePayment(dto: InitiatePaymentDto): Promise<InitiateResponseDto> {
const booking = await this.prisma.booking.findUnique({
where: { id: dto.bookingId },
@@ -127,6 +180,16 @@ export class PaymentsService {
}
const method = dto.method as PaymentMethodType;
const correctTotalMinor = await this.resolveBookingTotal(booking as any);
// Patch the DB if the stored total is wrong (single-leg for a round-trip package booking)
if (correctTotalMinor !== booking.totalMinor) {
await this.prisma.booking.update({
where: { id: booking.id },
data: { totalMinor: correctTotalMinor },
});
(booking as any).totalMinor = correctTotalMinor;
}
// WALLET is an internal balance debit — it never leaves this app.
if (method === PaymentMethodType.WALLET) {
@@ -508,12 +571,13 @@ export class PaymentsService {
): Promise<{ booking_id: string; currency: string; amount: number }> {
const booking = await this.prisma.booking.findUnique({
where: { id: bookingId },
select: { id: true, totalMinor: true },
select: { id: true, totalMinor: true, bookingType: true, packageId: true, priceTierId: true },
});
if (!booking) throw new NotFoundException('Booking not found');
const correctTotalMinor = await this.resolveBookingTotal(booking as any);
const requestedCurrency = currency.toUpperCase();
const amountInETB = booking.totalMinor / 100;
const amountInETB = correctTotalMinor / 100;
if (requestedCurrency === 'ETB') {
return { booking_id: bookingId, currency: 'ETB', amount: amountInETB };