fix permission

This commit is contained in:
Marshal
2026-08-07 11:17:44 +00:00
parent ffdb4c5117
commit 756325c814

View File

@@ -1074,16 +1074,23 @@ export class ContractsController {
// ── Booking under contract (Path A customer / Path B GL ET) ──────────────── // ── Booking under contract (Path A customer / Path B GL ET) ────────────────
@Post(':id/bookings') @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({ @ApiOperation({
summary: summary:
'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).', 'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).',
}) })
createBooking( async createBooking(
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@Body() dto: CreateBookingUnderContractDto, @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: // The service decides the execution path from the contract:
// Path A (customs disabled) → customer/staff create; status checks apply. // Path A (customs disabled) → customer/staff create; status checks apply.
// Path B (customs enabled) → GL Ethiopia only, once clearance is ready. // Path B (customs enabled) → GL Ethiopia only, once clearance is ready.
@@ -1096,16 +1103,25 @@ export class ContractsController {
} }
@Post(':id/bookings/initiate') @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({ @ApiOperation({
summary: 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.', '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, @Param('id', ParseUUIDPipe) id: string,
@Body() dto: CreateBookingUnderContractDto, @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( return this.contractBookingService.initiateUnderContract(
id, id,
{ contractRouteId: dto?.contractRouteId }, { contractRouteId: dto?.contractRouteId },
@@ -1115,17 +1131,24 @@ export class ContractsController {
} }
@Post(':id/bookings/:bookingId/complete') @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({ @ApiOperation({
summary: summary:
'Complete an initiated booking after Operations finalized its clearance — cargo + binding day, window and departure checks, pricing and invoicing.', '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('id', ParseUUIDPipe) id: string,
@Param('bookingId', ParseUUIDPipe) bookingId: string, @Param('bookingId', ParseUUIDPipe) bookingId: string,
@Body() dto: CreateBookingUnderContractDto, @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 // Customs (Path B) instances may only be completed by GL Ethiopia — the
// service checks the actor's contracts:create_booking permission. // service checks the actor's contracts:create_booking permission.
return this.contractBookingService.completeUnderContract( return this.contractBookingService.completeUnderContract(