Files
edr-platform/apps/edr-freight-api/src/modules/eims/eims.module.ts
Hagernesh fc55f48371 feat(eims): implement POST /v1/bulkRegister
New endpoints:
  POST invoices/eims/bulk-register  { invoiceIds: [...] }  — trigger
  POST eims/webhook/bulk-register                          — MoR's callback

Fundamentally different shape from single register: bulkRegister
answers only {conversationId, status:202} immediately: MoR processes
the array asynchronously and pushes the real per-invoice results
(a mix of accepted/rejected in one array, per the collection's own
examples) to a webhook configured out of band. So this ships as two
halves that don't share a call stack — EimsBulkRegistrationService.
registerBulk() reserves a contiguous block of counters (durable
reservation, same doctrine as single register, extended to N items)
and submits; handleBulkCallback(), invoked by the new
EimsWebhookController whenever MoR gets around to it, settles.

New EimsSystemState.inFlightConversationId is the bulk equivalent of
inFlightInvoiceId — a whole batch outstanding, not one invoice — and
the two markers block each other since they share the same counter
sequence. The conversation id isn't known until MoR's 202 arrives, so
reservation stamps a locally-generated placeholder first (same
commit-before-the-network-call reasoning as single register), then
swaps it for MoR's real id right after — the only value the callback
can actually use to find the batch again.

Only the first invoice in a bulk batch chains via PreviousIrn — every
other item gets an empty string, matching the collection's own
two-invoice example exactly (MoR doesn't expect a batch to chain to
IRNs that don't exist yet at submission time).

Webhook has no auth (MoR has no JWT to send) — the conversation id
embedded in the payload is what stands between this and a forged
callback: an item only ever touches an invoice actually holding that
exact id, and an unknown id is logged and ignored, never applied.

Migration 3580000000000: eims_system_state.in_flight_conversation_id,
invoices.eims_bulk_conversation_id (tags which batch an invoice was
submitted in, so a stuck batch — webhook never arrived — can be found
and reconciled by conversation id). Applied to dev DB and recorded in
freight.migrations directly (idempotent IF NOT EXISTS DDL).

Not live-testable from this sandbox (no route to MoR's real gateway).
Signing the whole array as one envelope, the way single /v1/register
was confirmed live to need despite the collection's raw example
showing no envelope, is the reasonable extension of that confirmed
behavior, not a blind guess — but it has not itself been exercised
against the real gateway. Left for the first live bulk attempt to
confirm, same as every other MoR-facing assumption this integration
has made.
2026-08-19 18:35:09 +00:00

68 lines
3.0 KiB
TypeScript

import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Invoice } from "../billing/entities/invoice.entity";
import { DocumentsModule } from "../billing/documents/documents.module";
import { CompaniesModule } from "../companies/companies.module";
import { NotificationInboxModule } from "../notification-inbox/notification-inbox.module";
import { NotificationsModule } from "../notifications/notifications.module";
import { EimsAuthService } from "./eims-auth.service";
import { EimsAutoSubmitService } from "./eims-auto-submit.service";
import { EimsBulkRegistrationService } from "./eims-bulk-registration.service";
import { EimsCancellationService } from "./eims-cancellation.service";
import { EimsClientService } from "./eims-client.service";
import { EimsCredentialsProvider } from "./eims-credentials.provider";
import { EimsInvoiceController } from "./eims-invoice.controller";
import { EimsInvoiceRegistrationService } from "./eims-invoice-registration.service";
import { EimsReceiptService } from "./eims-receipt.service";
import { EimsSellerCacheService } from "./eims-seller-cache.service";
import { EimsSignerService } from "./eims-signer.service";
import { EimsWebhookController } from "./eims-webhook.controller";
import { EimsReceipt } from "./entities/eims-receipt.entity";
import { EimsSystemState } from "./entities/eims-system-state.entity";
/**
* MoR EIMS e-invoicing: signed transport, authentication, and manual single-invoice registration.
*
* Exports only what other modules will consume; the credential loader and signer stay internal so
* the private key has exactly one user. Nothing here is called from invoice creation.
*/
@Module({
imports: [
HttpModule.register({ timeout: Number(process.env.EIMS_HTTP_TIMEOUT_MS) || 30_000 }),
TypeOrmModule.forFeature([EimsSystemState, Invoice, EimsReceipt]),
NotificationInboxModule,
NotificationsModule,
// For EimsReceiptService.document() — the shared sealed invoice/receipt PDF layout. No domain
// deps of its own (StampSettingsService/LogoSettingsService are both @Global), so no cycle.
DocumentsModule,
// For EimsSellerCacheService's ETradeService — CompaniesModule has a forwardRef cycle with
// ShippingLineCompaniesModule -> BillingModule, but nothing in that chain imports EimsModule,
// so this stays a plain one-directional import, not a new cycle.
CompaniesModule,
],
controllers: [EimsInvoiceController, EimsWebhookController],
providers: [
EimsCredentialsProvider,
EimsSignerService,
EimsAuthService,
EimsClientService,
EimsInvoiceRegistrationService,
EimsBulkRegistrationService,
EimsAutoSubmitService,
EimsCancellationService,
EimsReceiptService,
EimsSellerCacheService,
],
exports: [
EimsAuthService,
EimsClientService,
EimsInvoiceRegistrationService,
EimsBulkRegistrationService,
EimsCancellationService,
EimsReceiptService,
],
})
export class EimsModule {}