Files
edr-platform/apps/edr-freight-api/src/modules/eims/eims-invoice.controller.ts
Hagernesh 7573019038 feat(eims): register invoices with MoR EIMS and persist the outcome
Add manual single-invoice registration, verification and reconciliation.
Nothing submits automatically; invoice creation is untouched.

Sequencing uses a durable reservation. The counter is consumed and the
holder recorded in a committed transaction before the request leaves the
process, and the HTTP call runs outside every transaction. A counter is
therefore never reused once an attempt begins, a crash mid-flight leaves the
reservation standing instead of inviting a blind resubmission, and an
ambiguous result blocks the whole system number rather than one invoice --
PreviousIrn is unknown, so any later document would chain to a stale IRN.

Deterministic rejections (400/406/401/403) mark the invoice FAILED and clear
the block. Timeouts and 5xx mark it UNKNOWN and keep it. Since /v1/verify
takes an IRN we never received in that case, POST :id/eims/resolve is the
exit: record the IRN confirmed in the MoR portal, or discard. A recorded IRN
is verified against the gateway first and refused unless EIMS reports it
against this invoice's document number.

Business and tax configuration is validated locally before anything is
locked, allocated or sent, so a missing tax code fails naming the exact
environment variables instead of at the gateway. No tax value is defaulted.

Filing gets its own permission (invoices:eims_register) rather than riding
on invoices:export -- registration is irreversible at MoR and must not
follow from the right to download a PDF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00

61 lines
2.4 KiB
TypeScript

import { Body, Controller, Get, Param, ParseUUIDPipe, Post } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import { BookingStaff } from "../../common/booking-guards";
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
import { ResolveEimsRegistrationDto } from "./dto/resolve-eims-registration.dto";
import { EimsInvoiceRegistrationService } from "./eims-invoice-registration.service";
/**
* Staff-triggered EIMS actions on an existing invoice. Registration is manual and one invoice at a
* time — nothing in invoice creation submits automatically.
*
* 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) {}
@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.eimsRegister)
@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);
}
}