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 = { 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), }; }