Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha

This commit is contained in:
Abubeker Yasin
2026-07-27 11:07:44 +03:00
6 changed files with 508 additions and 3 deletions

View File

@@ -53,6 +53,12 @@ export class ReportsController {
return this.service.getSeatStatusReport(scheduleId);
}
@Get("boarding")
@ApiOperation({ summary: "Boarding report for a schedule — boarded vs not-boarded passengers" })
getBoardingReport(@Query('scheduleId') scheduleId: string) {
return this.service.getBoardingReport(scheduleId);
}
@Get("payments")
@ApiOperation({ summary: "Payments collected for a schedule" })
getPaymentsReport(@Query('scheduleId') scheduleId: string) {

View File

@@ -1022,6 +1022,119 @@ export class ReportsService {
return { total: rows.length, rows };
}
async getBoardingReport(scheduleId: string) {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: scheduleId },
select: {
id: true,
departureAt: true,
arrivalAt: true,
train: { select: { number: true, name: true } },
originStation: { select: { name: true } },
destinationStation: { select: { name: true } },
},
});
if (!schedule) return null;
const tickets = await this.prisma.ticket.findMany({
where: {
OR: [
{ scheduleId, booking: { status: { in: ['CONFIRMED', 'BOARDED'] } } },
{ leg: 2, booking: { returnScheduleId: scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
{ scheduleId: null, leg: 1, booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
],
},
select: {
id: true,
bookingRef: true,
passengerName: true,
boardedAt: true,
validatorId: true,
status: true,
booking: {
select: {
status: true,
originStationId: true,
destinationStationId: true,
},
},
seat: {
select: {
seatNumber: true,
bedPosition: true,
coach: {
select: {
number: true,
coachType: { select: { name: true, seatClasses: { select: { name: true, bedPosition: true } } } },
},
},
},
},
},
orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }],
});
const stationIds = [...new Set(
tickets.flatMap(t => [t.booking.originStationId, t.booking.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 | null => {
const classes = seat?.coach?.coachType?.seatClasses ?? [];
const matched = seat?.bedPosition
? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === seat.bedPosition.toLowerCase())
: null;
return (matched ?? classes[0])?.name ?? seat?.coach?.coachType?.name ?? null;
};
const rows = tickets.map(t => ({
bookingRef: t.bookingRef,
passengerName: t.passengerName,
coachNumber: t.seat?.coach?.number ?? null,
seatNumber: t.seat?.seatNumber ?? null,
seatClassName: resolveSeatClass(t.seat),
origin: t.booking.originStationId ? (stationName.get(t.booking.originStationId) ?? null) : null,
destination: t.booking.destinationStationId ? (stationName.get(t.booking.destinationStationId) ?? null) : null,
boarded: !!t.boardedAt,
boardedAt: t.boardedAt ?? null,
validatorId: t.validatorId ?? null,
bookingStatus: t.booking.status,
}));
const boardedCount = rows.filter(r => r.boarded).length;
const notBoardedCount = rows.length - boardedCount;
const byCoach = new Map<string, { coachNumber: string; total: number; boarded: number }>();
for (const r of rows) {
const key = r.coachNumber ?? 'Unknown';
if (!byCoach.has(key)) byCoach.set(key, { coachNumber: key, total: 0, boarded: 0 });
byCoach.get(key)!.total++;
if (r.boarded) byCoach.get(key)!.boarded++;
}
return {
schedule: {
id: schedule.id,
trainName: (schedule.train as any)?.name ?? (schedule.train as any)?.number,
origin: (schedule.originStation as any)?.name,
destination: (schedule.destinationStation as any)?.name,
departureAt: schedule.departureAt,
arrivalAt: schedule.arrivalAt,
},
summary: {
total: rows.length,
boardedCount,
notBoardedCount,
boardingRate: rows.length > 0 ? +((boardedCount / rows.length) * 100).toFixed(1) : 0,
},
byCoach: [...byCoach.values()].sort((a, b) => a.coachNumber.localeCompare(b.coachNumber)),
rows,
};
}
async getReport(reportId: string) {
return this.prisma.operationalReport.findUnique({
where: { id: reportId },

View File

@@ -761,7 +761,7 @@ export class TicketsService {
if (ticket.validatedAt) {
return { validated: true, ticketId: ticket.id, validatedAt: ticket.validatedAt, alreadyValidated: true };
}
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, validatorId: resolvedValidatorId } });
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, status: 'APPROVED' } });
this.fireBoardingPassNotification(booking, ticket, null);
await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 'ONE_WAY' } });
@@ -780,7 +780,7 @@ export class TicketsService {
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: `${resolvedLeg}_ALREADY_USED` } as any });
throw new BadRequestException(`${resolvedLeg} already validated`);
}
if (!ticket.validatedAt) await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, validatorId: resolvedValidatorId } });
if (!ticket.validatedAt) await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: resolvedLeg } });
@@ -813,7 +813,7 @@ export class TicketsService {
await this.prisma.booking.update({ where: { id: booking.id }, data: bookingData });
if (!ticket.validatedAt) {
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, validatorId: resolvedValidatorId } });
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
}
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);