mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
Boarding status update issue fix
This commit is contained in:
@@ -177,7 +177,7 @@ export class TicketsService {
|
|||||||
} : null,
|
} : null,
|
||||||
status: t.status,
|
status: t.status,
|
||||||
validatedAt: t.validatedAt,
|
validatedAt: t.validatedAt,
|
||||||
boardedAt: t.validatedAt,
|
boardedAt: t.boardedAt ?? t.validatedAt,
|
||||||
qrCode: t.qrPayload ?? null,
|
qrCode: t.qrPayload ?? null,
|
||||||
createdAt: t.issuedAt,
|
createdAt: t.issuedAt,
|
||||||
};
|
};
|
||||||
@@ -663,6 +663,13 @@ export class TicketsService {
|
|||||||
|
|
||||||
// Use existing validation logic to handle round trips properly
|
// Use existing validation logic to handle round trips properly
|
||||||
const result = await this.validate(bookingRef, validatorId, gateId);
|
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
|
// Get seat information
|
||||||
const seatInfo = (booking as any).seats[0];
|
const seatInfo = (booking as any).seats[0];
|
||||||
@@ -756,12 +763,23 @@ export class TicketsService {
|
|||||||
const type = booking.bookingType;
|
const type = booking.bookingType;
|
||||||
const now = new Date();
|
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) ───────────────────────────────────
|
// ── ONE_WAY / TRANSIT (single scan) ───────────────────────────────────
|
||||||
if (type === 'ONE_WAY') {
|
if (type === 'ONE_WAY') {
|
||||||
if (ticket.validatedAt) {
|
if (ticket.validatedAt) {
|
||||||
|
await markTicketUsed();
|
||||||
return { validated: true, ticketId: ticket.id, validatedAt: ticket.validatedAt, alreadyValidated: true };
|
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' } });
|
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, status: 'APPROVED' } });
|
||||||
this.fireBoardingPassNotification(booking, ticket, null);
|
this.fireBoardingPassNotification(booking, ticket, null);
|
||||||
await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 'ONE_WAY' } });
|
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);
|
const alreadyValidated = logs.some(l => l.leg === resolvedLeg);
|
||||||
if (alreadyValidated) {
|
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 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`);
|
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 });
|
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
|
||||||
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
|
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
|
||||||
await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 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 ────────────────────────
|
// ── ROUND_TRIP — leg=OUTBOUND or leg=RETURN ────────────────────────
|
||||||
@@ -798,12 +825,14 @@ export class TicketsService {
|
|||||||
if (resolvedLeg === 'OUTBOUND') {
|
if (resolvedLeg === 'OUTBOUND') {
|
||||||
if ((booking as any).outboundBoardedAt) {
|
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 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');
|
throw new BadRequestException('Outbound leg already validated');
|
||||||
}
|
}
|
||||||
bookingData.outboundBoardedAt = now;
|
bookingData.outboundBoardedAt = now;
|
||||||
} else if (resolvedLeg === 'RETURN') {
|
} else if (resolvedLeg === 'RETURN') {
|
||||||
if ((booking as any).returnBoardedAt) {
|
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 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');
|
throw new BadRequestException('Return leg already validated');
|
||||||
}
|
}
|
||||||
bookingData.returnBoardedAt = now;
|
bookingData.returnBoardedAt = now;
|
||||||
@@ -812,13 +841,19 @@ export class TicketsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.booking.update({ where: { id: booking.id }, data: bookingData });
|
await this.prisma.booking.update({ where: { id: booking.id }, data: bookingData });
|
||||||
|
const validatedAt = ticket.validatedAt ?? now;
|
||||||
if (!ticket.validatedAt) {
|
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 });
|
await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
|
||||||
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
|
this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
|
||||||
await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 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}`);
|
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 } });
|
const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
|
||||||
expect(ticket?.validatedAt).toBeTruthy();
|
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 () => {
|
it("refuses boarding before the boarding window opens", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user