mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
Payment discrepancy report updates
This commit is contained in:
@@ -47,6 +47,23 @@ export class ReportsController {
|
||||
return this.service.getPaymentDiscrepancyReport({ from, to, sortBy, search });
|
||||
}
|
||||
|
||||
@Get("payments")
|
||||
@ApiOperation({ summary: "Payments collected for a schedule" })
|
||||
getPaymentsReport(@Query('scheduleId') scheduleId: string) {
|
||||
return this.service.getPaymentsReport(scheduleId);
|
||||
}
|
||||
|
||||
@Get("payments/discrepancy")
|
||||
@ApiOperation({ summary: "Payment discrepancy breakdown for a schedule" })
|
||||
getPaymentDiscrepancyBySchedule(
|
||||
@Query('scheduleId') scheduleId: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('seatClass') seatClass?: string,
|
||||
@Query('sort') sort?: string,
|
||||
) {
|
||||
return this.service.getPaymentDiscrepancyBySchedule(scheduleId, { search, seatClass, sort });
|
||||
}
|
||||
|
||||
@Get(":reportId")
|
||||
@ApiOperation({ summary: "Get report by ID" })
|
||||
getReport(@Param("reportId") reportId: string) {
|
||||
|
||||
@@ -843,6 +843,157 @@ export class ReportsService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getPaymentsReport(scheduleId: string) {
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
scheduleId,
|
||||
status: { in: ['CONFIRMED', 'BOARDED', 'NO_SHOW'] as any },
|
||||
paymentIntent: { status: 'SUCCEEDED' },
|
||||
},
|
||||
include: {
|
||||
paymentIntent: { select: { amountMinor: true, currency: true, method: true, paidAt: true } },
|
||||
seats: {
|
||||
where: { leg: 1 },
|
||||
select: {
|
||||
passengerName: true,
|
||||
fareMinor: true,
|
||||
passengerCategory: true,
|
||||
seatLabelSnapshot: true,
|
||||
seat: { select: { coach: { select: { number: true, coachType: { select: { name: true } } } } } },
|
||||
},
|
||||
},
|
||||
passenger: { select: { user: { select: { phone: true, fullName: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
const rows = bookings.map(b => {
|
||||
const actualMinor = b.seats.reduce((s, seat) => s + (seat.fareMinor ?? 0), 0);
|
||||
const paidMinor = Math.round(b.paymentIntent!.amountMinor);
|
||||
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,
|
||||
actualMinor,
|
||||
paidMinor,
|
||||
currency: 'ETB',
|
||||
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 byMethod = rows.reduce((acc, r) => {
|
||||
acc[r.method] = (acc[r.method] ?? 0) + r.paidMinor;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return { totalActualMinor, totalPaidMinor, byMethod, rows };
|
||||
}
|
||||
|
||||
async getPaymentDiscrepancyBySchedule(scheduleId: string, params: {
|
||||
search?: string;
|
||||
seatClass?: string;
|
||||
sort?: string;
|
||||
}) {
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
scheduleId,
|
||||
status: { in: ['CONFIRMED', 'BOARDED', 'NO_SHOW'] as any },
|
||||
paymentIntent: { status: 'SUCCEEDED' },
|
||||
},
|
||||
include: {
|
||||
paymentIntent: { select: { amountMinor: true, currency: true } },
|
||||
schedule: {
|
||||
include: {
|
||||
originStation: { select: { name: true } },
|
||||
destinationStation: { select: { name: true } },
|
||||
},
|
||||
},
|
||||
seats: {
|
||||
where: { leg: 1 },
|
||||
orderBy: [
|
||||
{ seat: { coach: { number: 'asc' as const } } },
|
||||
{ seat: { seatNumber: 'asc' as const } },
|
||||
],
|
||||
select: {
|
||||
passengerName: true,
|
||||
passengerCategory: true,
|
||||
seatLabelSnapshot: true,
|
||||
fareMinor: true,
|
||||
seat: {
|
||||
select: {
|
||||
seatNumber: true,
|
||||
coach: { select: { number: true, coachType: { select: { name: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
passenger: { select: { user: { select: { phone: true, fullName: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
let rows = bookings.map(b => {
|
||||
const pi = b.paymentIntent!;
|
||||
const actualMinor = b.seats.reduce((s, seat) => s + (seat.fareMinor ?? 0), 0);
|
||||
const paidMinor = Math.round(pi.amountMinor);
|
||||
const varianceMinor = actualMinor - paidMinor;
|
||||
|
||||
// Per-seat-class breakdown
|
||||
const byClass = new Map<string, { seatClass: string; coachNumber: string | null; seatNumber: string | null; fareMinor: number }[]>();
|
||||
for (const s of b.seats) {
|
||||
const key = s.seatLabelSnapshot ?? s.seat?.coach?.coachType?.name ?? 'Unknown';
|
||||
if (!byClass.has(key)) byClass.set(key, []);
|
||||
byClass.get(key)!.push({
|
||||
seatClass: key,
|
||||
coachNumber: s.seat?.coach?.number ?? null,
|
||||
seatNumber: s.seat?.seatNumber ?? null,
|
||||
fareMinor: s.fareMinor ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
const breakdown = [...byClass.entries()].map(([seatClass, seats]) => ({
|
||||
seatClass,
|
||||
seats: seats.map(s => ({ coachNumber: s.coachNumber, seatNumber: s.seatNumber })),
|
||||
totalFareMinor: seats.reduce((s, x) => s + x.fareMinor, 0),
|
||||
count: seats.length,
|
||||
}));
|
||||
|
||||
const firstSeat = b.seats[0];
|
||||
return {
|
||||
bookingRef: b.bookingRef,
|
||||
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,
|
||||
paidMinor,
|
||||
varianceMinor,
|
||||
breakdown,
|
||||
};
|
||||
}).filter(r => r.varianceMinor > 0);
|
||||
|
||||
if (params.search?.trim()) {
|
||||
const q = params.search.trim().toUpperCase();
|
||||
rows = rows.filter(r => r.bookingRef.toUpperCase().includes(q));
|
||||
}
|
||||
if (params.seatClass?.trim()) {
|
||||
const sc = params.seatClass.trim().toLowerCase();
|
||||
rows = rows.filter(r => r.breakdown.some(b => b.seatClass.toLowerCase().includes(sc)));
|
||||
}
|
||||
if (params.sort === 'asc') {
|
||||
rows.sort((a, b) => a.varianceMinor - b.varianceMinor);
|
||||
} else {
|
||||
rows.sort((a, b) => b.varianceMinor - a.varianceMinor);
|
||||
}
|
||||
|
||||
return { total: rows.length, rows };
|
||||
}
|
||||
|
||||
async getReport(reportId: string) {
|
||||
return this.prisma.operationalReport.findUnique({
|
||||
where: { id: reportId },
|
||||
|
||||
Reference in New Issue
Block a user