mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
merge
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from '@edr/api-common';
|
||||
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { AiBookingRequestDto } from './dto/ai-booking-request.dto';
|
||||
import { AiBookingResult } from './types/ai-booking-result.type';
|
||||
import { MockAiService } from './mock-ai.service';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
|
||||
// @Public() — TODO: swap for real guard when this leaves dev/testing.
|
||||
// Safe while public: extracts + validates text only, never creates or
|
||||
// dispatches anything.
|
||||
@Public()
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.view)
|
||||
@ApiTags('AI Assistant (mock)')
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { ListUsersQueryDto } from './dto/list-users-query.dto';
|
||||
import { ListUsersService } from './list-users.service';
|
||||
import { StaffReference } from '../../common/booking-guards';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('staff/users')
|
||||
@@ -12,7 +13,7 @@ export class ListUsersController {
|
||||
constructor(private readonly service: ListUsersService) {}
|
||||
|
||||
@Get()
|
||||
@StaffReference()
|
||||
@BookingStaff(FREIGHT_PERMS.staff.users.view)
|
||||
@ApiOperation({
|
||||
summary: 'List IAM users (paginated) for backoffice pickers',
|
||||
})
|
||||
|
||||
@@ -10,18 +10,19 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { FreightAdmin } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { BackofficeService } from "./backoffice.service";
|
||||
import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto";
|
||||
import { UpdateEmployeeUserRolesDto } from "./dto/update-employee-user-roles.dto";
|
||||
|
||||
@ApiTags("backoffice")
|
||||
@Controller("backoffice")
|
||||
@FreightAdmin()
|
||||
export class BackofficeController {
|
||||
constructor(private readonly backofficeService: BackofficeService) {}
|
||||
|
||||
@Post("organizations/:orgId/users")
|
||||
@BookingStaff([FREIGHT_PERMS.staff.employeeRegistration.create, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Create an organization user without assigning positions" })
|
||||
createOrganizationUser(
|
||||
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||
@@ -31,6 +32,7 @@ export class BackofficeController {
|
||||
}
|
||||
|
||||
@Get("organizations/:orgId/employees")
|
||||
@BookingStaff([FREIGHT_PERMS.staff.roleAssignment.view, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Get deduplicated organization employees for backoffice" })
|
||||
getOrganizationEmployees(
|
||||
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||
@@ -44,6 +46,7 @@ export class BackofficeController {
|
||||
}
|
||||
|
||||
@Get("organizations/:orgId/employee-users/:userId/roles")
|
||||
@BookingStaff([FREIGHT_PERMS.staff.roleAssignment.view, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" })
|
||||
getEmployeeUserRoles(
|
||||
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||
@@ -53,6 +56,7 @@ export class BackofficeController {
|
||||
}
|
||||
|
||||
@Put("organizations/:orgId/employee-users/:userId/roles")
|
||||
@BookingStaff([FREIGHT_PERMS.staff.roleAssignment.replace, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Replace org-scoped roles assigned to an employee user" })
|
||||
replaceEmployeeUserRoles(
|
||||
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||
|
||||
@@ -12,14 +12,15 @@ import type { Response } from "express";
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||
|
||||
import { BookingView } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service";
|
||||
import { BillingService } from "./billing.service";
|
||||
import { FilterInvoiceDto } from "./dto/filter-invoice.dto";
|
||||
|
||||
@ApiTags("billing")
|
||||
@Controller("billing")
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.invoices.view)
|
||||
@ApiBearerAuth()
|
||||
export class BillingController {
|
||||
constructor(
|
||||
@@ -51,6 +52,7 @@ export class BillingController {
|
||||
}
|
||||
|
||||
@Get("invoices/:id/document")
|
||||
@BookingStaff(FREIGHT_PERMS.invoices.export)
|
||||
@ApiOperation({ summary: "Download the sealed invoice PDF" })
|
||||
async document(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.billingService.document(id);
|
||||
@@ -58,6 +60,7 @@ export class BillingController {
|
||||
}
|
||||
|
||||
@Get("invoices/:id/receipt")
|
||||
@BookingStaff(FREIGHT_PERMS.invoices.export)
|
||||
@ApiOperation({ summary: "Download the sealed payment receipt PDF" })
|
||||
async receipt(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.billingService.receipt(id);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import type { Response } from "express";
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
|
||||
import { PortalCustomer } from "../../common/booking-guards";
|
||||
import {
|
||||
type AuthUserPayload,
|
||||
resolveAuthUserId,
|
||||
@@ -28,6 +29,7 @@ import { ConfirmOtpDto, PayInvoiceDto } from "./dto/pay-invoice.dto";
|
||||
@ApiTags("billing")
|
||||
@ApiBearerAuth()
|
||||
@Controller("billing")
|
||||
@PortalCustomer()
|
||||
export class PortalBillingController {
|
||||
constructor(private readonly billingService: BillingService) {}
|
||||
|
||||
|
||||
@@ -279,7 +279,9 @@ export class BookingPricingService {
|
||||
|
||||
return {
|
||||
lineItems,
|
||||
totalAmount: total,
|
||||
// Grand total is billed in whole currency units — fractional line sums
|
||||
// (rate × tons can yield e.g. 260519.2) round to the nearest whole birr/USD.
|
||||
totalAmount: Math.round(total),
|
||||
currency: booking.paymentCurrency,
|
||||
usedRates: [...usedRatesMap.values()],
|
||||
appliedModifiers: ruleResult.appliedModifiers,
|
||||
|
||||
@@ -15,15 +15,15 @@ import {
|
||||
UnauthorizedException,
|
||||
UploadedFile,
|
||||
UploadedFiles,
|
||||
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 {
|
||||
BookingStaff,
|
||||
BookingView,
|
||||
MixedAudience,
|
||||
PortalCustomer,
|
||||
WagonCancellationView,
|
||||
} from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
@@ -167,6 +167,7 @@ export class BookingsController {
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({ summary: "Create a new freight booking (DRAFT)" })
|
||||
@@ -207,6 +208,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
@@ -223,6 +225,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "List freight bookings (paginated)" })
|
||||
async findAll(
|
||||
@Query() filter: FilterBookingDto,
|
||||
@@ -298,6 +301,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get("my")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "List the current customer's bookings ready for payment",
|
||||
description:
|
||||
@@ -328,6 +332,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get("reference-data")
|
||||
@MixedAudience([])
|
||||
@ApiOperation({ summary: "Booking form catalog" })
|
||||
@ApiOkResponse({ type: BookingReferenceDataDto })
|
||||
getReferenceData(): Promise<BookingReferenceDataDto> {
|
||||
@@ -335,6 +340,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get("by-reference/:reference")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Get booking by reference" })
|
||||
async findByReference(
|
||||
@Param("reference") reference: string,
|
||||
@@ -352,6 +358,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Get booking by ID" })
|
||||
async findOne(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -373,6 +380,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/available-days')
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Days bookable for THIS booking (cargo-aware wagon-TYPE gate; days only, no capacity counts)',
|
||||
@@ -395,6 +403,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/day-availability')
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Advisory free-wagon count for a shipment day (planning hint, not enforced). ' +
|
||||
@@ -420,6 +429,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/mile-summary')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({
|
||||
summary: 'First/last-mile operational summary for a booking (customer-safe)',
|
||||
})
|
||||
@@ -447,6 +457,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/customer-truck-assignment')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Customer assigns external truck and driver for terminal pickup' })
|
||||
async assignCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -462,6 +473,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/customer-truck-assignment/freight-order')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Download freight order copies. The 2 gate copies always print; ?copies=1,2,8 adds waybill-style copies (catalog indexes 1-8).',
|
||||
@@ -488,6 +500,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/carriage-acceptance-sheet')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Download the carriage acceptance sheet (one per booking, lists every allocated wagon)',
|
||||
@@ -652,6 +665,11 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/customer-trucks')
|
||||
@MixedAudience([
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.operations,
|
||||
FREIGHT_PERMS.warehouseInventory.view,
|
||||
])
|
||||
@ApiOperation({ summary: 'List customer self-haul trucks (multi-truck) for a booking' })
|
||||
async listCustomerTrucks(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -665,6 +683,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Add a customer self-haul truck carrying 1–2 of the booking containers' })
|
||||
async addCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -679,6 +698,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks/bulk')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Bulk add customer trucks from array payload (Excel parsed)' })
|
||||
async bulkAddCustomerTrucks(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -693,6 +713,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Patch(':id/customer-trucks/:assignmentId')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Edit a not-yet-arrived customer truck (plate/driver/type + containers)' })
|
||||
async updateCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -708,6 +729,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Delete(':id/customer-trucks/:assignmentId')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Remove a not-yet-arrived customer truck from a booking' })
|
||||
async removeCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -722,6 +744,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/customer-trucks/loadable-containers')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: 'Booking containers not yet loaded onto a truck' })
|
||||
async loadableContainers(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -735,6 +758,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks/:assignmentId/load')
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||||
@ApiOperation({ summary: 'Truck_dispatch: load selected containers onto a truck (staff)' })
|
||||
async loadCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -749,6 +773,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks/:assignmentId/depart')
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||||
@ApiOperation({
|
||||
summary: 'Register an import truck leaving: containers loaded + weighed gross (staff)',
|
||||
})
|
||||
@@ -766,6 +791,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/received-pending-grn')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: 'Containers received into port but not yet on a GRN' })
|
||||
async receivedPendingGrn(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -779,6 +805,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/generate-grn')
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Generate a GRN over the received containers (all received, or a subset) — one GRN per batch',
|
||||
@@ -796,6 +823,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/tracking')
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({
|
||||
summary: "Shipment tracking timeline for a booking",
|
||||
description:
|
||||
@@ -818,6 +846,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@MixedAudience([])
|
||||
@HttpCode(204)
|
||||
@ApiOperation({ summary: "Soft-delete DRAFT booking" })
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
@@ -825,6 +854,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/documents")
|
||||
@MixedAudience([])
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({ summary: "Upload documents for a booking (DRAFT only)" })
|
||||
@@ -837,6 +867,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/generate-price")
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary: "Generate price preview (DRAFT or CHANGES_REQUESTED)",
|
||||
description:
|
||||
@@ -848,6 +879,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/submit")
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary: "Customer submit booking",
|
||||
description:
|
||||
@@ -859,6 +891,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/confirm-submit")
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary: "Confirm submit after price change",
|
||||
description:
|
||||
@@ -870,6 +903,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/reject")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "Customer reject price estimate",
|
||||
description:
|
||||
@@ -900,6 +934,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(':id/clearance')
|
||||
@MixedAudience([FREIGHT_PERMS.bookings.clearanceView, FREIGHT_PERMS.bookings.reviewDocuments])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Document-clearance grid (required docs + upload + GL review status)",
|
||||
@@ -909,6 +944,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/clearance/documents")
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
@@ -925,7 +961,10 @@ export class BookingsController {
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
}
|
||||
|
||||
// Customer requests the operation; GL ET also resubmits here on the
|
||||
// customer's behalf after operations requests changes (BookingChangesRequestedAlert).
|
||||
@Post(":id/clearance/proceed")
|
||||
@MixedAudience(FREIGHT_PERMS.contracts.createBooking)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Customer requests operation with a schedule day " +
|
||||
@@ -944,6 +983,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id/export-trains")
|
||||
@MixedAudience([])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Export train picker: the day's export trains on the booking's corridor " +
|
||||
@@ -1145,6 +1185,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/clearance/draft-declaration/accept')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Customer accepts the draft customs declaration — unlocks the real customs declaration step for GL Ethiopia',
|
||||
@@ -1155,6 +1196,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/clearance/draft-declaration/change')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Customer requests a change to the draft customs declaration with a reason — GL Ethiopia sends a corrected draft (repeatable)',
|
||||
@@ -1181,6 +1223,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/clearance/duty-slip')
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Customer uploads duty/tax payment slip on booking' })
|
||||
@@ -1332,7 +1375,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/government-expedite")
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.staffAccept)
|
||||
@BookingStaff(FREIGHT_PERMS.bookings.governmentExpedite)
|
||||
@ApiOperation({
|
||||
summary: "Expedite government booking to PAID / ELIGIBLE for scheduling",
|
||||
})
|
||||
@@ -1356,6 +1399,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id/contract/view")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOkResponse({ type: ContractViewDto })
|
||||
@ApiOperation({ summary: "Contract HTML view for portal and backoffice" })
|
||||
getContractView(
|
||||
@@ -1367,6 +1411,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id/contract/document")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Download contract PDF" })
|
||||
async downloadContractDocument(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -1382,6 +1427,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id/contract")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Download contract file (alias)" })
|
||||
async downloadContract(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -1391,7 +1437,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/contract/sign")
|
||||
@UseGuards(JwtGuard)
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.signStaff)
|
||||
@ApiOperation({ summary: "Apply digital signature (customer or staff)" })
|
||||
async signContract(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -1412,18 +1458,21 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Get(":id/contract/signatures")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "List contract signatures" })
|
||||
getContractSignatures(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.contractService.getSignatures(id);
|
||||
}
|
||||
|
||||
@Get(":id/summary")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Contract summary string for dashboard" })
|
||||
getSummary(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.contractService.getSummary(id);
|
||||
}
|
||||
|
||||
@Post(":id/customer/sign")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "Customer digital signature (deprecated — use POST contract/sign)",
|
||||
})
|
||||
@@ -1504,6 +1553,7 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/cancel-hold")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Customer cancels an unpaid hold (SELECTED_FOR_BATCH → CANCELLED); " +
|
||||
@@ -1518,18 +1568,21 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(":id/consolidation")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Request freight consolidation" })
|
||||
requestConsolidation(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.bookingsService.requestConsolidation(id);
|
||||
}
|
||||
|
||||
@Delete(":id/consolidation")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Remove consolidation pairing" })
|
||||
removeConsolidation(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.bookingsService.removeConsolidation(id);
|
||||
}
|
||||
|
||||
@Get(":id/consolidation")
|
||||
@MixedAudience(FREIGHT_PERMS.bookings.view)
|
||||
@ApiOperation({ summary: "Get consolidation details" })
|
||||
getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.bookingsService.getConsolidationDetails(id);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { BookingLifecycleNotifierService } from './booking-lifecycle-notifier.se
|
||||
import { BookingTransitionService } from './booking-transition.service';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module';
|
||||
import { BookingAllocationController } from './booking-allocation.controller';
|
||||
import { BookingsController } from './bookings.controller';
|
||||
// import { PayController } from './pay.controller';
|
||||
import { BookingsRepository } from './bookings.repository';
|
||||
@@ -92,7 +93,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module";
|
||||
SignaturesModule,
|
||||
registerExchangeModule(),
|
||||
],
|
||||
controllers: [BookingsController],
|
||||
controllers: [BookingsController, BookingAllocationController],
|
||||
providers: [
|
||||
BookingsService,
|
||||
BookingsRepository,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
UploadedFiles,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
@@ -20,8 +19,7 @@ import { AnyFilesInterceptor } from "@nestjs/platform-express";
|
||||
import { ApiOperation, ApiTags, ApiConsumes } from "@nestjs/swagger";
|
||||
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 { BookingStaff } from "../../common/booking-guards";
|
||||
import { BookingStaff, MixedAudience, PortalCustomer } from "../../common/booking-guards";
|
||||
import {
|
||||
assertFreightPermission,
|
||||
hasFreightPermission,
|
||||
@@ -118,6 +116,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("getInfo")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Get company info for the current user" })
|
||||
async getInfo(
|
||||
@CurrentUser() user: CurrentIamUser,
|
||||
@@ -131,6 +130,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("profile")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Get flattened profile for the settings page" })
|
||||
async getProfile(
|
||||
@CurrentUser() user: CurrentIamUser,
|
||||
@@ -146,6 +146,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("profile/change-request")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "Current user's open profile change request (pending/rejected)",
|
||||
})
|
||||
@@ -161,6 +162,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("company-profiles/:profileId/reapply")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "Resubmit a rejected operational role for approval (→ pending)",
|
||||
})
|
||||
@@ -176,6 +178,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("dashboard")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Get portal dashboard KPIs (delivered, spend, freight volume) for the current user",
|
||||
@@ -191,6 +194,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("fetch-etrade-info")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Fetch company info from eTrade by TIN" })
|
||||
async fetchETradeInfo(
|
||||
@CurrentUser() user: CurrentIamUser,
|
||||
@@ -211,6 +215,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Patch("profile")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Update profile (flattened settings page)" })
|
||||
async updateProfile(
|
||||
@CurrentUser() user: CurrentIamUser,
|
||||
@@ -220,6 +225,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("company-profiles")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Add operational profile(s) (importer/exporter/forwarder) to the current user's company",
|
||||
@@ -236,6 +242,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("onboarding/start")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Begin onboarding: create a draft company + profile + role(s) so later steps can save incrementally",
|
||||
@@ -261,6 +268,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("company-profile")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Create a single operational profile for the current user's company. The role starts pending and does not become the active mode",
|
||||
@@ -278,6 +286,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("company-profiles/:profileId/license")
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
@@ -298,6 +307,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("company-profiles/:profileId/license/:fileId/replace")
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
@@ -324,6 +334,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Delete("company-profiles/:profileId/license/:fileId")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Remove a business-license file (staged for review on an approved company).",
|
||||
@@ -341,6 +352,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("company-profiles/:profileId/license")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: "List business-license documents (with review state) for a profile",
|
||||
})
|
||||
@@ -352,6 +364,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("poa-delegation")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"List the Power of Attorney delegation letter (with review state) for the current user's company",
|
||||
@@ -363,6 +376,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("poa-delegation")
|
||||
@PortalCustomer()
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
@@ -383,6 +397,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Delete("poa-delegation/:fileId")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Remove the Power of Attorney delegation letter (staged for review on an approved company).",
|
||||
@@ -395,6 +410,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("identity/fayda/complete")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Bind a completed Fayda verification to the company's owner or Power of Attorney. " +
|
||||
@@ -409,6 +425,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("identity/gm/same-as-owner")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Declare the General Manager is the company's owner, copying the owner's verified identity across. " +
|
||||
@@ -421,6 +438,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Delete("identity/gm")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Clear the General Manager's identity — the \"same as owner\" declaration or a verification, and the details either wrote. " +
|
||||
@@ -433,6 +451,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Delete("identity/fayda/poa")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Remove the company's Power of Attorney — the verified identity, its details and the delegation paper together. " +
|
||||
@@ -445,6 +464,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Patch("onboarding-step")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Persist the user's current onboarding wizard step" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async setOnboardingStep(
|
||||
@@ -455,6 +475,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Get("onboarding/requirements")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"What the current user's company still needs to finish onboarding (server-driven documents + outstanding items)",
|
||||
@@ -466,6 +487,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post("onboarding/complete")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Mark the current user's onboarding as complete" })
|
||||
async completeOnboarding(
|
||||
@CurrentUser() user: CurrentIamUser,
|
||||
@@ -477,6 +499,7 @@ export class CompaniesController {
|
||||
|
||||
// Used by portal
|
||||
@Post("create")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Create a company with its associated external profile (onboarding)",
|
||||
@@ -591,7 +614,11 @@ export class CompaniesController {
|
||||
* permission still needs the applicant's documents.
|
||||
*/
|
||||
@Get(":companyId/documents")
|
||||
@UseGuards(JwtGuard)
|
||||
@MixedAudience([
|
||||
FREIGHT_PERMS.customers.view,
|
||||
FREIGHT_PERMS.contracts.view,
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
])
|
||||
@ApiOperation({ summary: "List documents uploaded for a company" })
|
||||
async listDocuments(
|
||||
@Param("companyId", ParseUUIDPipe) companyId: string,
|
||||
@@ -661,6 +688,7 @@ export class CompaniesController {
|
||||
}
|
||||
|
||||
@Post(":companyId/documents")
|
||||
@MixedAudience(FREIGHT_PERMS.customers.update)
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({ summary: "Upload documents for a company (onboarding)" })
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Controller, Post, Get, Patch, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { ComplianceService } from './compliance.service';
|
||||
import {
|
||||
CreateComplianceRecordDto,
|
||||
@@ -9,10 +11,12 @@ import { ComplianceType } from './entities/compliance-record.entity';
|
||||
|
||||
@ApiTags('Vehicle Compliance')
|
||||
@Controller('compliance')
|
||||
@BookingStaff(FREIGHT_PERMS.compliance.view)
|
||||
export class ComplianceController {
|
||||
constructor(private readonly complianceService: ComplianceService) {}
|
||||
|
||||
@Post()
|
||||
@BookingStaff(FREIGHT_PERMS.compliance.manage)
|
||||
@ApiOperation({ summary: 'Create a compliance record' })
|
||||
create(@Body() dto: CreateComplianceRecordDto) {
|
||||
return this.complianceService.create(dto);
|
||||
@@ -40,12 +44,14 @@ export class ComplianceController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@BookingStaff(FREIGHT_PERMS.compliance.manage)
|
||||
@ApiOperation({ summary: 'Update a compliance record' })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateComplianceRecordDto) {
|
||||
return this.complianceService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@BookingStaff(FREIGHT_PERMS.compliance.manage)
|
||||
@ApiOperation({ summary: 'Soft-delete a compliance record' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.complianceService.remove(id);
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { FreightAdmin } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { ContractTemplatesService } from "./contract-templates.service";
|
||||
import {
|
||||
CreateArticleDto,
|
||||
@@ -25,29 +26,44 @@ import {
|
||||
export class ContractTemplatesController {
|
||||
constructor(private readonly service: ContractTemplatesService) {}
|
||||
|
||||
// Reads stay open to authenticated staff (the backoffice Templates tab);
|
||||
// Reads are staff-only (the backoffice Templates tab is the only consumer);
|
||||
// writes are admin-guarded like other freight configuration resources.
|
||||
|
||||
@Get()
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.settings.contractTemplates.view,
|
||||
FREIGHT_PERMS.settings.contractTemplates.manage,
|
||||
FREIGHT_PERMS.admin,
|
||||
])
|
||||
@ApiOperation({ summary: "List the six contract document templates" })
|
||||
list() {
|
||||
return this.service.list();
|
||||
}
|
||||
|
||||
@Get(":code")
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.settings.contractTemplates.view,
|
||||
FREIGHT_PERMS.settings.contractTemplates.manage,
|
||||
FREIGHT_PERMS.admin,
|
||||
])
|
||||
@ApiOperation({ summary: "Get one contract template by code" })
|
||||
getByCode(@Param("code") code: string) {
|
||||
return this.service.getByCode(code);
|
||||
}
|
||||
|
||||
@Patch(":code")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update template metadata (name, title, recitals, active flag)" })
|
||||
update(@Param("code") code: string, @Body() dto: UpdateContractTemplateDto) {
|
||||
return this.service.update(code, dto);
|
||||
}
|
||||
|
||||
@Post(":code/preview")
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.settings.contractTemplates.view,
|
||||
FREIGHT_PERMS.settings.contractTemplates.manage,
|
||||
FREIGHT_PERMS.admin,
|
||||
])
|
||||
@ApiOperation({
|
||||
summary: "Render an HTML preview of the template against mock contract data",
|
||||
})
|
||||
@@ -61,21 +77,21 @@ export class ContractTemplatesController {
|
||||
/* ------------------------- article routes ------------------------- */
|
||||
|
||||
@Put(":code/articles")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Replace the full ordered article list (used for reorder)" })
|
||||
replaceArticles(@Param("code") code: string, @Body() dto: ReplaceArticlesDto) {
|
||||
return this.service.replaceArticles(code, dto.articles);
|
||||
}
|
||||
|
||||
@Post(":code/articles")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Add an article to the template" })
|
||||
addArticle(@Param("code") code: string, @Body() dto: CreateArticleDto) {
|
||||
return this.service.addArticle(code, dto);
|
||||
}
|
||||
|
||||
@Patch(":code/articles/:articleId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update an article's title or body" })
|
||||
updateArticle(
|
||||
@Param("code") code: string,
|
||||
@@ -86,7 +102,7 @@ export class ContractTemplatesController {
|
||||
}
|
||||
|
||||
@Delete(":code/articles/:articleId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Remove an article from the template" })
|
||||
removeArticle(
|
||||
@Param("code") code: string,
|
||||
|
||||
@@ -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).',
|
||||
@@ -1132,6 +1153,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)',
|
||||
@@ -1144,12 +1166,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);
|
||||
@@ -1283,7 +1307,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({
|
||||
@@ -1310,6 +1334,7 @@ export class ContractsController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/final-invoice/approve')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary: 'Customer approves the drafted final invoice — unlocks the payment slip',
|
||||
})
|
||||
@@ -1324,6 +1349,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' })
|
||||
@@ -1335,10 +1361,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,
|
||||
@@ -1381,6 +1404,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' })
|
||||
@@ -1406,6 +1430,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' })
|
||||
@@ -1422,6 +1447,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);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Controller, Get, UseGuards } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { PermissionGuard } from "@tria-plc/api-common/modules/auth/services/permission.guard";
|
||||
|
||||
@ApiTags("demo-permissions")
|
||||
@Controller()
|
||||
export class DemoPermissionsController {
|
||||
@Get("test_user1")
|
||||
@ApiOperation({ summary: "Permission demo (can:demo:user1)" })
|
||||
@UseGuards(PermissionGuard(["can:demo:user1"]))
|
||||
testUser1() {
|
||||
return { ok: true, permission: "can:demo:user1" };
|
||||
}
|
||||
|
||||
@Get("test_user2")
|
||||
@ApiOperation({ summary: "Permission demo (can:demo:user2)" })
|
||||
@UseGuards(PermissionGuard(["can:demo:user2"]))
|
||||
testUser2() {
|
||||
return { ok: true, permission: "can:demo:user2" };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { DemoPermissionsController } from "./demo-permissions.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [DemoPermissionsController],
|
||||
})
|
||||
export class DemoPermissionsModule {}
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { FreightAdmin } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
|
||||
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
|
||||
import { ListDropdownSettingsQueryDto } from "./dto/list-dropdown-settings-query.dto";
|
||||
@@ -58,14 +59,14 @@ export class DropdownSettingsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Create a new dropdown setting" })
|
||||
create(@Body() dto: CreateDropdownSettingDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update a dropdown setting's metadata" })
|
||||
update(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -75,7 +76,7 @@ export class DropdownSettingsController {
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Soft-delete a dropdown setting" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
@@ -85,7 +86,7 @@ export class DropdownSettingsController {
|
||||
/* ------------------------- option routes ------------------------- */
|
||||
|
||||
@Put(":id/options")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Replace the full option list for a setting" })
|
||||
replaceOptions(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -95,7 +96,7 @@ export class DropdownSettingsController {
|
||||
}
|
||||
|
||||
@Post(":id/options")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Append a single option to a setting" })
|
||||
addOption(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -105,7 +106,7 @@ export class DropdownSettingsController {
|
||||
}
|
||||
|
||||
@Patch("options/:optionId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update a single option" })
|
||||
updateOption(
|
||||
@Param("optionId", ParseUUIDPipe) optionId: string,
|
||||
@@ -115,7 +116,7 @@ export class DropdownSettingsController {
|
||||
}
|
||||
|
||||
@Delete("options/:optionId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Soft-delete a single option" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeOption(@Param("optionId", ParseUUIDPipe) optionId: string) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||
|
||||
import { FreightAdmin } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { UpdateExchangeSettingDto } from "./dto/update-exchange-setting.dto";
|
||||
import { ExchangeSettingsService } from "./exchange-settings.service";
|
||||
|
||||
@@ -14,7 +15,7 @@ export class ExchangeSettingsController {
|
||||
constructor(private readonly service: ExchangeSettingsService) {}
|
||||
|
||||
@Get()
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({
|
||||
summary: "Current USD→ETB fallback rate and CBE feed health",
|
||||
})
|
||||
@@ -32,7 +33,7 @@ export class ExchangeSettingsController {
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Set the USD→ETB fallback by hand (used only while CBE is unreachable)",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { CreateFacilityDto } from './dto/create-facility.dto';
|
||||
import { UpdateFacilityDto } from './dto/update-facility.dto';
|
||||
import { Facility } from './entities/facility.entity';
|
||||
@@ -8,10 +10,12 @@ import { FacilitiesService } from './facilities.service';
|
||||
|
||||
@ApiTags('Facilities')
|
||||
@Controller('facilities')
|
||||
@BookingStaff(FREIGHT_PERMS.facilities.view)
|
||||
export class FacilitiesController {
|
||||
constructor(private readonly facilitiesService: FacilitiesService) {}
|
||||
|
||||
@Post()
|
||||
@BookingStaff(FREIGHT_PERMS.facilities.manage)
|
||||
@ApiOperation({ summary: 'Create a new facility' })
|
||||
async create(@Body() createFacilityDto: CreateFacilityDto): Promise<Facility> {
|
||||
return this.facilitiesService.create(createFacilityDto);
|
||||
@@ -30,6 +34,7 @@ export class FacilitiesController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@BookingStaff(FREIGHT_PERMS.facilities.manage)
|
||||
@ApiOperation({ summary: 'Update a facility' })
|
||||
async update(@Param('id') id: string, @Body() updateFacilityDto: UpdateFacilityDto): Promise<Facility | null> {
|
||||
return this.facilitiesService.update(id, updateFacilityDto);
|
||||
@@ -37,6 +42,7 @@ export class FacilitiesController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
@BookingStaff(FREIGHT_PERMS.facilities.manage)
|
||||
@ApiOperation({ summary: 'Delete a facility (soft delete)' })
|
||||
async remove(@Param('id') id: string): Promise<void> {
|
||||
return this.facilitiesService.remove(id);
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { FreightAdmin } from "../../common/booking-guards";
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { CreateFileUploadFieldDto } from "./dto/create-file-upload-field.dto";
|
||||
import { CreateFileUploadSettingDto } from "./dto/create-file-upload-setting.dto";
|
||||
import { UpdateFileUploadFieldDto } from "./dto/update-file-upload-field.dto";
|
||||
@@ -53,14 +54,14 @@ export class FileUploadSettingsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Create a new file upload setting" })
|
||||
create(@Body() dto: CreateFileUploadSettingDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update a file upload setting's metadata" })
|
||||
update(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -70,7 +71,7 @@ export class FileUploadSettingsController {
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Soft-delete a file upload setting" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
@@ -80,7 +81,7 @@ export class FileUploadSettingsController {
|
||||
/* ------------------------- field routes ------------------------- */
|
||||
|
||||
@Put(":id/fields")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Replace the full field list for a setting" })
|
||||
replaceFields(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -90,7 +91,7 @@ export class FileUploadSettingsController {
|
||||
}
|
||||
|
||||
@Post(":id/fields")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Append a single field to a setting" })
|
||||
addField(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@@ -100,7 +101,7 @@ export class FileUploadSettingsController {
|
||||
}
|
||||
|
||||
@Patch("fields/:fieldId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Update a single field" })
|
||||
updateField(
|
||||
@Param("fieldId", ParseUUIDPipe) fieldId: string,
|
||||
@@ -110,7 +111,7 @@ export class FileUploadSettingsController {
|
||||
}
|
||||
|
||||
@Delete("fields/:fieldId")
|
||||
@FreightAdmin()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({ summary: "Soft-delete a single field" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeField(@Param("fieldId", ParseUUIDPipe) fieldId: string) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@nestjs/swagger";
|
||||
import { Response } from "express";
|
||||
|
||||
import { MixedAudience } from "../../common/booking-guards";
|
||||
import { FilesService } from "./files.service";
|
||||
|
||||
@ApiTags("files")
|
||||
@@ -25,6 +26,7 @@ export class FilesController {
|
||||
constructor(private readonly filesService: FilesService) {}
|
||||
|
||||
@Get(":fileId")
|
||||
@MixedAudience([])
|
||||
// Authenticated: no @Public, so the global JwtGuard applies. Unguessable file
|
||||
// UUIDs are obscurity, not authorization — raw byte streams must require auth.
|
||||
// Browser inline previews (<img>/<iframe>/<a>) that can't carry the Bearer
|
||||
|
||||
@@ -28,7 +28,9 @@ import { FirstMileInvoiceService } from './first-mile-invoice.service';
|
||||
@ApiTags('first-mile')
|
||||
@ApiBearerAuth()
|
||||
@Controller('first-mile')
|
||||
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
||||
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||
// `view` would AND with every action key below and lock out staff granted only
|
||||
// an action (e.g. assign_vehicles). Each route carries its own key instead.
|
||||
export class FirstMileController {
|
||||
constructor(
|
||||
private readonly firstMileService: FirstMileService,
|
||||
@@ -36,6 +38,7 @@ export class FirstMileController {
|
||||
) { }
|
||||
|
||||
@Get()
|
||||
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
||||
@ApiOperation({ summary: 'List first-mile legs' })
|
||||
findAll(
|
||||
@Query('status') status?: string,
|
||||
@@ -58,6 +61,7 @@ export class FirstMileController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
||||
@ApiOperation({ summary: 'Get a first-mile leg by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.firstMileService.findById(id);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
import { LastMileRequestStatus } from '@edr/types';
|
||||
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { BookingStaff, MixedAudience, PortalCustomer } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { ApproveLastMileRequestDto } from './dto/approve-last-mile-request.dto';
|
||||
import { RejectLastMileRequestDto } from './dto/reject-last-mile-request.dto';
|
||||
@@ -61,12 +61,14 @@ export class LastMileRequestsController {
|
||||
// Customer-facing like :id/submit — the service ownership-checks against the
|
||||
// resolved company; staff may also open it (read-only view).
|
||||
@Get(':id/contract/view')
|
||||
@MixedAudience(FREIGHT_PERMS.lastMile.requestView)
|
||||
@ApiOperation({ summary: 'LM contract view model + rendered HTML + saved signature' })
|
||||
contractView(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) {
|
||||
return this.contractService.getContractView(id, user?.id ?? null);
|
||||
}
|
||||
|
||||
@Get(':id/contract/document')
|
||||
@MixedAudience(FREIGHT_PERMS.lastMile.requestView)
|
||||
@ApiOperation({ summary: 'Download the LM contract PDF (LM_<CustomerName>.pdf)' })
|
||||
async contractDocument(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -79,6 +81,7 @@ export class LastMileRequestsController {
|
||||
}
|
||||
|
||||
@Post(':id/contract/sign')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: 'Customer agrees and signs the LM contract — then the advance invoice is issued' })
|
||||
signContract(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@@ -99,6 +102,7 @@ export class LastMileRequestsController {
|
||||
// TODO: integrate @edr/auth — @CurrentUser is a stub until then; the service
|
||||
// still cross-checks the request's booking against the resolved company.
|
||||
@Post(':id/submit')
|
||||
@PortalCustomer()
|
||||
@ApiOperation({ summary: "Customer confirms which containers go via EDR last-mile" })
|
||||
submit(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
|
||||
@@ -34,7 +34,9 @@ import { LastMileInvoiceService } from './last-mile-invoice.service';
|
||||
@ApiTags('last-mile')
|
||||
@ApiBearerAuth()
|
||||
@Controller('last-mile')
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||
// `view` would AND with every action key below and lock out staff granted only
|
||||
// an action (e.g. assign_vehicles). Each route carries its own key instead.
|
||||
export class LastMileController {
|
||||
constructor(
|
||||
private readonly lastMileService: LastMileService,
|
||||
@@ -42,6 +44,7 @@ export class LastMileController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||
@ApiOperation({ summary: 'List last-mile legs' })
|
||||
findAll(
|
||||
@Query('status') status?: string,
|
||||
@@ -64,18 +67,21 @@ export class LastMileController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||
@ApiOperation({ summary: 'Get a last-mile leg by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.lastMileService.findById(id);
|
||||
}
|
||||
|
||||
@Get('booking/:bookingId/arrival-trucks')
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||
@ApiOperation({ summary: "Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill)" })
|
||||
arrivalTrucks(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.lastMileService.arrivalTrucksForBooking(bookingId);
|
||||
}
|
||||
|
||||
@Get('booking/:bookingId/remaining-tons')
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||
@ApiOperation({ summary: 'Bulk drawdown: tonnage still to be hauled (total − departed trucks)' })
|
||||
remainingTons(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.lastMileService.remainingTonsForBooking(bookingId);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
import {
|
||||
NotificationAudience,
|
||||
NotificationPriority,
|
||||
NotificationType,
|
||||
} from "@edr/types";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
@@ -62,22 +56,4 @@ export class NotificationInboxController {
|
||||
return this.service.markAllRead(resolveAuthUserId(user));
|
||||
}
|
||||
|
||||
// TODO: remove before merge — dev/verification helper only.
|
||||
@Post("test")
|
||||
@ApiOperation({
|
||||
summary: "[dev] Send a test notification to the current user",
|
||||
})
|
||||
sendTest(
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
@Body()
|
||||
body: {
|
||||
audience?: NotificationAudience;
|
||||
type?: NotificationType;
|
||||
priority?: NotificationPriority;
|
||||
title?: string;
|
||||
message?: string;
|
||||
},
|
||||
) {
|
||||
return this.service.sendTestToUser(resolveAuthUserId(user), body ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
NotificationAudience,
|
||||
NotificationChannels,
|
||||
NotificationChannelsSent,
|
||||
NotificationDto,
|
||||
NotificationListResult,
|
||||
NotificationPriority,
|
||||
NotificationType,
|
||||
NotifyInput,
|
||||
} from "@edr/types";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
@@ -106,31 +104,6 @@ export class NotificationInboxService {
|
||||
return { updated, unreadCount };
|
||||
}
|
||||
|
||||
/** [dev/verification only] Send a canned notification straight to one user. */
|
||||
async sendTestToUser(
|
||||
userId: string,
|
||||
body: {
|
||||
audience?: NotificationAudience;
|
||||
type?: NotificationType;
|
||||
priority?: NotificationPriority;
|
||||
title?: string;
|
||||
message?: string;
|
||||
},
|
||||
): Promise<NotificationDto> {
|
||||
const entity = await this.repo.create({
|
||||
recipientUserId: userId,
|
||||
audience: body.audience ?? NotificationAudience.BACKOFFICE,
|
||||
type: body.type ?? NotificationType.GENERIC,
|
||||
title: body.title ?? "Test notification",
|
||||
body: body.message ?? "This is a test in-app notification.",
|
||||
priority: body.priority ?? NotificationPriority.NORMAL,
|
||||
isRead: false,
|
||||
});
|
||||
const dto = this.toDto(entity);
|
||||
this.gateway.emitNew(userId, dto, await this.repo.countUnread(userId));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private async deliverToUser(
|
||||
userId: string,
|
||||
input: NotifyInput,
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
|
||||
import { BookingView } from '../../common/booking-guards';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { OverviewQueryDto } from './dto/overview-query.dto';
|
||||
import { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import {
|
||||
@@ -32,7 +33,7 @@ export class OverviewController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Aggregated dashboard summary for backoffice overview' })
|
||||
@ApiOkResponse({ type: OverviewResponseDto })
|
||||
async getDashboard(
|
||||
@@ -48,7 +49,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('bookings')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Bookings tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewBookingsTabDto })
|
||||
async getBookingsTab(
|
||||
@@ -64,7 +65,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('contracts')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Contracts tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewContractsTabDto })
|
||||
async getContractsTab(
|
||||
@@ -80,7 +81,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('billing')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Billing tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewBillingTabDto })
|
||||
async getBillingTab(
|
||||
@@ -96,7 +97,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('operations')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Operations tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewOperationsTabDto })
|
||||
getOperationsTab(
|
||||
@@ -106,7 +107,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('customers')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Customers tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewCustomersTabDto })
|
||||
async getCustomersTab(
|
||||
@@ -122,7 +123,7 @@ export class OverviewController {
|
||||
}
|
||||
|
||||
@Get('staff')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Staff tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewStaffTabDto })
|
||||
getStaffTab(@Query() query: OverviewQueryDto): Promise<OverviewStaffTabDto> {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { Response } from "express";
|
||||
import { CurrentUser, Public } from "@edr/api-common";
|
||||
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||
import { BookingStaff, BookingView } from "../../common/booking-guards";
|
||||
import { BookingStaff, MixedAudience, PortalCustomer } from "../../common/booking-guards";
|
||||
import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { PaymentService } from "./payment.service";
|
||||
@@ -43,14 +43,14 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
@Get("summary")
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.payments.view)
|
||||
@ApiOperation({ summary: "Payment count/amount summary for dashboard cards" })
|
||||
getSummary() {
|
||||
return this.paymentService.getSummary();
|
||||
}
|
||||
|
||||
@Get("all")
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.payments.view)
|
||||
@ApiOperation({ summary: "Get all payments with filters (view-only, any staff)" })
|
||||
@ApiQuery({ name: "search", required: false })
|
||||
@ApiQuery({ name: "status", required: false })
|
||||
@@ -79,6 +79,7 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
@Get("intents/:bookingId")
|
||||
@MixedAudience(FREIGHT_PERMS.payments.view)
|
||||
@ApiOperation({ summary: "Get payment intent status for a booking" })
|
||||
@ApiOkResponse({ type: IntentStatusDto })
|
||||
getIntent(@Param("bookingId") bookingId: string) {
|
||||
@@ -86,6 +87,7 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
@Post("redirect-success/:bookingId")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Success-redirect ack: mark payment processing + invoice PAYMENT_PROCESSING (webhook remains source of truth)",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Controller, Post, Get, Patch, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { ProcurementService } from './procurement.service';
|
||||
import {
|
||||
CreateVendorDto,
|
||||
@@ -11,11 +13,13 @@ import {
|
||||
|
||||
@ApiTags('Procurement & Asset Lifecycle')
|
||||
@Controller('procurement')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.view)
|
||||
export class ProcurementController {
|
||||
constructor(private readonly procurementService: ProcurementService) {}
|
||||
|
||||
// ---- Vendors ----
|
||||
@Post('vendors')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.vendorManage)
|
||||
@ApiOperation({ summary: 'Create a vendor' })
|
||||
async createVendor(@Body() dto: CreateVendorDto) {
|
||||
return this.procurementService.createVendor(dto);
|
||||
@@ -28,12 +32,14 @@ export class ProcurementController {
|
||||
}
|
||||
|
||||
@Patch('vendors/:id')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.vendorManage)
|
||||
@ApiOperation({ summary: 'Update a vendor' })
|
||||
async updateVendor(@Param('id') id: string, @Body() dto: UpdateVendorDto) {
|
||||
return this.procurementService.updateVendor(id, dto);
|
||||
}
|
||||
|
||||
@Delete('vendors/:id')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.vendorManage)
|
||||
@ApiOperation({ summary: 'Delete a vendor' })
|
||||
async deleteVendor(@Param('id') id: string) {
|
||||
return this.procurementService.deleteVendor(id);
|
||||
@@ -41,6 +47,7 @@ export class ProcurementController {
|
||||
|
||||
// ---- Acquisitions ----
|
||||
@Post('acquisitions')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.acquisitionManage)
|
||||
@ApiOperation({ summary: 'Create an asset acquisition' })
|
||||
async createAcquisition(@Body() dto: CreateAcquisitionDto) {
|
||||
return this.procurementService.createAcquisition(dto);
|
||||
@@ -59,12 +66,14 @@ export class ProcurementController {
|
||||
}
|
||||
|
||||
@Patch('acquisitions/:id')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.acquisitionManage)
|
||||
@ApiOperation({ summary: 'Update an asset acquisition' })
|
||||
async updateAcquisition(@Param('id') id: string, @Body() dto: UpdateAcquisitionDto) {
|
||||
return this.procurementService.updateAcquisition(id, dto);
|
||||
}
|
||||
|
||||
@Delete('acquisitions/:id')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.acquisitionManage)
|
||||
@ApiOperation({ summary: 'Delete an asset acquisition' })
|
||||
async deleteAcquisition(@Param('id') id: string) {
|
||||
return this.procurementService.deleteAcquisition(id);
|
||||
@@ -72,6 +81,7 @@ export class ProcurementController {
|
||||
|
||||
// ---- Disposals ----
|
||||
@Post('disposals')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.disposalManage)
|
||||
@ApiOperation({ summary: 'Create an asset disposal' })
|
||||
async createDisposal(@Body() dto: CreateDisposalDto) {
|
||||
return this.procurementService.createDisposal(dto);
|
||||
@@ -84,6 +94,7 @@ export class ProcurementController {
|
||||
}
|
||||
|
||||
@Delete('disposals/:id')
|
||||
@BookingStaff(FREIGHT_PERMS.procurement.disposalManage)
|
||||
@ApiOperation({ summary: 'Delete an asset disposal' })
|
||||
async deleteDisposal(@Param('id') id: string) {
|
||||
return this.procurementService.deleteDisposal(id);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swa
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
|
||||
import { BookingView } from '../../common/booking-guards';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { UserTradeAccessService } from '../user-trade-access/user-trade-access.service';
|
||||
import { ReportQueryDto } from './dto/report-query.dto';
|
||||
import { ReportResultDto } from './dto/report-result.dto';
|
||||
@@ -19,7 +20,7 @@ export class ReportsController {
|
||||
) {}
|
||||
|
||||
@Get(':key')
|
||||
@BookingView()
|
||||
@BookingStaff(FREIGHT_PERMS.reports.view)
|
||||
@ApiOperation({ summary: 'Run a canned report by key with optional filters' })
|
||||
@ApiOkResponse({ type: ReportResultDto })
|
||||
async run(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineApprove, RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { isSuperAdmin } from '../../../common/freight-permission.util';
|
||||
import { CreateRateDto } from '../dto/create-rate.dto';
|
||||
import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
@@ -68,7 +68,7 @@ export class RatesController {
|
||||
}
|
||||
|
||||
@Post(':id/approve')
|
||||
@RuleEngineUpdate('rates')
|
||||
@RuleEngineApprove('rates')
|
||||
@ApiOperation({ summary: 'CEO approves a rate' })
|
||||
approve(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
} from "@nestjs/swagger";
|
||||
import { Response } from "express";
|
||||
|
||||
import { MixedAudience } from "../../common/booking-guards";
|
||||
import {
|
||||
AuthUserPayload,
|
||||
resolveAuthUserId,
|
||||
} from "../../common/resolve-auth-user-id";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { FilesService } from "../files/files.service";
|
||||
import { SupportChatService } from "./support-chat.service";
|
||||
|
||||
@@ -48,6 +50,7 @@ export class SupportAttachmentController {
|
||||
) {}
|
||||
|
||||
@Get(":fileId")
|
||||
@MixedAudience(FREIGHT_PERMS.support.agentView)
|
||||
@ApiOperation({
|
||||
summary: "Download a support chat attachment",
|
||||
description:
|
||||
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
import { FilesInterceptor } from "@nestjs/platform-express";
|
||||
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import {
|
||||
AuthUserPayload,
|
||||
resolveAuthUserId,
|
||||
} from "../../common/resolve-auth-user-id";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import {
|
||||
SUPPORT_ATTACHMENT_FIELD,
|
||||
supportAttachmentMulterOptions,
|
||||
@@ -38,12 +40,14 @@ export class SupportChatAgentController {
|
||||
constructor(private readonly service: SupportChatService) {}
|
||||
|
||||
@Get("conversations")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentView)
|
||||
@ApiOperation({ summary: "List all support threads (shared inbox)" })
|
||||
list(@Query() query: ListConversationsQueryDto) {
|
||||
return this.service.listForAgents(query);
|
||||
}
|
||||
|
||||
@Post("conversations")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentSend)
|
||||
@ApiOperation({
|
||||
summary: "Start chatting with a company (returns the thread if one exists)",
|
||||
})
|
||||
@@ -52,6 +56,7 @@ export class SupportChatAgentController {
|
||||
}
|
||||
|
||||
@Get("conversations/:id/messages")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentView)
|
||||
@ApiOperation({
|
||||
summary: "List messages in a thread (newest page first)",
|
||||
description:
|
||||
@@ -67,6 +72,7 @@ export class SupportChatAgentController {
|
||||
}
|
||||
|
||||
@Post("conversations/:id/messages")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentSend)
|
||||
@UseInterceptors(
|
||||
FilesInterceptor(
|
||||
SUPPORT_ATTACHMENT_FIELD,
|
||||
@@ -106,6 +112,7 @@ export class SupportChatAgentController {
|
||||
}
|
||||
|
||||
@Post("conversations/:id/read")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentView)
|
||||
@ApiOperation({ summary: "Mark a thread read (agent side)" })
|
||||
read(
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
@@ -115,6 +122,7 @@ export class SupportChatAgentController {
|
||||
}
|
||||
|
||||
@Get("unread-count")
|
||||
@BookingStaff(FREIGHT_PERMS.support.agentView)
|
||||
@ApiOperation({ summary: "Count unread threads (agent side)" })
|
||||
unread(@CurrentUser() user: AuthUserPayload) {
|
||||
return this.service.unreadCount(
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SUPPORT_ATTACHMENT_FIELD,
|
||||
supportAttachmentMulterOptions,
|
||||
} from "./attachment-upload.options";
|
||||
import { PortalCustomer } from "../../common/booking-guards";
|
||||
import { ListMessagesQueryDto } from "./dto/list-messages-query.dto";
|
||||
import { SendMessageDto } from "./dto/send-message.dto";
|
||||
import { SupportChatService } from "./support-chat.service";
|
||||
@@ -34,6 +35,7 @@ import { SupportChatService } from "./support-chat.service";
|
||||
*/
|
||||
@ApiTags("support-chat")
|
||||
@Controller("support")
|
||||
@PortalCustomer()
|
||||
export class SupportChatController {
|
||||
constructor(private readonly service: SupportChatService) {}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
import {
|
||||
BookingDocReviewAlert,
|
||||
BookingStaff,
|
||||
MixedAudience,
|
||||
PortalCustomer,
|
||||
TrainSchedulingCancel,
|
||||
TrainSchedulingCreate,
|
||||
TrainSchedulingReschedule,
|
||||
@@ -17,6 +20,7 @@ import {
|
||||
TrainSchedulingUpdate,
|
||||
TrainSchedulingView,
|
||||
} from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { AcceptIntercityBookingsDto } from "./dto/accept-intercity-bookings.dto";
|
||||
import { AssignBookingsDto } from "./dto/assign-bookings.dto";
|
||||
import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto";
|
||||
@@ -71,6 +75,7 @@ export class TrainSchedulingController {
|
||||
) { }
|
||||
|
||||
@Get("my-booking-windows")
|
||||
@PortalCustomer()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Upcoming/open booking windows announced to the signed-in customer (all window-engine schedules; their own contract lanes carry a Book-now target)",
|
||||
@@ -85,6 +90,10 @@ export class TrainSchedulingController {
|
||||
}
|
||||
|
||||
@Get("contracts/:contractId/booking-windows")
|
||||
@MixedAudience([
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
FREIGHT_PERMS.contracts.createBooking,
|
||||
])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Upcoming/open booking windows on a contract's routes — gates the booking form for customer + Ethiopian GL",
|
||||
@@ -496,14 +505,14 @@ export class TrainSchedulingController {
|
||||
}
|
||||
|
||||
@Post("schedules/:id/finalize")
|
||||
@TrainSchedulingUpdate()
|
||||
@BookingStaff(FREIGHT_PERMS.trainScheduling.dispatch)
|
||||
@ApiOperation({ summary: "Finalize a draft train schedule" })
|
||||
finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.finalizeSchedule(id);
|
||||
}
|
||||
|
||||
@Post("schedules/:id/dispatch")
|
||||
@TrainSchedulingUpdate()
|
||||
@BookingStaff(FREIGHT_PERMS.trainScheduling.dispatch)
|
||||
@ApiOperation({ summary: "Dispatch a scheduled train" })
|
||||
dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.dispatchSchedule(id);
|
||||
@@ -824,7 +833,7 @@ export class TrainSchedulingController {
|
||||
}
|
||||
|
||||
@Post("bookings/:bookingId/mark-paid")
|
||||
@TrainSchedulingUpdate()
|
||||
@BookingStaff(FREIGHT_PERMS.trainScheduling.markPaid)
|
||||
@ApiOperation({
|
||||
summary: "Staff: mark a reserved booking paid and allocate it now",
|
||||
})
|
||||
@@ -834,7 +843,7 @@ export class TrainSchedulingController {
|
||||
}
|
||||
|
||||
@Post("bookings/:bookingId/expire")
|
||||
@TrainSchedulingUpdate()
|
||||
@BookingStaff(FREIGHT_PERMS.trainScheduling.expireBooking)
|
||||
@ApiOperation({
|
||||
summary: "Staff: expire a reservation and free its capacity",
|
||||
})
|
||||
|
||||
@@ -11,8 +11,9 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
|
||||
import { StaffReference } from '../../common/booking-guards';
|
||||
import { BookingStaff, StaffReference } from '../../common/booking-guards';
|
||||
import { isFreightApprovalAdmin } from '../../common/freight-permission.util';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { UpsertUserTradeAccessDto } from './dto/upsert-user-trade-access.dto';
|
||||
import { UserTradeAccessService } from './user-trade-access.service';
|
||||
|
||||
@@ -24,6 +25,7 @@ export class UserTradeAccessController {
|
||||
constructor(private readonly service: UserTradeAccessService) {}
|
||||
|
||||
@Get()
|
||||
@BookingStaff(FREIGHT_PERMS.tradeAccess.view)
|
||||
@ApiOperation({ summary: 'List every configured user trade-direction scope' })
|
||||
list(@CurrentUser() user: TCurrentUser) {
|
||||
this.assertAdmin(user);
|
||||
@@ -41,6 +43,7 @@ export class UserTradeAccessController {
|
||||
}
|
||||
|
||||
@Put(':userId')
|
||||
@BookingStaff(FREIGHT_PERMS.tradeAccess.manage)
|
||||
@ApiOperation({
|
||||
summary: 'Set the trade directions a backoffice user may see',
|
||||
})
|
||||
|
||||
@@ -41,7 +41,9 @@ const toInt = (value?: string): number | undefined => {
|
||||
*/
|
||||
@ApiTags('wagon-transfer-requests')
|
||||
@Controller('wagon-transfer-requests')
|
||||
@WagonTransferView()
|
||||
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||
// `transfer_view` would AND with every action key below and lock out the OCC
|
||||
// staff granted only `transfer_fulfill`. Reads carry the view key themselves.
|
||||
export class WagonTransferRequestsController {
|
||||
constructor(private readonly service: WagonTransferRequestsService) {}
|
||||
|
||||
@@ -56,6 +58,7 @@ export class WagonTransferRequestsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@WagonTransferView()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Transfer desk list — paginated, filterable by status (comma-separated), yards and wagon type',
|
||||
@@ -84,6 +87,7 @@ export class WagonTransferRequestsController {
|
||||
// matches in declaration order, so `/history` would otherwise be captured by
|
||||
// the `:id` param route (and rejected by ParseUUIDPipe).
|
||||
@Get('history')
|
||||
@WagonTransferView()
|
||||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'pageSize', required: false })
|
||||
@ApiOperation({
|
||||
@@ -129,6 +133,7 @@ export class WagonTransferRequestsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@WagonTransferView()
|
||||
@ApiOperation({ summary: 'Get one transfer request' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/curre
|
||||
import {
|
||||
BookingStaff,
|
||||
FleetManage,
|
||||
StaffReference,
|
||||
FleetView,
|
||||
} from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { CreateWagonDto } from './dto/create-wagon.dto';
|
||||
@@ -44,7 +44,7 @@ export class WagonsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@StaffReference()
|
||||
@FleetView(FREIGHT_PERMS.wagons.view)
|
||||
@ApiOperation({
|
||||
summary: 'List wagons, paginated ({items, meta}) — 10 per page by default',
|
||||
})
|
||||
@@ -53,14 +53,14 @@ export class WagonsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@StaffReference()
|
||||
@FleetView(FREIGHT_PERMS.wagons.view)
|
||||
@ApiOperation({ summary: 'Get a wagon by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.wagonsService.findById(id);
|
||||
}
|
||||
|
||||
@Get(':id/movements')
|
||||
@StaffReference()
|
||||
@FleetView(FREIGHT_PERMS.wagons.view)
|
||||
@ApiOperation({
|
||||
summary: "Wagon movement ledger (loaded legs, empty repositions, manual moves), newest first",
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
|
||||
import { actorLabel } from './current-actor.util';
|
||||
import { BookingStaff, StaffReference } from '../../common/booking-guards';
|
||||
import { BookingStaff, MixedAudience } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { BulkReceiveDto } from './dto/bulk-receive.dto';
|
||||
import { BulkInspectDto } from './dto/bulk-inspect.dto';
|
||||
@@ -457,7 +457,7 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Get(':id/handover-document')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'View import goods handover document PDF' })
|
||||
async handoverDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.inventoryService.handoverDocument(id);
|
||||
@@ -468,7 +468,7 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/approve-delivery')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.deliver)
|
||||
@ApiOperation({ summary: "Approve delivery — customer records their full name (signature optional)" })
|
||||
approveDeliveryForBooking(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@@ -484,14 +484,14 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/handovers')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'Handover records for a booking (per-booking or per-truck)' })
|
||||
bookingHandovers(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.handoverService.list(bookingId);
|
||||
}
|
||||
|
||||
@Post('handovers/:handoverId/sign')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.deliver)
|
||||
@ApiOperation({ summary: 'Customer signs one handover (EDR last-mile: one signature per truck)' })
|
||||
signHandover(
|
||||
@Param('handoverId', ParseUUIDPipe) handoverId: string,
|
||||
@@ -507,14 +507,14 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Post('bookings/:bookingId/request-handover-signature')
|
||||
@StaffReference()
|
||||
@BookingStaff(FREIGHT_PERMS.warehouseInventory.deliver)
|
||||
@ApiOperation({ summary: 'Ask the customer to sign the handover (creates one if none, then notifies)' })
|
||||
requestHandoverSignature(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.handoverService.requestSignature(bookingId);
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/grn-document')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'View GRN PDF for a booking (customer portal)' })
|
||||
async bookingGrnDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.inventoryService.grnDocumentForBooking(bookingId);
|
||||
@@ -525,7 +525,7 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/release-document')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'View gate-clearance / release-order PDF for a booking (customer portal)' })
|
||||
async bookingReleaseDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.inventoryService.releaseDocumentForBooking(bookingId);
|
||||
@@ -536,7 +536,7 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/handover-document')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'View import goods handover document PDF (resolved by booking; ?handoverId= for the per-truck variant)' })
|
||||
async bookingHandoverDocument(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@@ -571,21 +571,21 @@ export class WarehouseInventoryController {
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/container-items')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'Per-container/bulk items of a booking with lifecycle stage + refs' })
|
||||
containerItems(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.inventoryService.containerItems(bookingId);
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/container-weights')
|
||||
@StaffReference()
|
||||
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: "A booking's containers + VGM cargo weight (tonnes) for exit weighing" })
|
||||
containerWeights(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.inventoryService.bookingContainerWeights(bookingId);
|
||||
}
|
||||
|
||||
@Get('bookings/:bookingId/location')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: "Warehouse location of a booking's inventory (customer portal)" })
|
||||
bookingLocation(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.inventoryService.bookingLocation(bookingId);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CurrentUser } from '@edr/api-common';
|
||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||
|
||||
import { actorLabel } from './current-actor.util';
|
||||
import { BookingStaff, StaffReference } from '../../common/booking-guards';
|
||||
import { BookingStaff, MixedAudience } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { PayInvoiceDto as GatewayPayInvoiceDto } from '../billing/dto/pay-invoice.dto';
|
||||
import { GenerateInvoiceDto, PayInvoiceBodyDto } from './dto/invoice.dto';
|
||||
@@ -43,7 +43,7 @@ export class WarehouseInvoiceController {
|
||||
}
|
||||
|
||||
@Get('bookings/:id/warehouse-fee-invoices')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseFeeInvoices.view)
|
||||
@ApiOperation({ summary: 'List warehouse fee invoices for a booking' })
|
||||
listForBooking(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoiceService.listForBooking(id);
|
||||
@@ -71,14 +71,14 @@ export class WarehouseInvoiceController {
|
||||
}
|
||||
|
||||
@Get('warehouse-fee-invoices/:id')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseFeeInvoices.view)
|
||||
@ApiOperation({ summary: 'Get a warehouse fee invoice with items + payment history' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoiceService.findById(id);
|
||||
}
|
||||
|
||||
@Get('warehouse-fee-invoices/:id/document')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseFeeInvoices.view)
|
||||
@ApiOperation({ summary: 'Download sealed warehouse fee invoice PDF' })
|
||||
async document(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.invoiceService.document(id);
|
||||
@@ -89,7 +89,7 @@ export class WarehouseInvoiceController {
|
||||
}
|
||||
|
||||
@Get('warehouse-fee-invoices/:id/receipt')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseFeeInvoices.view)
|
||||
@ApiOperation({ summary: 'Download sealed warehouse fee payment receipt PDF' })
|
||||
async receipt(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
|
||||
const { filename, buffer } = await this.invoiceService.receipt(id);
|
||||
@@ -114,7 +114,7 @@ export class WarehouseInvoiceController {
|
||||
}
|
||||
|
||||
@Post('warehouse-fee-invoices/:id/pay-online')
|
||||
@StaffReference()
|
||||
@MixedAudience(FREIGHT_PERMS.warehouseFeeInvoices.pay)
|
||||
@ApiOperation({ summary: 'Initiate Telebirr/Waafi payment for a warehouse fee invoice' })
|
||||
payOnline(@Param('id', ParseUUIDPipe) id: string, @Body() dto: GatewayPayInvoiceDto) {
|
||||
return this.invoiceService.initiatePayment(id, dto);
|
||||
|
||||
@@ -14,13 +14,19 @@ import { WarehousesService } from './warehouses.service';
|
||||
@ApiTags('warehouses')
|
||||
@ApiBearerAuth()
|
||||
// Baseline read: warehouse reference data is consumed by inventory/dashboard
|
||||
// flows too, so any of the three view permissions grants reads. Writes stack
|
||||
// their specific create/update permission per route on top.
|
||||
// flows too, so any of the view permissions grants reads. Writes stack their
|
||||
// specific create/update permission per route on top — which means every key
|
||||
// used by a route below must also appear here, or the class guard denies
|
||||
// before the route's own key is ever consulted (Nest ANDs the two).
|
||||
@Controller('warehouses')
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.warehouses.view,
|
||||
FREIGHT_PERMS.warehouseInventory.view,
|
||||
FREIGHT_PERMS.warehouseDashboard.view,
|
||||
FREIGHT_PERMS.warehouses.create,
|
||||
FREIGHT_PERMS.warehouses.update,
|
||||
FREIGHT_PERMS.warehouseYards.view,
|
||||
FREIGHT_PERMS.warehouseYards.create,
|
||||
])
|
||||
export class WarehousesController {
|
||||
constructor(
|
||||
|
||||
Reference in New Issue
Block a user