Boarding status update issue fix

This commit is contained in:
Stephanos A
2026-08-05 21:32:23 +03:00
parent 9b62ca6922
commit bf8906f983
2 changed files with 59 additions and 6 deletions

View File

@@ -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}`);

View File

@@ -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 () => {