mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Boarding status update issue fix
This commit is contained in:
@@ -177,7 +177,7 @@ export class TicketsService {
|
||||
} : null,
|
||||
status: t.status,
|
||||
validatedAt: t.validatedAt,
|
||||
boardedAt: t.validatedAt,
|
||||
boardedAt: t.boardedAt ?? t.validatedAt,
|
||||
qrCode: t.qrPayload ?? null,
|
||||
createdAt: t.issuedAt,
|
||||
};
|
||||
@@ -663,6 +663,13 @@ export class TicketsService {
|
||||
|
||||
// Use existing validation logic to handle round trips properly
|
||||
const result = await this.validate(bookingRef, validatorId, gateId);
|
||||
if ((result as any).alreadyValidated) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Ticket already used',
|
||||
errorCode: 'ALREADY_USED',
|
||||
};
|
||||
}
|
||||
|
||||
// Get seat information
|
||||
const seatInfo = (booking as any).seats[0];
|
||||
@@ -756,12 +763,23 @@ export class TicketsService {
|
||||
const type = booking.bookingType;
|
||||
const now = new Date();
|
||||
|
||||
const markTicketUsed = async () => {
|
||||
if (ticket.status !== 'USED') {
|
||||
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { status: 'USED' } });
|
||||
ticket.status = 'USED';
|
||||
}
|
||||
};
|
||||
|
||||
// ── ONE_WAY / TRANSIT (single scan) ───────────────────────────────────
|
||||
if (type === 'ONE_WAY') {
|
||||
if (ticket.validatedAt) {
|
||||
await markTicketUsed();
|
||||
return { validated: true, ticketId: ticket.id, validatedAt: ticket.validatedAt, alreadyValidated: true };
|
||||
}
|
||||
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
|
||||
await this.prisma.ticket.update({
|
||||
where: { id: ticket.id },
|
||||
data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId, status: 'USED' },
|
||||
});
|
||||
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' } });
|
||||
@@ -778,13 +796,22 @@ export class TicketsService {
|
||||
const alreadyValidated = logs.some(l => l.leg === resolvedLeg);
|
||||
if (alreadyValidated) {
|
||||
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: `${resolvedLeg}_ALREADY_USED` } as any });
|
||||
await markTicketUsed();
|
||||
throw new BadRequestException(`${resolvedLeg} already validated`);
|
||||
}
|
||||
if (!ticket.validatedAt) await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
|
||||
const validatedAt = ticket.validatedAt ?? now;
|
||||
if (!ticket.validatedAt) {
|
||||
await this.prisma.ticket.update({
|
||||
where: { id: ticket.id },
|
||||
data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId, status: 'USED' },
|
||||
});
|
||||
} else {
|
||||
await markTicketUsed();
|
||||
}
|
||||
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 } });
|
||||
return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now };
|
||||
return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt };
|
||||
}
|
||||
|
||||
// ── ROUND_TRIP — leg=OUTBOUND or leg=RETURN ────────────────────────
|
||||
@@ -798,12 +825,14 @@ export class TicketsService {
|
||||
if (resolvedLeg === 'OUTBOUND') {
|
||||
if ((booking as any).outboundBoardedAt) {
|
||||
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: 'OUTBOUND_ALREADY_USED' } as any });
|
||||
await markTicketUsed();
|
||||
throw new BadRequestException('Outbound leg already validated');
|
||||
}
|
||||
bookingData.outboundBoardedAt = now;
|
||||
} else if (resolvedLeg === 'RETURN') {
|
||||
if ((booking as any).returnBoardedAt) {
|
||||
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: 'RETURN_ALREADY_USED' } as any });
|
||||
await markTicketUsed();
|
||||
throw new BadRequestException('Return leg already validated');
|
||||
}
|
||||
bookingData.returnBoardedAt = now;
|
||||
@@ -812,13 +841,19 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
await this.prisma.booking.update({ where: { id: booking.id }, data: bookingData });
|
||||
const validatedAt = ticket.validatedAt ?? now;
|
||||
if (!ticket.validatedAt) {
|
||||
await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
|
||||
await this.prisma.ticket.update({
|
||||
where: { id: ticket.id },
|
||||
data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId, status: 'USED' },
|
||||
});
|
||||
} else {
|
||||
await markTicketUsed();
|
||||
}
|
||||
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 } });
|
||||
return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now };
|
||||
return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt };
|
||||
}
|
||||
|
||||
throw new BadRequestException(`Unsupported booking type: ${type}`);
|
||||
|
||||
@@ -228,6 +228,24 @@ describe("Ticketing — generate / scanAndBoard / validate / smart-reassign", ()
|
||||
|
||||
const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
|
||||
expect(ticket?.validatedAt).toBeTruthy();
|
||||
expect(ticket?.status).toBe('USED');
|
||||
});
|
||||
|
||||
it("does not allow boarding the same ticket twice", async () => {
|
||||
const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-BOARD-REUSE-${Date.now()}`, departureAt: future(60), arrivalAt: future(120) });
|
||||
const booking = await createOneWayBooking(schedule.id, seats[0].id);
|
||||
await markSucceeded(booking.id);
|
||||
await ticketsService.generate(booking.id);
|
||||
|
||||
const first = await ticketsService.scanAndBoard(booking.bookingRef, "gate-validator-1");
|
||||
expect(first.success).toBe(true);
|
||||
|
||||
const second = await ticketsService.scanAndBoard(booking.bookingRef, "gate-validator-1");
|
||||
expect(second.success).toBe(false);
|
||||
expect(second.error).toMatch(/already used/i);
|
||||
|
||||
const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
|
||||
expect(ticket?.status).toBe('USED');
|
||||
});
|
||||
|
||||
it("refuses boarding before the boarding window opens", async () => {
|
||||
|
||||
Reference in New Issue
Block a user