mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 12:25:02 +00:00
fix: ( bookings ) report the full amount paid after a reschedule
This commit is contained in:
@@ -7,6 +7,7 @@ import { TicketsService } from '../tickets/tickets.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { CreateBookingDto } from './bookings.dto';
|
||||
import { assertIdentitiesNotAlreadyBooked, resolveIdentityRef } from './booking-identity.util';
|
||||
import { buildPaymentBreakdown } from './payment-breakdown.util';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { VerifaydaService } from '../verifayda/verifayda.service';
|
||||
import { CurrencyService } from '../currency/currency.service';
|
||||
@@ -2037,6 +2038,8 @@ export class BookingsService {
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
|
||||
seats: { include: { seat: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } } } },
|
||||
paymentIntent: true, tickets: true,
|
||||
// Money collected after the original payment (reschedule fees, underpayments).
|
||||
supplementaryCharges: { orderBy: { createdAt: 'asc' } },
|
||||
priceTier: { select: { priceMinor: true } },
|
||||
},
|
||||
});
|
||||
@@ -2130,6 +2133,8 @@ export class BookingsService {
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
|
||||
seats: { include: { seat: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } } } },
|
||||
paymentIntent: true, tickets: true,
|
||||
// Money collected after the original payment (reschedule fees, underpayments).
|
||||
supplementaryCharges: { orderBy: { createdAt: 'asc' } },
|
||||
priceTier: { select: { priceMinor: true } },
|
||||
},
|
||||
});
|
||||
@@ -2208,12 +2213,20 @@ export class BookingsService {
|
||||
},
|
||||
};
|
||||
}),
|
||||
// `amountMinor` stays exactly as it was — the original intent, in the major units that
|
||||
// column actually stores — so existing callers keep working. Everything collected since
|
||||
// (reschedule fees and fare differences) lives in `breakdown`, whose `totalPaidMinor` is
|
||||
// the number to show as "Total paid". See payment-breakdown.util.ts.
|
||||
payment: (booking as any).paymentIntent
|
||||
? {
|
||||
method: (booking as any).paymentIntent.method,
|
||||
status: (booking as any).paymentIntent.status,
|
||||
amountMinor: (booking as any).paymentIntent.amountMinor,
|
||||
currency: (booking as any).paymentIntent.currency,
|
||||
breakdown: buildPaymentBreakdown(
|
||||
(booking as any).paymentIntent,
|
||||
(booking as any).supplementaryCharges ?? [],
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
// One ticket per passenger per leg (round trips have a separate ticket — and
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
export type PaymentLineKind = 'BOOKING' | 'SUPPLEMENTARY';
|
||||
|
||||
export interface PaymentLine {
|
||||
kind: PaymentLineKind;
|
||||
/** Human label — "Original booking", "Reschedule", "Excess baggage". */
|
||||
label: string;
|
||||
/** Raw reason for supplementary lines (RESCHEDULE, UNDERPAYMENT, …); null for the booking line. */
|
||||
reason: string | null;
|
||||
/** Always true minor units, whatever the source column stored. */
|
||||
amountMinor: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
paidAt: string | null;
|
||||
/** Whether this line represents money actually collected. */
|
||||
settled: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentBreakdown {
|
||||
lines: PaymentLine[];
|
||||
/** Sum of settled lines, or null when they are not all in one currency. */
|
||||
totalPaidMinor: number | null;
|
||||
totalPaidCurrency: string | null;
|
||||
/** True when something is still owed (a charge raised but not yet paid). */
|
||||
hasOutstanding: boolean;
|
||||
outstandingMinor: number;
|
||||
}
|
||||
|
||||
/** `PaymentIntent.amountMinor` is a Float in major units — bring it onto the minor-unit scale. */
|
||||
export function intentAmountToMinor(amount: number | null | undefined): number {
|
||||
if (amount == null) return 0;
|
||||
return Math.round(amount * 100);
|
||||
}
|
||||
|
||||
const SUPPLEMENTARY_LABELS: Record<string, string> = {
|
||||
RESCHEDULE: 'Reschedule',
|
||||
UNDERPAYMENT: 'Underpayment',
|
||||
FARE_CORRECTION: 'Fare correction',
|
||||
EXCESS_BAGGAGE: 'Excess baggage',
|
||||
};
|
||||
|
||||
function labelFor(reason: string): string {
|
||||
return (
|
||||
SUPPLEMENTARY_LABELS[reason] ??
|
||||
// "SOME_OTHER_REASON" → "Some other reason"
|
||||
reason.charAt(0).toUpperCase() + reason.slice(1).toLowerCase().replace(/_/g, ' ')
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPaymentBreakdown(
|
||||
paymentIntent: { status?: string | null; amountMinor?: number | null; currency?: string | null } | null | undefined,
|
||||
supplementaryCharges: Array<{
|
||||
reason: string;
|
||||
amountMinor: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
paidAt: Date | string | null;
|
||||
}> = [],
|
||||
): PaymentBreakdown {
|
||||
const lines: PaymentLine[] = [];
|
||||
|
||||
if (paymentIntent) {
|
||||
lines.push({
|
||||
kind: 'BOOKING',
|
||||
label: 'Original booking',
|
||||
reason: null,
|
||||
amountMinor: intentAmountToMinor(paymentIntent.amountMinor),
|
||||
currency: paymentIntent.currency ?? 'ETB',
|
||||
status: paymentIntent.status ?? 'UNKNOWN',
|
||||
paidAt: null,
|
||||
settled: paymentIntent.status === 'SUCCEEDED',
|
||||
});
|
||||
}
|
||||
|
||||
for (const charge of supplementaryCharges) {
|
||||
if (charge.status === 'WAIVED' || charge.status === 'EXPIRED') continue;
|
||||
lines.push({
|
||||
kind: 'SUPPLEMENTARY',
|
||||
label: labelFor(charge.reason),
|
||||
reason: charge.reason,
|
||||
amountMinor: charge.amountMinor,
|
||||
currency: charge.currency ?? 'ETB',
|
||||
status: charge.status,
|
||||
paidAt: charge.paidAt ? new Date(charge.paidAt).toISOString() : null,
|
||||
settled: charge.status === 'PAID',
|
||||
});
|
||||
}
|
||||
|
||||
const settled = lines.filter((l) => l.settled);
|
||||
const currencies = new Set(settled.map((l) => l.currency));
|
||||
const singleCurrency = currencies.size === 1 ? [...currencies][0] : null;
|
||||
|
||||
const outstanding = lines.filter((l) => l.status === 'PENDING');
|
||||
|
||||
return {
|
||||
lines,
|
||||
totalPaidMinor: singleCurrency ? settled.reduce((sum, l) => sum + l.amountMinor, 0) : null,
|
||||
totalPaidCurrency: singleCurrency,
|
||||
hasOutstanding: outstanding.length > 0,
|
||||
outstandingMinor: outstanding.reduce((sum, l) => sum + l.amountMinor, 0),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user