import { Body, Controller, Post } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { SearchService } from './search.service'; import { SearchTripsDto } from './search.dto'; import { PassengerWrite } from '../../common/passenger-guards'; import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry'; /** * The staff group-booking search channel. * * Deliberately a SEPARATE controller from `SearchController` rather than another route on it. * `SearchController` carries a class-level `@IsPublic()`, and `JwtGuard` resolves that key with * `getAllAndOverride([handler, class])` — a handler with no key of its own inherits the class's * `true` and stays public. Worse, `JwtGuard` returns early for a public route and never * populates `request.user`, so an in-handler permission check there is not merely awkward, it is * impossible: there is no user to check. * * Splitting the controller is what makes the guard real. The channel is therefore decided by * which route the caller can reach, not by a field they send — which matters because this * channel exposes unpublished DRAFT schedules. */ @ApiTags('Search') @Controller('search') export class GroupSearchController { constructor(private service: SearchService) {} @Post('group') @PassengerWrite(PASSENGER_PERMS.bookings.create, PASSENGER_PERMS.bookings.manage) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Search trips as staff for a group booking — includes unpublished DRAFT trips', description: `Same request body and response shape as the public \`POST /search\`, but run on the staff channel. The staff channel is a strict **superset** of the public one: | Schedule state | Public \`POST /search\` | This route | | --- | --- | --- | | \`DRAFT\` (not yet published) | hidden | **visible** | | \`SCHEDULED\`/\`BOARDING\`/\`EN_ROUTE\`, ordinary | visible | visible | | \`SCHEDULED\`/\`BOARDING\`/\`EN_ROUTE\`, \`isGroupBookingOnly\` | hidden | **visible** | | \`CANCELLED\`/\`ARRIVED\`/\`DELAYED\` | hidden | hidden | | \`isPackageOnly\` | hidden | hidden | So a group can be booked onto an ordinary scheduled service, onto a trip reserved for groups, or onto a draft trip that has not gone on sale yet — all through the same seat inventory as normal booking. Replaces the old \`channel: 'GROUP_BOOKING'\` body field on \`POST /search\`, which was unguarded.`, }) @ApiResponse({ status: 200, description: 'Matching schedules, including DRAFT and group-reserved ones' }) @ApiResponse({ status: 403, description: 'Caller lacks bookings:create / bookings:manage' }) searchTripsForGroup(@Body() dto: SearchTripsDto) { return this.service.searchTrips(dto, true); } }