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 5a2692f44..23f89124a 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -418,6 +418,7 @@ export class ReportsService { select: { id: true, departureAt: true, + isPackageOnly: true, train: { select: { number: true } }, originStation: { select: { name: true } }, destinationStation: { select: { name: true } }, @@ -428,7 +429,10 @@ export class ReportsService { return schedules.map((s) => ({ id: s.id, departureAt: s.departureAt, - label: `${s.train.number} · ${s.originStation.name} → ${s.destinationStation.name} · ${new Date(s.departureAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}`, + isPackage: s.isPackageOnly, + label: `${s.train.number} · ${s.originStation.name} → ${s.destinationStation.name} · ${new Date(s.departureAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}${ + s.isPackageOnly ? ' (package)' : '' + }`, })); } @@ -915,6 +919,7 @@ export class ReportsService { destinationStation: { select: { name: true } }, }, }, + package: { select: { id: true } }, seats: { where: { leg: 1 }, orderBy: [ @@ -953,29 +958,34 @@ export class ReportsService { const paidMinor = Math.round(pi.amountMinor); const varianceMinor = actualMinor - paidMinor; + const isPackage = !!(b as any).package; + const effectiveActualMinor = isPackage ? actualMinor * 2 : actualMinor; + const effectiveVarianceMinor = effectiveActualMinor - paidMinor; + const breakdown = b.seats.map(s => ({ passengerName: s.passengerName ?? '—', seatClass: resolveSeatClass(s.seat), coachNumber: s.seat?.coach?.number ?? null, seatNumber: s.seat?.seatNumber ?? null, - fareMinor: s.fareMinor ?? 0, + fareMinor: isPackage ? (s.fareMinor ?? 0) * 2 : (s.fareMinor ?? 0), })); const firstSeat = b.seats[0]; return { bookingRef: b.bookingRef, + isPackage, 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, phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—', - actualMinor, + actualMinor: effectiveActualMinor, paidMinor, - varianceMinor, + varianceMinor: effectiveVarianceMinor, breakdown, }; - }).filter(r => r.varianceMinor > 0); + }).filter(r => r.varianceMinor > 0 && r.paidMinor > 0); if (params.search?.trim()) { const q = params.search.trim().toUpperCase(); diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/payments/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/payments/page.tsx index 4fe1a17a0..7ff4e5247 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/payments/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/payments/page.tsx @@ -10,7 +10,7 @@ import { apiClient } from '@/lib/api-client'; // ── Types ───────────────────────────────────────────────────────────────────── -interface ScheduleOption { id: string; label: string; departureAt: string; } +interface ScheduleOption { id: string; label: string; departureAt: string; isPackage: boolean; } interface PassengerBreakdown { passengerName: string; @@ -22,6 +22,7 @@ interface PassengerBreakdown { interface DiscrepancyRow { bookingRef: string; + isPackage: boolean; seatClass: string; coachNumber: string | null; seatNumber: string | null; @@ -81,10 +82,15 @@ function Pagination({ page, totalPages, setPage, total }: { // ── Helpers ─────────────────────────────────────────────────────────────────── -function fmt(minor: number) { +function fmtMinor(minor: number) { return `ETB ${(minor / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; } +// paidMinor from PaymentIntent.amountMinor is a Float stored as full units (not cents) +function fmtPaid(amount: number) { + return `ETB ${amount.toLocaleString('en-US', { minimumFractionDigits: 2 })}`; +} + function downloadCsv(csv: string, filename: string) { const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); @@ -231,18 +237,10 @@ export default function PaymentsReportPage() { {data && (
-
+

{data.total} discrepanc{data.total !== 1 ? 'ies' : 'y'} found

- {data.rows.length > 0 && ( - - )}
@@ -270,7 +268,14 @@ export default function PaymentsReportPage() { - + - - + + @@ -314,14 +319,14 @@ export default function PaymentsReportPage() { - + ))} diff --git a/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx b/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx index ca81b5514..98f76cc18 100644 --- a/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx +++ b/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx @@ -125,7 +125,7 @@ const navigationSections: { title: string; items: NavItem[] }[] = [ { name: 'Seats', href: '/reports/seats', icon: Armchair, permission: PERMS.reports.view }, { name: 'Passengers', href: '/reports/passengers', icon: Users, permission: PERMS.reports.view }, { name: 'Payments', href: '/reports/payments', icon: CreditCard, permission: PERMS.reports.view }, - { name: 'Payment Discrepancy', href: '/reports/payment-discrepancy', icon: AlertTriangle, permission: PERMS.reports.view }, + // { name: 'Payment Discrepancy', href: '/reports/payment-discrepancy', icon: AlertTriangle, permission: PERMS.reports.view }, // { name: 'Operational Reports', href: '/operational-reports', icon: FileText, permission: PERMS.reports.view }, ] },
{isExpanded ? : } {r.bookingRef} + {r.bookingRef} + {r.isPackage && ( + + package + + )} + {r.seatClass} {r.coachNumber && · {r.coachNumber}} @@ -279,12 +284,12 @@ export default function PaymentsReportPage() { {r.origin} → {r.destination} {fmt(r.actualMinor)}{fmt(r.paidMinor)}{fmtMinor(r.actualMinor)}{fmtPaid(r.paidMinor)} - {fmt(r.varianceMinor)} + {fmtMinor(r.varianceMinor)} {r.phone}{b.seatClass} {b.coachNumber ?? '—'} {b.seatNumber ?? '—'}{fmt(b.fareMinor)}{fmtMinor(b.fareMinor)}
Total actual vs paid - {fmt(r.actualMinor)} / {fmt(r.paidMinor)} - (+{fmt(r.varianceMinor)}) + {fmtMinor(r.actualMinor)} / {fmtPaid(r.paidMinor)} + (+{fmtMinor(r.varianceMinor)})