Added group booking option

This commit is contained in:
Roba Boru
2026-08-24 18:04:13 +03:00
parent 8e2bab27cf
commit a5385e1462
13 changed files with 2000 additions and 51 deletions

View File

@@ -37,6 +37,7 @@ import {
import { JwtGuard } from "../../common/jwt.guard";
import { PassengerAdmin, PassengerStaff, PassengerStaffStrict } from "../../common/passenger-guards";
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
import { SeatsService } from "../seats/seats.service";
@ApiTags("Booking")
@Controller("bookings")
@@ -45,6 +46,7 @@ export class BookingsController {
constructor(
private service: BookingsService,
private guestService: GuestBookingService,
private seatsService: SeatsService,
) {}
@Get("my")
@@ -359,6 +361,37 @@ export class BookingsController {
return this.guestService.createGuestBooking(dto, req);
}
@Post("group")
@PassengerStaff([PASSENGER_PERMS.bookings.manage])
@ApiBearerAuth("IAM-auth")
@ApiOperation({
summary: "Create a group booking — staff bulk/group reservation, one PNR for the whole group",
description: `Staff-only entry point for bulk/group bookings (e.g. tour groups booked via an uploaded passenger list and auto-assigned seats from POST /seats/auto-assign-hold).
Same body shape as POST /bookings/guest (CreateGuestBookingDto) and the same underlying pipeline — fare engine, ADULT/CHILD age pricing — just gated to staff and always ONE_WAY.
Skips Verifayda national-ID verification: the roster comes from a staff-uploaded spreadsheet, not a live Fayda identity flow, so there is nothing to verify an ID number against. Passenger fields (name, DOB, nationality) are trusted exactly as uploaded.
Deliberately does NOT forward the staff caller's identity into booking creation: the acting staff member is not a Passenger, so the underlying guest-booking flow (which tries to resolve an authenticated caller as an existing Passenger profile) would reject the request. The booking is created exactly like a guest booking — a fresh passenger record, contact info from the first passenger in the list — with staff authorization enforced only at this route.
If booking creation fails after the seats were already held, the hold is released immediately so the seats don't sit locked for the rest of the hold TTL.`,
})
@ApiResponse({ status: 201, description: "Group booking created successfully with fareBreakdown" })
@ApiResponse({ status: 400, description: "Missing required seat IDs" })
async createGroup(@Body() dto: CreateGuestBookingDto) {
try {
return await this.guestService.createGuestBooking({ ...dto, bookingType: "ONE_WAY", skipIdentityVerification: true });
} catch (err) {
try {
await this.seatsService.releaseHold(dto.holdId);
} catch (releaseErr) {
// Best-effort — the hold may already be gone (e.g. it expired mid-request). The
// original booking-creation error is what the caller actually needs to see.
}
throw err;
}
}
@Post("reservations/:seatId/issue")
@PassengerStaffStrict(PASSENGER_PERMS.tickets.generate)
@ApiBearerAuth("IAM-auth")

View File

@@ -168,6 +168,15 @@ export class CreateGuestBookingDto {
@ApiPropertyOptional({ description: 'Total amount in minor units (ETB) as computed and displayed on the review page. When provided, overrides the fare engine total — use to pass the exact berth-specific amount the user saw.' })
@IsOptional() @IsNumber() reviewedTotalMinor?: number;
@ApiPropertyOptional({
description:
'Skip Verifayda national-ID verification and trust passenger fields as given (name, DOB, nationality). ' +
'For staff-entered/bulk-uploaded rosters (e.g. group bookings) where there is no live Fayda identity ' +
'flow to verify against — calling Verifayda for typed-in ID numbers either returns dev-mode mock data ' +
'(overwriting the real name) or, once configured, would reject the whole booking on a non-match.',
})
@IsOptional() @IsBoolean() skipIdentityVerification?: boolean;
}
export class SavedPassengerProfileDto {

View File

@@ -196,7 +196,7 @@ export class GuestBookingService {
passenger.idDocumentType === IdDocumentType.NATIONAL_ID;
if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
if (passenger.idDocumentNumber) {
if (passenger.idDocumentNumber && !dto.skipIdentityVerification) {
const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
if (!verification.verified) {
throw new BadRequestException(