diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts index 6273be365..0ff9b7c24 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts @@ -1074,16 +1074,23 @@ export class ContractsController { // ── Booking under contract (Path A customer / Path B GL ET) ──────────────── @Post(':id/bookings') - @BookingStaff(FREIGHT_PERMS.contracts.createBooking) + // Path A is a customer flow — both audiences must reach the service, whose + // assertGate decides per role. Staff still need contracts:create_booking. + @MixedAudience(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).', }) - createBooking( + async createBooking( @Param('id', ParseUUIDPipe) id: string, @Body() dto: CreateBookingUnderContractDto, - @CurrentUser() user: AuthUserPayload, + @CurrentUser() user: TCurrentUser & { sub?: string }, ) { + // Customer callers may only book on their own contract. + if (!hasFreightPermission(user, FREIGHT_PERMS.contracts.createBooking)) { + const contract = await this.contractsService.findById(id); + await this.contractsService.assertCustomerCanAccessContract(user?.id, contract); + } // The service decides the execution path from the contract: // Path A (customs disabled) → customer/staff create; status checks apply. // Path B (customs enabled) → GL Ethiopia only, once clearance is ready. @@ -1096,16 +1103,25 @@ export class ContractsController { } @Post(':id/bookings/initiate') - @BookingStaff(FREIGHT_PERMS.contracts.createBooking) + // Customer initiates their own ONE_TIME instance; GL initiates on customs + // contracts — the service's assertGate decides per role, so both audiences + // must reach it. Staff still need contracts:create_booking. + @MixedAudience(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Initiate a bare booking instance under an import/export contract (ONE_TIME or GENERAL) — no cargo, no date; enters per-booking clearance (AWAITING_DOCUMENTS). ONE_TIME customs instances are opened by the customer (or GL); GENERAL customs comes from a shipment request.', }) - initiateBooking( + async initiateBooking( @Param('id', ParseUUIDPipe) id: string, @Body() dto: CreateBookingUnderContractDto, - @CurrentUser() user: AuthUserPayload, + @CurrentUser() user: TCurrentUser & { sub?: string }, ) { + // Customer callers may only initiate on their own contract; the service's + // assertGate then decides what a customer may do on it. + if (!hasFreightPermission(user, FREIGHT_PERMS.contracts.createBooking)) { + const contract = await this.contractsService.findById(id); + await this.contractsService.assertCustomerCanAccessContract(user?.id, contract); + } return this.contractBookingService.initiateUnderContract( id, { contractRouteId: dto?.contractRouteId }, @@ -1115,17 +1131,24 @@ export class ContractsController { } @Post(':id/bookings/:bookingId/complete') - @BookingStaff(FREIGHT_PERMS.contracts.createBooking) + // Customers complete their own initiated (non-customs) instances; the + // service keeps customs completion GL-only via the actor's permissions. + @MixedAudience(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Complete an initiated booking after Operations finalized its clearance — cargo + binding day, window and departure checks, pricing and invoicing.', }) - completeBooking( + async completeBooking( @Param('id', ParseUUIDPipe) id: string, @Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: CreateBookingUnderContractDto, - @CurrentUser() user: AuthUserPayload, + @CurrentUser() user: TCurrentUser & { sub?: string }, ) { + // Customer callers may only complete bookings on their own contract. + if (!hasFreightPermission(user, FREIGHT_PERMS.contracts.createBooking)) { + const contract = await this.contractsService.findById(id); + await this.contractsService.assertCustomerCanAccessContract(user?.id, contract); + } // Customs (Path B) instances may only be completed by GL Ethiopia — the // service checks the actor's contracts:create_booking permission. return this.contractBookingService.completeUnderContract(