import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { InjectDataSource } from "@nestjs/typeorm"; import { DataSource, EntityManager } from "typeorm"; import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity.js"; import { EimsConfig } from "../../config/eims.config"; import { Invoice } from "../billing/entities/invoice.entity"; import { EimsDocumentType, EimsInvoiceRequest, EimsMapperLine, toEimsInvoice, } from "../billing/eims-invoice.mapper"; import { NotificationAudience, NotificationPriority, NotificationType } from "@edr/types"; import { NotificationInboxService } from "../notification-inbox/notification-inbox.service"; import { NotificationsService } from "../notifications/notifications.service"; import { sendCompanyChannels } from "../notifications/notify-company.util"; import { toEimsInvoiceStatusView } from "./eims-invoice-view.util"; import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { EimsAuthService } from "./eims-auth.service"; import { EimsClientService } from "./eims-client.service"; import { EimsApiException } from "./eims.errors"; import { EimsSystemState } from "./entities/eims-system-state.entity"; import { assertEimsInvoiceConfig, buildEimsContext, buildEimsSeller, } from "./eims-invoice-context"; import { EimsInvoiceError, EimsInvoiceStatus, EimsInvoiceStatusView, EimsRegisterResponse, EimsVerifyRequest, EimsVerifyResponse, } from "./eims-registration.types"; /** * Failure kinds where the gateway gave a complete answer: the document was rejected and is * definitively not registered. These clear the system-wide block; anything else does not. */ const DETERMINISTIC_KINDS = new Set(["SCHEMA_VALIDATION", "RULE_VALIDATION", "AUTH", "FORBIDDEN"]); interface Reservation { stateId: string; invoiceCounter: number; /** MoR requires a plain integer here, so it cannot be our own `invoiceNumber`. */ documentNumber: string; previousIrn: string; } /** * Registers a single invoice with MoR EIMS. * * Sequencing is a **durable reservation**: the counter is consumed and the holder recorded in a * committed transaction *before* the request leaves the process, and the network call happens * outside any transaction. That gives three properties the naive design could not: * * - a counter is never reused once an attempt has begun, even across a crash; * - a crash mid-flight leaves the reservation standing, so nothing blindly resubmits a document * that may already have reached MoR; * - an ambiguous result blocks every invoice for the system number, not just its own, because * `PreviousIrn` is unknown and any later document would chain to a stale IRN. * * Signing, authentication and error normalisation belong to `EimsClientService`. Manual only — * nothing in invoice creation calls this. */ @Injectable() export class EimsInvoiceRegistrationService { private readonly logger = new Logger(EimsInvoiceRegistrationService.name); constructor( @InjectDataSource() private readonly dataSource: DataSource, private readonly config: ConfigService, private readonly client: EimsClientService, private readonly auth: EimsAuthService, private readonly inbox: NotificationInboxService, private readonly notifications: NotificationsService, ) {} private get cfg(): EimsConfig { return this.config.get("eims")!; } async registerInvoiceWithEims(invoiceId: string): Promise { const cfg = this.cfg; // Static seller/tax configuration is validated before anything is locked, allocated or sent. assertEimsInvoiceConfig(cfg); const invoice = await this.loadInvoiceForMapping(invoiceId); if (invoice.eimsIrn) return this.toView(invoice); // Debit/credit notes (confirmed by MoR support: same endpoint, Type DEB/CRE + Reason, // ReferenceDetails.RelatedDocument = the original's IRN) must fail here — before a counter is // touched — if the original was never actually registered. const documentType = (invoice.eimsDocumentType as EimsDocumentType | undefined) ?? "INV"; let relatedDocument: string | null = null; if (documentType !== "INV") { if (!invoice.relatedInvoice) { throw new BadRequestException({ code: "EIMS_RELATED_INVOICE_REQUIRED", message: `Invoice ${invoice.invoiceNumber} is a ${documentType} but has no related invoice set.`, }); } if (!invoice.relatedInvoice.eimsIrn) { throw new BadRequestException({ code: "EIMS_RELATED_INVOICE_NOT_REGISTERED", message: `Invoice ${invoice.invoiceNumber} is a ${documentType} against invoice ${invoice.relatedInvoice.invoiceNumber}, which was never registered with EIMS — nothing to reference.`, }); } relatedDocument = invoice.relatedInvoice.eimsIrn; } // Authenticate before reserving: the source system comes from the token, and the state row is // keyed by it. A login failure here costs nothing — no counter has been consumed yet. const session = await this.auth.getSessionContext(); const reservation = await this.reserve(invoiceId, session.systemNumber); if (!reservation) return this.getEimsStatus(invoiceId); // The request can only be built now: InvoiceCounter and PreviousIrn come from the reservation. const request = toEimsInvoice( invoice, buildEimsSeller(cfg), buildEimsContext(cfg, { // Allocated from the system state, not our invoiceNumber: MoR validates DocumentNumber // against ^(0|[1-9][0-9]{0,8})$, which "INV-20260807-00006" can never satisfy. documentNumber: reservation.documentNumber, invoiceCounter: reservation.invoiceCounter, previousIrn: reservation.previousIrn, session, documentType, reason: invoice.eimsReason, relatedDocument, }), ); let irn: string; let ackDate: string | undefined; let signedQR: string | undefined; try { // Deliberately outside every transaction — no DB lock is held across the wire. const result = await this.submit(request); irn = result.irn; ackDate = result.ackDate; signedQR = result.signedQR; } catch (err) { await this.settleFailure(invoiceId, reservation, err); throw err; } try { await this.settleSuccess(invoiceId, reservation, irn, ackDate, signedQR); } catch (err) { // MoR already accepted this document — unlike settleFailure's targets, this is not // ambiguous, it is a *known* IRN we simply failed to persist (confirmed live 2026-08-12: a // column too narrow for a real IRN). Losing it here would be worse than the persistence // bug itself, so it goes straight into the block reason and the alert, not just a log line. await this.blockOnKnownIrnPersistFailure(invoiceId, reservation, irn, ackDate, err); throw err; } this.logger.log( `Invoice ${invoice.invoiceNumber} registered with EIMS (counter ${reservation.invoiceCounter})`, ); return this.getEimsStatus(invoiceId); } /** * Verify a registered invoice at `POST /v1/verify`. * * Requires a stored IRN. An invoice whose submission was never acknowledged cannot be reconciled * here — the gateway offers no lookup by document number — so it must be resolved with MoR and * recorded through `resolveEimsRegistration`. */ async verifyInvoiceWithEims(invoiceId: string): Promise { const invoice = await this.loadInvoiceRow(this.dataSource.manager, invoiceId); if (!invoice.eimsIrn) { throw new BadRequestException({ code: "EIMS_NO_IRN", message: `Invoice ${invoice.invoiceNumber} has no EIMS IRN to verify (status ${invoice.eimsStatus}). ` + "EIMS can only be queried by IRN, so an unacknowledged submission must be resolved with MoR first.", }); } return this.queryVerify(invoice.eimsIrn); } /** * `POST /v1/verify` for one IRN, with the one check that always applies: the gateway must echo * an `Irn` back. A 200 without it is not a confirmation of anything. * * The request property is lowercase `irn`; the response spells it `Irn`. The two are never * compared — the supplied collection's own fixture uses different example values on each side, * so equality there would assert a property of the mock rather than of the gateway. * * Bearer-authenticated but unsigned, via `postBearer` — see that method for why. */ private async queryVerify(irn: string): Promise { const response = await this.client.postBearer( "/v1/verify", { irn }, ); if (!response?.body?.Irn?.trim()) { throw new EimsApiException( "SCHEMA_VALIDATION", "EIMS verify returned no Irn in its response body", response?.statusCode, ); } return response; } /** * Refuse a manual resolution unless the gateway confirms *both* halves of the claim: that this * IRN is the one it holds, and that it belongs to this invoice. * * The document-number check is against `DocumentDetails.DocumentNumber`, which registration * allocated and stored on the invoice as `eimsDocumentNumber` — the only field tying an IRN back * to a row in this database. * * Recording a wrong IRN is not a local mistake: it marks an unregistered invoice as filed and * chains every later document to a stranger's reference, so both checks are refusals rather * than warnings. */ private async assertIrnBelongsToInvoice( irn: string, expectedDocumentNumber: string, ): Promise { const response = await this.queryVerify(irn); const returnedIrn = response.body?.Irn?.trim(); const documentNumber = response.body?.DocumentDetails?.DocumentNumber?.trim(); if (returnedIrn !== irn) { throw new ConflictException({ code: "EIMS_RESOLVE_IRN_MISMATCH", message: `EIMS answered the lookup for IRN ${irn} with ${returnedIrn ?? "(none)"}. ` + "Refusing to record it — recheck the IRN in the MoR portal.", }); } if (documentNumber !== expectedDocumentNumber) { throw new ConflictException({ code: "EIMS_RESOLVE_DOCUMENT_MISMATCH", message: `EIMS reports IRN ${irn} against document ${documentNumber ?? "(none)"}, not ` + `${expectedDocumentNumber}. Refusing to record it — recheck the IRN in the MoR portal.`, }); } } /** * Manual reconciliation of a blocked system number. * * With an `irn` (found in the MoR portal) the invoice is recorded as registered and the chain * resumes from it. With `discard` the invoice is marked failed and the chain resumes from the * previous IRN. Either way the block is cleared — this is the only exit from an ambiguous result. * * An IRN is never taken on trust: it is verified at the gateway first, and the document it * belongs to must be *this* invoice. A transposed digit would otherwise chain every later * document to a stranger's IRN and mark this invoice registered when it is not. */ async resolveEimsRegistration( invoiceId: string, input: { irn?: string; discard?: boolean }, ): Promise { const irn = input.irn?.trim(); if (!irn && !input.discard) { throw new BadRequestException({ code: "EIMS_RESOLVE_INPUT_REQUIRED", message: "Provide the IRN confirmed with MoR, or discard: true to abandon the submission", }); } // Cheap ownership check before touching the gateway: resolving an invoice that does not hold // the reservation is a caller mistake, not something to spend a MoR round trip on. The // authoritative re-check happens under lock in the transaction below. const [preState]: { in_flight_invoice_id: string | null }[] = await this.dataSource.query( `SELECT in_flight_invoice_id FROM freight.eims_system_state WHERE system_number = $1 AND deleted_at IS NULL LIMIT 1`, [(await this.auth.getSessionContext()).systemNumber], ); if (preState?.in_flight_invoice_id && preState.in_flight_invoice_id !== invoiceId) { throw new ConflictException({ code: "EIMS_RESOLVE_WRONG_INVOICE", message: `The in-flight EIMS submission is invoice ${preState.in_flight_invoice_id}, not ${invoiceId}`, }); } // Outside the transaction: no lock is held across the wire, and a refused verification must // leave the block exactly as it was. if (irn) { const invoice = await this.loadInvoiceRow(this.dataSource.manager, invoiceId); if (!invoice.eimsDocumentNumber) { throw new BadRequestException({ code: "EIMS_NO_DOCUMENT_NUMBER", message: `Invoice ${invoice.invoiceNumber} was never allocated an EIMS document number, so a ` + "returned IRN cannot be tied back to it.", }); } await this.assertIrnBelongsToInvoice(irn, invoice.eimsDocumentNumber); } // Same source of truth as registration: the state row is keyed by the token's system number. const session = await this.auth.getSessionContext(); await this.dataSource.transaction(async (manager) => { const state = await this.lockSystemState(manager, session.systemNumber); if (state.inFlightInvoiceId && state.inFlightInvoiceId !== invoiceId) { throw new ConflictException({ code: "EIMS_RESOLVE_WRONG_INVOICE", message: `The in-flight EIMS submission is invoice ${state.inFlightInvoiceId}, not ${invoiceId}`, }); } const invoice = await this.lockInvoice(manager, invoiceId); if (invoice.eimsIrn) { throw new ConflictException({ code: "EIMS_ALREADY_REGISTERED", message: `Invoice ${invoice.invoiceNumber} already has IRN ${invoice.eimsIrn}`, }); } await manager.update(Invoice, invoiceId, { eimsStatus: irn ? EimsInvoiceStatus.Registered : EimsInvoiceStatus.Failed, eimsIrn: irn ?? null, }); await manager.update(EimsSystemState, state.id, { // Only a confirmed IRN may advance the chain; a discard leaves it where it was. ...(irn ? { previousIrn: irn } : {}), inFlightInvoiceId: null, inFlightCounter: null, inFlightDocumentNumber: null, blockedReason: null, }); }); this.logger.warn( `EIMS block on invoice ${invoiceId} resolved manually (${irn ? "IRN recorded" : "discarded"})`, ); return this.getEimsStatus(invoiceId); } async getEimsStatus(invoiceId: string): Promise { return this.toView(await this.loadInvoiceRow(this.dataSource.manager, invoiceId)); } // ── transactions ───────────────────────────────────────────────────────────────────────────── /** * TX1. Consume a counter and record the holder, committed before any HTTP call. Returns `null` * when the invoice turned out to be registered already (checked under the lock). */ private async reserve(invoiceId: string, systemNumber: string): Promise { return this.dataSource.transaction(async (manager) => { const state = await this.lockSystemState(manager, systemNumber); if (state.blockedReason) { throw new ConflictException({ code: "EIMS_SYSTEM_BLOCKED", message: `EIMS registration is blocked for system ${systemNumber}: ${state.blockedReason}. ` + "Resolve the affected invoice before registering anything else.", }); } if (state.inFlightInvoiceId) { throw new ConflictException({ code: "EIMS_SUBMISSION_IN_FLIGHT", message: `A submission for invoice ${state.inFlightInvoiceId} is already in flight on system ` + `${systemNumber}. Wait for it to settle, or resolve it if the process was interrupted.`, }); } const invoice = await this.lockInvoice(manager, invoiceId); if (invoice.eimsIrn) return null; const invoiceCounter = Number(state.nextInvoiceCounter); const documentNumber = String(Number(state.nextDocumentNumber)); const previousIrn = state.previousIrn ?? ""; // Counter consumed here, not on success: once an attempt begins it can never be reused, // whatever happens next. A gap is harmless at MoR; a collision is not. await manager.update(EimsSystemState, state.id, { nextInvoiceCounter: invoiceCounter + 1, nextDocumentNumber: Number(documentNumber) + 1, inFlightInvoiceId: invoiceId, inFlightCounter: invoiceCounter, inFlightDocumentNumber: Number(documentNumber), }); await manager.update(Invoice, invoiceId, { eimsStatus: EimsInvoiceStatus.Submitting, eimsInvoiceCounter: invoiceCounter, eimsDocumentNumber: documentNumber, eimsSubmittedAt: new Date(), eimsLastError: null, }); return { stateId: state.id, invoiceCounter, documentNumber, previousIrn }; }); } /** TX2a. Record the IRN, advance the chain, release the reservation. */ private async settleSuccess( invoiceId: string, reservation: Reservation, irn: string, ackDate?: string, signedQR?: string, ): Promise { let companyId: string | undefined; let invoiceNumber = invoiceId; await this.dataSource.transaction(async (manager) => { const invoice = await this.lockInvoice(manager, invoiceId); companyId = invoice.companyId ?? undefined; invoiceNumber = invoice.invoiceNumber; await manager.update(Invoice, invoiceId, { eimsStatus: EimsInvoiceStatus.Registered, eimsIrn: irn, eimsAckDate: ackDate ?? null, eimsSignedQr: signedQR ?? null, eimsLastError: null, }); await manager.update(EimsSystemState, reservation.stateId, { previousIrn: irn, inFlightInvoiceId: null, inFlightCounter: null, inFlightDocumentNumber: null, blockedReason: null, }); }); // Best-effort, outside the transaction: MoR checklist ADD-N001 wants the buyer notified of a // registration event. Never lets a notification failure mask a filing that already succeeded. if (companyId) { try { await sendCompanyChannels( this.dataSource, this.notifications, companyId, `Invoice ${invoiceNumber} has been registered with MoR EIMS. Reference (IRN): ${irn}`, ); } catch (err) { this.logger.warn(`EIMS buyer notification failed for invoice ${invoiceId}: ${(err as Error).message}`); } } } /** * TX2b. A deterministic rejection releases the reservation **and returns both numbers**; an * ambiguous result keeps both and blocks the system number, because `PreviousIrn` is now unknown * for every later document. * * Both `InvoiceCounter` and `DocumentNumber` roll back together on a deterministic rejection — * MoR's own expected-next-value only advances on acceptance, for both fields: * * - `InvoiceCounter`: "Invoice counter is not correct. expected : 1". * - `DocumentNumber`: "Document number error. Document number is not in correct sequence * expected : 1" (rule 7001) — confirmed live 2026-08-12. An earlier design burned * `DocumentNumber` forward on every attempt, reasoning from a separate "Document number is * not unique" rejection; that turned out to describe the same constraint from the other side * (MoR rejects both reuse *and* skipping ahead of its true next value), and burning forward on * every rejection permanently drifted past what MoR would ever accept again — confirmed live * when two rejected self-test attempts deadlocked the sequence until a manual DB reset. * * An ambiguous result keeps both: MoR may have counted and stored the document. */ private async settleFailure( invoiceId: string, reservation: Reservation, err: unknown, ): Promise { const api = err instanceof EimsApiException ? err : null; const deterministic = api ? DETERMINISTIC_KINDS.has(api.kind) : false; const status = deterministic ? EimsInvoiceStatus.Failed : EimsInvoiceStatus.Unknown; const lastError: EimsInvoiceError = { kind: api?.kind ?? "UNKNOWN", message: (err as Error)?.message ?? "unknown error", httpStatus: api?.httpStatus, details: api?.details, at: new Date().toISOString(), }; await this.dataSource.transaction(async (manager) => { await manager.update(Invoice, invoiceId, { eimsStatus: status, eimsLastError: lastError, } as QueryDeepPartialEntity); await manager.update( EimsSystemState, reservation.stateId, deterministic ? { // Both return: MoR never counted a refused document against either sequence. nextInvoiceCounter: reservation.invoiceCounter, nextDocumentNumber: Number(reservation.documentNumber), inFlightInvoiceId: null, inFlightCounter: null, inFlightDocumentNumber: null, blockedReason: null, } : { blockedReason: `Invoice ${invoiceId} was submitted with counter ${reservation.invoiceCounter} but ` + `never acknowledged (${lastError.kind}). Its IRN is unknown, so no further document ` + "can be chained until it is resolved with MoR.", }, ); }); this.logger.error(`Invoice ${invoiceId} EIMS registration ${status}: ${lastError.message}`); await this.alertStaff(invoiceId, status, lastError, deterministic); } /** * MoR accepted the document (a real IRN came back) but recording that locally failed — the * reservation is still held from TX1, so the system-wide block goes on regardless of *why* * `settleSuccess` failed. The IRN and ack date are written straight into `blockedReason` so a * human resolving this never has to dig through logs for the one thing that must not be lost. */ private async blockOnKnownIrnPersistFailure( invoiceId: string, reservation: Reservation, irn: string, ackDate: string | undefined, err: unknown, ): Promise { const reason = `Invoice ${invoiceId} was ACCEPTED by EIMS (IRN ${irn}${ackDate ? `, ack ${ackDate}` : ""}) ` + `but recording it locally failed: ${(err as Error)?.message ?? "unknown error"}. Resolve with ` + `POST /invoices/${invoiceId}/eims/resolve using this IRN once the underlying issue is fixed — ` + "do not resubmit, the document already exists at MoR."; try { await this.dataSource.manager.update(EimsSystemState, reservation.stateId, { blockedReason: reason }); } catch (updateErr) { // Even the block itself failed to write — last resort is the log, since there is nothing // left to retry into. this.logger.error(`Could not record EIMS block for invoice ${invoiceId}: ${reason}`, updateErr as Error); return; } this.logger.error(reason); // Not alertStaff(): its canned copy for a non-deterministic result always says "the IRN is // unknown", which is false here — the whole point of this path is that the IRN *is* known. try { await this.inbox.notify({ recipients: { permissionKeys: [FREIGHT_PERMS.invoices.eimsResolve] }, audience: NotificationAudience.BACKOFFICE, type: NotificationType.GENERIC, priority: NotificationPriority.HIGH, title: "EIMS accepted an invoice but it was not recorded — all further filing is blocked", body: reason, link: `/dashboard/invoices/${invoiceId}`, data: { invoiceId, eimsStatus: EimsInvoiceStatus.Unknown, irn, action: "EIMS_PERSIST_FAILED" }, }); } catch (notifyErr) { this.logger.warn(`EIMS staff alert failed for invoice ${invoiceId}: ${(notifyErr as Error).message}`); } } /** * Tell the people who can act about a failed filing. * * An ambiguous result is the urgent one: it blocks *every* further invoice for this system * number until a human resolves it, and nothing else in the system would surface that — the * sweep just goes quiet. A deterministic rejection affects one invoice, so it is normal * priority. Never throws: an alert that fails must not mask the filing outcome. */ private async alertStaff( invoiceId: string, status: EimsInvoiceStatus, error: EimsInvoiceError, deterministic: boolean, ): Promise { try { await this.inbox.notify({ recipients: { permissionKeys: [FREIGHT_PERMS.invoices.eimsResolve] }, audience: NotificationAudience.BACKOFFICE, type: NotificationType.GENERIC, priority: deterministic ? NotificationPriority.NORMAL : NotificationPriority.HIGH, title: deterministic ? "EIMS rejected an invoice" : "EIMS filing unresolved — all further filing is blocked", body: deterministic ? `MoR rejected the filing (${error.kind}): ${error.message}. The invoice is marked FAILED; correct it and file again.` : `A submission was sent but never acknowledged (${error.kind}). Its IRN is unknown, so no further invoice can be filed until it is resolved with MoR.`, link: `/dashboard/invoices/${invoiceId}`, data: { invoiceId, eimsStatus: status, kind: error.kind, action: "EIMS_FILING_FAILED" }, }); } catch (err) { this.logger.warn(`EIMS staff alert failed for invoice ${invoiceId}: ${(err as Error).message}`); } } // ── internals ──────────────────────────────────────────────────────────────────────────────── /** A non-empty IRN is the only success signal; anything else is a failed registration. */ private async submit( request: EimsInvoiceRequest, ): Promise<{ irn: string; ackDate?: string; signedQR?: string }> { const response = await this.client.postSigned( "/v1/register", request, ); const irn = response?.body?.irn; if (!irn) { // The gateway answered, so this is deterministic: the document is not registered. throw new EimsApiException( "SCHEMA_VALIDATION", `EIMS register returned no IRN${response?.body?.errorMessage ? `: ${response.body.errorMessage}` : ""}`, response?.statusCode, ); } return { irn, ackDate: response.body?.ackDate, signedQR: response.body?.signedQR }; } private async lockInvoice(manager: EntityManager, invoiceId: string): Promise { const invoice = await manager .createQueryBuilder(Invoice, "invoice") .setLock("pessimistic_write") .where("invoice.id = :invoiceId", { invoiceId }) .getOne(); if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`); return invoice; } /** Locks the system-state row, creating it on first use. */ private async lockSystemState( manager: EntityManager, systemNumber: string, ): Promise { const select = () => manager .createQueryBuilder(EimsSystemState, "state") .setLock("pessimistic_write") .where("state.system_number = :systemNumber", { systemNumber }) .getOne(); const existing = await select(); if (existing) return existing; await manager.query( `INSERT INTO freight.eims_system_state (system_number) VALUES ($1) ON CONFLICT (system_number) DO NOTHING`, [systemNumber], ); const created = await select(); if (!created) throw new Error(`Could not initialise EIMS system state for ${systemNumber}`); return created; } /** Header + buyer + lines — everything the mapper needs. */ private async loadInvoiceForMapping( invoiceId: string, ): Promise { const invoice = await this.dataSource.getRepository(Invoice).findOne({ where: { id: invoiceId }, relations: { company: true, companyProfile: true, relatedInvoice: true }, }); if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`); const lines: EimsMapperLine[] = await this.dataSource.query( `SELECT charge_type AS "chargeType", description, quantity, unit_rate AS "unitRate", amount, currency, metadata FROM freight.invoice_lines WHERE invoice_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC`, [invoiceId], ); return Object.assign(invoice, { lines }); } private async loadInvoiceRow(manager: EntityManager, invoiceId: string): Promise { const invoice = await manager.findOne(Invoice, { where: { id: invoiceId } }); if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`); return invoice; } private toView(invoice: Invoice): EimsInvoiceStatusView { return toEimsInvoiceStatusView(invoice); } }