mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
feat(auth): gate and regate freight API controllers
Gates the previously open support-agent, procurement, compliance, facilities, list-users and trade-access controllers, separates customer from staff routes across bookings, contracts, companies, billing, warehouses, files and train scheduling, and moves billing, overview, reports and the settings controllers onto their own keys instead of the blanket admin key. Drops the demo-permissions module and the untested notification test route.
This commit is contained in:
@@ -14,12 +14,10 @@ import {
|
||||
UnauthorizedException,
|
||||
UploadedFiles,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
|
||||
import { AnyFilesInterceptor, FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import {
|
||||
@@ -32,7 +30,7 @@ import {
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { actorLabel } from '../warehouses/current-actor.util';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { BookingStaff, MixedAudience, PortalCustomer } from '../../common/booking-guards';
|
||||
import { ContractDocumentHistoryService } from './contract-document-history.service';
|
||||
import {
|
||||
FREIGHT_PERMS,
|
||||
@@ -127,6 +125,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get('booking-requests/:reqId')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'A single shipment request' })
|
||||
getBookingRequest(@Param('reqId', ParseUUIDPipe) reqId: string) {
|
||||
return this.bookingRequestService.findOne(reqId);
|
||||
@@ -159,6 +158,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('booking-requests/:reqId/cancel')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Customer cancels their own pending shipment request' })
|
||||
cancelBookingRequest(
|
||||
@Param('reqId', ParseUUIDPipe) reqId: string,
|
||||
@@ -168,6 +168,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/booking-requests')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Customer submits a shipment request on a GENERAL customs contract' })
|
||||
submitBookingRequest(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -178,12 +179,14 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get(':id/booking-requests')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'List the shipment requests on a contract' })
|
||||
listBookingRequests(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.bookingRequestService.listForContract(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Create a new contract (DRAFT) with routes + cargo scope' })
|
||||
@@ -203,6 +206,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@MixedAudience([])
|
||||
@ApiOperation({ summary: 'List contracts (paginated)' })
|
||||
async findAll(
|
||||
@Query() filter: FilterContractDto,
|
||||
@@ -247,6 +251,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "List the current customer's contracts" })
|
||||
async findMy(
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
@@ -301,6 +306,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Get contract by ID (routes, cargo scope, unit rates)' })
|
||||
async findOne(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -319,6 +325,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({
|
||||
@@ -337,6 +344,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@MixedAudience([])
|
||||
@HttpCode(204)
|
||||
@ApiOperation({ summary: 'Soft-delete DRAFT contract' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
@@ -344,6 +352,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/documents')
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Upload intake documents for a contract (DRAFT only)' })
|
||||
@@ -355,18 +364,21 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/generate-price')
|
||||
@MixedAudience([])
|
||||
@ApiOperation({ summary: 'Generate unit-rate breakdown (no totals at contract phase)' })
|
||||
generatePrice(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.pricingService.generatePrice(id);
|
||||
}
|
||||
|
||||
@Post(':id/submit')
|
||||
@MixedAudience([])
|
||||
@ApiOperation({ summary: 'Customer submit contract (freezes contract_rate_snapshots)' })
|
||||
submit(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.transitionService.submit(id);
|
||||
}
|
||||
|
||||
@Post(':id/confirm-submit')
|
||||
@MixedAudience([])
|
||||
@ApiOperation({ summary: 'Confirm submit after a price change' })
|
||||
confirmSubmit(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.transitionService.confirmSubmit(id);
|
||||
@@ -427,10 +439,7 @@ export class ContractsController {
|
||||
// real boundary: it admits only the approver whose step is currently pending
|
||||
// (edit rights hand off down the chain on each approval).
|
||||
@Put(':id/document/articles')
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.contracts.view,
|
||||
...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept),
|
||||
])
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.editDocument)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Edit this contract\'s document articles only (per-contract; never touches the six shared templates)',
|
||||
@@ -515,6 +524,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/cancel')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: 'Customer cancels their own contract (blocked while a booking is live)',
|
||||
})
|
||||
@@ -591,6 +601,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get(':id/contract/view')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Contract PDF view-model + rendered HTML for signing' })
|
||||
async getContractView(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -630,6 +641,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get(':id/contract/document')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Download contract PDF' })
|
||||
async downloadContractDocument(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -653,7 +665,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/contract/send-signing-otp')
|
||||
@UseGuards(JwtGuard)
|
||||
@MixedAudience(bothFreightTypes(FREIGHT_PERMS.contracts.signStaff))
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Send the sudo-mode signing OTP to the contract company's registered phone (server picks the number)",
|
||||
@@ -666,7 +678,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/contract/sign')
|
||||
@UseGuards(JwtGuard)
|
||||
@MixedAudience(bothFreightTypes(FREIGHT_PERMS.contracts.signStaff))
|
||||
@ApiOperation({ summary: 'Apply digital signature (customer or staff/director/ceo)' })
|
||||
async signContract(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -691,6 +703,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/renew')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Create a renewal draft linked via renewalOfId' })
|
||||
async renew(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -712,12 +725,14 @@ export class ContractsController {
|
||||
// ── Pre-booking clearance (Path B, doc §15.2.1) ────────────────────────────
|
||||
|
||||
@Get(':id/clearance')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Pre-booking clearance document grid on the contract' })
|
||||
getClearance(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.clearanceService.getClearanceView(id);
|
||||
}
|
||||
|
||||
@Post(':id/clearance/documents')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer uploads clearance documents (fieldname = document key)' })
|
||||
@@ -901,6 +916,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/clearance/duty/dispute')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Customer disputes the advised duty/tax with a reason — reopens the step so GL Ethiopia can re-advise (repeatable)',
|
||||
@@ -914,6 +930,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/clearance/duty-slip')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer uploads duty/tax payment slip on contract' })
|
||||
@@ -1057,6 +1074,7 @@ export class ContractsController {
|
||||
// ── Booking under contract (Path A customer / Path B GL ET) ────────────────
|
||||
|
||||
@Post(':id/bookings')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.createBooking)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).',
|
||||
@@ -1078,6 +1096,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/bookings/initiate')
|
||||
@BookingStaff(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.',
|
||||
@@ -1096,6 +1115,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/bookings/:bookingId/complete')
|
||||
@BookingStaff(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.',
|
||||
@@ -1117,6 +1137,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post(':id/validate-shipment')
|
||||
@MixedAudience([FREIGHT_PERMS.contracts.createBooking, FREIGHT_PERMS.contracts.view])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Pre-create validation + authoritative price preview: full booking price breakdown (rail, first/last mile, surcharges), overweight lines and 20ft weight-pairing errors for a shipment payload (no booking created).',
|
||||
@@ -1129,6 +1150,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get(':id/capacity')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Remaining bookable quantity per cargo line (GENERAL draw-down cap, or the outstanding remainder of a split ONE_TIME contract)',
|
||||
@@ -1141,12 +1163,14 @@ export class ContractsController {
|
||||
// ── Clearance milestones (doc §11.3, §12.2) ────────────────────────────────
|
||||
|
||||
@Get(':id/milestones')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Pre-booking clearance milestones for a contract cycle' })
|
||||
listContractMilestones(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.milestoneService.listForContract(id);
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/milestones')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'Post-booking clearance milestones for a shipment booking' })
|
||||
listBookingMilestones(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.milestoneService.listForBooking(bookingId);
|
||||
@@ -1280,7 +1304,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/final-invoice')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.finalInvoiceRaise)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({
|
||||
@@ -1307,6 +1331,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/final-invoice/approve')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: 'Customer approves the drafted final invoice — unlocks the payment slip',
|
||||
})
|
||||
@@ -1321,6 +1346,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/final-invoice-slip')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer attaches the payment slip for the final invoice' })
|
||||
@@ -1332,10 +1358,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/final-invoice/confirm')
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.contracts.clearanceDjActions,
|
||||
FREIGHT_PERMS.contracts.clearanceEtActions,
|
||||
])
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.finalInvoiceConfirm)
|
||||
@ApiOperation({ summary: 'GL (ET or DJ) confirms the payment slip — settles the final invoice' })
|
||||
confirmFinalInvoicePaid(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@@ -1378,6 +1401,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/second-duty-slip')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer attaches the additional duty/tax payment slip' })
|
||||
@@ -1403,6 +1427,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/duty-slip')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer uploads the duty/tax payment slip' })
|
||||
@@ -1419,6 +1444,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/incidents')
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.view)
|
||||
@ApiOperation({ summary: 'List cargo exception/damage reports for a shipment' })
|
||||
listIncidents(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.glOperationsService.listIncidents(bookingId);
|
||||
|
||||
Reference in New Issue
Block a user