Merge branch 'dev' into passenger/feat/iam

This commit is contained in:
Abubeker Yasin
2026-06-22 21:01:08 +03:00
17 changed files with 1356 additions and 200 deletions

View File

@@ -411,6 +411,17 @@ export class BookingsController {
return this.service.create(dto);
}
@Get(':id/usage')
@ApiOperation({
summary: 'Check if booking is in use',
description: 'Returns list of modules/data that reference this booking'
})
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
@ApiResponse({ status: 404, description: 'Booking not found' })
checkUsage(@Param('id') id: string) {
return this.service.checkBookingUsage(id);
}
@Get(':bookingRef')
@ApiOperation({
summary: 'Get booking details by reference (no auth required)',
@@ -422,17 +433,6 @@ export class BookingsController {
return this.service.getByRef(ref);
}
@Patch(':id')
@ApiOperation({
summary: 'Update booking details',
description: 'Updates booking information for admin/agent operations'
})
@ApiResponse({ status: 200, description: 'Booking updated successfully' })
@ApiResponse({ status: 404, description: 'Booking not found' })
update(@Param('id') id: string, @Body() dto: any) {
return this.service.update(id, dto);
}
@Patch(':bookingRef/modify')
@UseGuards(JwtGuard)
@ApiBearerAuth('JWT-auth')
@@ -457,17 +457,17 @@ export class BookingsController {
return this.service.delete(id);
}
@Get(':id/usage')
@Patch(':id')
@ApiOperation({
summary: 'Check if booking is in use',
description: 'Returns list of modules/data that reference this booking'
summary: 'Update booking details',
description: 'Updates booking information for admin/agent operations'
})
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
@ApiResponse({ status: 200, description: 'Booking updated successfully' })
@ApiResponse({ status: 404, description: 'Booking not found' })
checkUsage(@Param('id') id: string) {
return this.service.checkBookingUsage(id);
update(@Param('id') id: string, @Body() dto: any) {
return this.service.update(id, dto);
}
@Delete(':bookingRef')
@UseGuards(JwtGuard)
@ApiBearerAuth('JWT-auth')

View File

@@ -988,31 +988,47 @@ export class BookingsService {
where: { bookingRef },
include: {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
seats: { include: { seat: { include: { coach: true } } } },
seats: { include: { seat: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } } } },
paymentIntent: true, ticket: true,
},
});
if (!booking) throw new NotFoundException('Booking not found');
return {
id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
totalFare: booking.totalMinor / 100, adultCount: booking.adultCount, childCount: booking.childCount,
displayCurrency: booking.displayCurrency, displayTotalFare: booking.displayTotalMinor ? booking.displayTotalMinor / 100 : undefined,
totalMinor: booking.totalMinor, currency: 'ETB',
adultCount: booking.adultCount, childCount: booking.childCount,
displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor ?? undefined,
bookingType: booking.bookingType,
returnLegStatus: (booking as any).returnLegStatus ?? null,
outboundBoardedAt: (booking as any).outboundBoardedAt ?? null,
returnBoardedAt: (booking as any).returnBoardedAt ?? null,
contactEmail: booking.contactEmail,
contactPhone: booking.contactPhone,
createdAt: booking.createdAt,
schedule: {
number: booking.schedule.train.number,
id: booking.schedule.id,
trainNumber: booking.schedule.train.number,
trainName: booking.schedule.train.name,
origin: { id: booking.schedule.originStation.id, name: booking.schedule.originStation.name, code: booking.schedule.originStation.code, city: booking.schedule.originStation.city },
destination: { id: booking.schedule.destinationStation.id, name: booking.schedule.destinationStation.name, code: booking.schedule.destinationStation.code, city: booking.schedule.destinationStation.city },
departureAt: booking.schedule.departureAt, arrivalAt: booking.schedule.arrivalAt,
},
passengers: booking.seats?.map((bs: any) => ({
fullName: bs.passengerName, category: bs.passengerCategory, verifaydaVerified: bs.verifaydaVerified,
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass.name },
fullName: bs.passengerName,
category: bs.passengerCategory,
leg: bs.leg ?? 1,
fareMinor: bs.fareMinor,
verifaydaVerified: bs.verifaydaVerified,
seat: {
id: bs.seat.id,
number: bs.seat.seatNumber,
coach: bs.seat.coach.number,
coachId: bs.seat.coach.id,
seatClass: bs.seat.coach.coachType?.seatClasses?.[0]?.name ?? null,
},
})),
payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined,
ticket: booking.ticket ? { id: booking.ticket.id, qrPayload: booking.ticket.qrPayload, barcodePayload: booking.ticket.barcodePayload, status: booking.ticket.status } : undefined,
};
}

View File

@@ -191,7 +191,9 @@ export class GuestBookingService {
displayCurrency,
displayTotalMinor,
bookingType: 'ONE_WAY',
userAgent: dto.deviceId,
userAgent: dto.deviceId,
contactEmail: firstPassenger.email || null,
contactPhone: firstPassenger.phone || null,
seats: {
create: passengersData.map((p) => ({
seat: { connect: { id: p.seatId } },
@@ -383,6 +385,8 @@ export class GuestBookingService {
returnSeatClassId,
returnLegStatus: 'NEITHER_USED',
userAgent: dto.deviceId,
contactEmail: passengersData[0]?.email || null,
contactPhone: passengersData[0]?.phone || null,
seats: {
create: [
...passengersData.map((p) => ({
@@ -573,6 +577,8 @@ export class GuestBookingService {
leg2DestinationStationId: dto.leg2DestinationStationId,
leg2SeatClassId: leg2SeatClassId,
userAgent: dto.deviceId,
contactEmail: passengersData[0]?.email || null,
contactPhone: passengersData[0]?.phone || null,
seats: {
create: [
...passengersData.map(p => ({
@@ -784,6 +790,8 @@ export class GuestBookingService {
returnLeg2SeatClassId: retL2ClassId,
returnLegStatus: 'NEITHER_USED',
userAgent: dto.deviceId,
contactEmail: passengersData[0]?.email || null,
contactPhone: passengersData[0]?.phone || null,
seats: {
create: [
...passengersData.map(p => makeSeat(p, p.seatId, 1, dto.scheduleId, obL1Fare)),

View File

@@ -108,10 +108,20 @@ export class TicketsService {
include: {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
seats: { include: { seat: { include: { coach: true } } } },
paymentIntent: true,
},
});
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
if (booking.status !== 'CONFIRMED') {
const paymentStatus = booking.paymentIntent?.status ?? null;
throw new BadRequestException(
`Payment not completed. Please complete your payment before accessing the ticket. ` +
`Booking status: ${booking.status}` +
(paymentStatus ? `. Payment status: ${paymentStatus}` : ''),
);
}
// Build a compact multi-leg payload for the QR so gate scanners see all legs
const legSummary = this.buildLegSummary(booking);
const qrData = JSON.stringify({