diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 2d41c6bda..7889c67a1 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -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(); + 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); + }, {} as Record); - 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); diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 736b781bd..e6f3b0781 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -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) {