import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import type { Response } from "express"; import { BookingStaff } from "../../common/booking-guards"; import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { sendPdf } from "../billing/billing.controller"; import { BulkCancelEimsRegistrationDto } from "./dto/bulk-cancel-eims-registration.dto"; import { CancelEimsRegistrationDto } from "./dto/cancel-eims-registration.dto"; import { RegisterSalesReceiptDto } from "./dto/register-sales-receipt.dto"; import { RegisterWithholdingReceiptDto } from "./dto/register-withholding-receipt.dto"; import { ResolveEimsRegistrationDto } from "./dto/resolve-eims-registration.dto"; import { EimsCancellationService } from "./eims-cancellation.service"; import { EimsInvoiceRegistrationService } from "./eims-invoice-registration.service"; import { EimsReceiptService } from "./eims-receipt.service"; /** * Manual EIMS actions on an existing invoice. * * Invoices are produced by the freight workflow, not by a person, so these routes are **not** the * normal production path — they exist for controlled testing and exceptional operations. Automatic * submission after an invoice is issued is a separate phase; nothing here is called by it. * * `eims_register`, `eims_resolve`, `eims_cancel` and `eims_receipt_register` are intentionally left * out of every role preset and assigned to named admins instead. They are also separate * permissions: resolving clears the system-wide chain block and can record an IRN against an * invoice, cancelling is its own irreversible-at-MoR action, and filing a receipt is a third — * none follows from the right to register. Only `eims/status` and `eims/receipts` ride on the * ordinary `invoices:view`. * * Filing gets its own permission (`invoices:eims_register`) rather than riding on an existing key: * registration is irreversible at MoR, so it must not follow from the right to download a PDF. * The key is seeded through FINANCE_PERMISSIONS, which reaches `iam.permissions` via * ADVANCED_BACKOFFICE_PERMISSIONS → BOOKING_RULE_ENGINE_PERMISSIONS → EDR_FREIGHT_PERMISSIONS. */ @ApiTags("eims") @ApiBearerAuth() @Controller("invoices") export class EimsInvoiceController { constructor( private readonly registration: EimsInvoiceRegistrationService, private readonly cancellation: EimsCancellationService, private readonly receipts: EimsReceiptService, ) {} @Post(":id/eims/register") @BookingStaff(FREIGHT_PERMS.invoices.eimsRegister) @ApiOperation({ summary: "Register the invoice with MoR EIMS. Idempotent — an invoice that already has an IRN is returned unchanged.", }) register(@Param("id", ParseUUIDPipe) id: string) { return this.registration.registerInvoiceWithEims(id); } @Post(":id/eims/verify") @BookingStaff(FREIGHT_PERMS.invoices.eimsRegister) @ApiOperation({ summary: "Verify the invoice's stored IRN against EIMS" }) verify(@Param("id", ParseUUIDPipe) id: string) { return this.registration.verifyInvoiceWithEims(id); } @Post(":id/eims/resolve") @BookingStaff(FREIGHT_PERMS.invoices.eimsResolve) @ApiOperation({ summary: "Resolve an unacknowledged submission: record the IRN confirmed with MoR, or discard it. Clears the system-wide block.", }) resolve( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ResolveEimsRegistrationDto, ) { return this.registration.resolveEimsRegistration(id, dto); } @Get(":id/eims/status") @BookingStaff(FREIGHT_PERMS.invoices.view) @ApiOperation({ summary: "EIMS registration status, IRN and last error for the invoice" }) status(@Param("id", ParseUUIDPipe) id: string) { return this.registration.getEimsStatus(id); } @Post(":id/eims/cancel") @BookingStaff(FREIGHT_PERMS.invoices.eimsCancel) @ApiOperation({ summary: "Cancel the invoice's registered EIMS document. Refuses (409) an already-cancelled invoice rather than a silent no-op — see IRC-N010.", }) cancel(@Param("id", ParseUUIDPipe) id: string, @Body() dto: CancelEimsRegistrationDto) { return this.cancellation.cancelInvoiceWithEims(id, dto.reasonCode, dto.remark); } @Post("eims/bulk-cancel") @BookingStaff(FREIGHT_PERMS.invoices.eimsCancel) @ApiOperation({ summary: "Cancel multiple invoices' registered EIMS documents in one call. Each invoice's outcome is " + "reported independently — one failure never blocks the rest.", }) bulkCancel(@Body() dto: BulkCancelEimsRegistrationDto) { return this.cancellation.cancelBulkWithEims(dto.items); } @Post(":id/eims/receipt/sales") @BookingStaff(FREIGHT_PERMS.invoices.eimsReceiptRegister) @ApiOperation({ summary: "Register a sales receipt with MoR EIMS against a registered invoice" }) registerSalesReceipt(@Param("id", ParseUUIDPipe) id: string, @Body() dto: RegisterSalesReceiptDto) { return this.receipts.registerSalesReceipt(id, dto); } @Post(":id/eims/receipt/withholding") @BookingStaff(FREIGHT_PERMS.invoices.eimsReceiptRegister) @ApiOperation({ summary: "Register a withholding receipt with MoR EIMS against a registered invoice" }) registerWithholdingReceipt( @Param("id", ParseUUIDPipe) id: string, @Body() dto: RegisterWithholdingReceiptDto, ) { return this.receipts.registerWithholdingReceipt(id, dto); } @Get(":id/eims/receipts") @BookingStaff(FREIGHT_PERMS.invoices.view) @ApiOperation({ summary: "List every EIMS receipt filed against this invoice, newest first" }) listReceipts(@Param("id", ParseUUIDPipe) id: string) { return this.receipts.listReceipts(id); } @Get(":id/eims/receipts/:receiptId/document") @BookingStaff(FREIGHT_PERMS.invoices.export) @ApiOperation({ summary: "Download the sealed receipt PDF (RRN + QR) for a filed EIMS receipt" }) async receiptDocument( @Param("id", ParseUUIDPipe) id: string, @Param("receiptId", ParseUUIDPipe) receiptId: string, @Res() res: Response, ) { const { filename, buffer } = await this.receipts.document(id, receiptId); sendPdf(res, filename, buffer); } }