Payment report and missing ticket generation updates

This commit is contained in:
Stephanos A
2026-07-23 07:27:05 +03:00
parent 614fab806d
commit e2bdc21c95
2 changed files with 48 additions and 22 deletions

View File

@@ -847,6 +847,20 @@ export class ReportsService {
}
async getPaymentsReport(scheduleId: string) {
const rateRows = await this.prisma.currencyExchangeRate.findMany({
where: { toCurrency: 'ETB' as any },
orderBy: { effectiveDate: 'desc' },
});
const rateToEtb = new Map<string, number>();
for (const r of rateRows) {
if (!rateToEtb.has(r.fromCurrency)) rateToEtb.set(r.fromCurrency, Number(r.rate));
}
const toEtbMinor = (minor: number, currency: string): number => {
if (currency === 'ETB') return minor;
const rate = rateToEtb.get(currency);
return rate ? Math.round(minor * rate) : minor;
};
const bookings = await this.prisma.booking.findMany({
where: {
scheduleId,
@@ -860,6 +874,8 @@ export class ReportsService {
select: {
passengerName: true,
fareMinor: true,
displayFareMinor: true,
displayCurrency: true,
passengerCategory: true,
seatLabelSnapshot: true,
seat: { select: { coach: { select: { number: true, coachType: { select: { name: true } } } } } },
@@ -870,30 +886,38 @@ export class ReportsService {
});
const rows = bookings.map(b => {
const actualMinor = b.seats.reduce((s, seat) => s + (seat.fareMinor ?? 0), 0);
const paidMinor = Math.round(b.paymentIntent!.amountMinor);
const pi = b.paymentIntent!;
const actualMinor = b.displayTotalMinor ?? b.totalMinor;
const actualCurrency = (b.displayCurrency as string | null) ?? b.currency;
// pi.amountMinor is stored in major units — convert to minor
const paidMinor = Math.round(pi.amountMinor * 100);
const paidCurrency = pi.currency;
const varianceMinor = toEtbMinor(actualMinor, actualCurrency) - toEtbMinor(paidMinor, paidCurrency);
return {
bookingRef: b.bookingRef,
passengerName: b.seats[0]?.passengerName ?? b.passenger?.user?.fullName ?? '—',
phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
method: b.paymentIntent!.method,
paidAt: b.paymentIntent!.paidAt,
method: pi.method,
paidAt: pi.paidAt,
actualMinor,
actualCurrency,
paidMinor,
currency: 'ETB',
paidCurrency,
varianceMinor,
passengerCount: b.seats.length,
};
});
const totalActualMinor = rows.reduce((s, r) => s + r.actualMinor, 0);
const totalPaidMinor = rows.reduce((s, r) => s + r.paidMinor, 0);
const totalActualEtbMinor = rows.reduce((s, r) => s + toEtbMinor(r.actualMinor, r.actualCurrency), 0);
const totalPaidEtbMinor = rows.reduce((s, r) => s + toEtbMinor(r.paidMinor, r.paidCurrency), 0);
const byMethod = rows.reduce((acc, r) => {
acc[r.method] = (acc[r.method] ?? 0) + r.paidMinor;
if (!acc[r.method]) acc[r.method] = { totalPaidEtbMinor: 0, currency: 'ETB' };
acc[r.method].totalPaidEtbMinor += toEtbMinor(r.paidMinor, r.paidCurrency);
return acc;
}, {} as Record<string, number>);
}, {} as Record<string, { totalPaidEtbMinor: number; currency: string }>);
return { totalActualMinor, totalPaidMinor, byMethod, rows };
return { totalActualEtbMinor, totalPaidEtbMinor, byMethod, rows };
}
async getPaymentDiscrepancyBySchedule(scheduleId: string, params: {
@@ -909,13 +933,7 @@ export class ReportsService {
},
include: {
paymentIntent: { select: { amountMinor: true, currency: true } },
schedule: {
include: {
originStation: { select: { name: true } },
destinationStation: { select: { name: true } },
},
},
package: { select: { id: true } },
schedule: { select: { id: true } },
seats: {
where: { leg: 1 },
orderBy: [
@@ -940,6 +958,14 @@ export class ReportsService {
},
});
const stationIds = [...new Set(
bookings.flatMap(b => [b.originStationId, b.destinationStationId]).filter(Boolean) as string[],
)];
const stations = stationIds.length > 0
? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } })
: [];
const stationName = new Map(stations.map(s => [s.id, s.name]));
const resolveSeatClass = (seat: any): string => {
const classes = seat?.coach?.coachType?.seatClasses ?? [];
const matched = seat?.bedPosition
@@ -954,7 +980,7 @@ export class ReportsService {
// pi.amountMinor is a Float in full currency units — convert to cents once
const paidMinorCents = Math.round(pi.amountMinor * 100);
const isPackage = !!(b as any).package;
const isPackage = !!(b as any).packageId;
const effectiveActualMinor = isPackage ? actualMinor * 2 : actualMinor;
const effectiveVarianceMinor = effectiveActualMinor - paidMinorCents;
@@ -973,8 +999,8 @@ export class ReportsService {
seatClass: firstSeat?.seatLabelSnapshot ?? firstSeat?.seat?.coach?.coachType?.name ?? '—',
coachNumber: firstSeat?.seat?.coach?.number ?? null,
seatNumber: firstSeat?.seat?.seatNumber ?? null,
origin: b.schedule.originStation.name,
destination: b.schedule.destinationStation.name,
origin: b.originStationId ? (stationName.get(b.originStationId) ?? '—') : '—',
destination: b.destinationStationId ? (stationName.get(b.destinationStationId) ?? '—') : '—',
phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
actualMinor: effectiveActualMinor,
paidMinor: paidMinorCents,
@@ -989,7 +1015,7 @@ export class ReportsService {
}
if (params.seatClass?.trim()) {
const sc = params.seatClass.trim().toLowerCase();
rows = rows.filter(r => r.breakdown.some(b => b.seatClass.toLowerCase().includes(sc)));
rows = rows.filter(r => r.breakdown.some(bd => bd.seatClass.toLowerCase().includes(sc)));
}
if (params.sort === 'asc') {
rows.sort((a, b) => a.varianceMinor - b.varianceMinor);

View File

@@ -921,7 +921,7 @@ export class TicketsService {
for (const booking of confirmedWithNoTickets) {
try {
await this.generate(booking.id);
await this.smartAssignAndGenerate(booking.id);
generated++;
details.push({ bookingId: booking.id, bookingRef: booking.bookingRef, status: 'generated' });
} catch (err) {