feat(eims): implement POST /v1/bulkCancel

New endpoint: POST invoices/eims/bulk-cancel, body { items: [{invoiceId,
reasonCode, remark?}] }. Same eimsCancel permission as single cancel — a
batch-scale version of the same irreversible-at-MoR action, not a new
capability.

Same local-eligibility doctrine as single cancel: an already-cancelled or
never-registered invoice is refused right here, no HTTP call, before it
gets a seat in the batch. Only genuinely eligible invoices go into the one
/v1/bulkCancel request; every outcome (local refusal or MoR's own
per-IRN result) is reported back independently — one invoice failing
never blocks the rest.

MoR's bulk response mixes success and error shapes in the same array,
disambiguated by Status (capital, error) vs status (lowercase, success)
— matched back to our invoices by IRN. Notably the bulk success shape
carries no cancellationDate at all, unlike single cancel.

Left out of this pass: bulkRegister. It's async (returns only a
conversationId immediately, results arrive via a webhook callback we
don't have yet) and needs manual counter/previousIrn management per
the collection's own docs — a materially different reservation model
than today's single-invoice TX1/TX2 pattern. Scoping that is a
separate, bigger piece of work.
This commit is contained in:
Hagernesh
2026-08-17 17:33:58 +00:00
parent 8ed5642544
commit 7d8ab932c2
5 changed files with 294 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ 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";
@@ -89,6 +90,17 @@ export class EimsInvoiceController {
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" })