eims integration master test complete

This commit is contained in:
Hagernesh
2026-08-12 14:08:28 +00:00
parent 8d53dc17ce
commit 2d4da8110b
38 changed files with 2270 additions and 173 deletions

View File

@@ -1,4 +1,5 @@
import { Freight, PaymentReferenceType } from "@edr/types";
import { ConfigService } from "@nestjs/config";
import {
BadRequestException,
forwardRef,
@@ -12,6 +13,7 @@ import { logCtx } from "@edr/api-common";
import { DataSource, EntityManager, In } from "typeorm";
import { Booking } from "../bookings/entities/booking.entity";
import { EimsConfig } from "../../config/eims.config";
import { CompaniesService } from "../companies/companies.service";
import { FilesService } from "../files/files.service";
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
@@ -161,6 +163,7 @@ export class BillingService {
private readonly companies: CompaniesService,
private readonly invoiceDocuments: InvoiceDocumentService,
private readonly files: FilesService,
private readonly config: ConfigService,
) { }
// ── Reads ──────────────────────────────────────────────────────────────────
@@ -384,7 +387,7 @@ export class BillingService {
async document(id: string): Promise<{ filename: string; buffer: Buffer }> {
const invoice = await this.findById(id);
return this.invoiceDocuments.render(
this.toDocumentModel(invoice, "INVOICE"),
await this.toDocumentModel(invoice, "INVOICE"),
);
}
@@ -397,15 +400,24 @@ export class BillingService {
);
}
return this.invoiceDocuments.render(
this.toDocumentModel(invoice, "RECEIPT"),
await this.toDocumentModel(invoice, "RECEIPT"),
);
}
/**
* `Invoice.eimsSignedQr` is already a base64 PNG straight from MoR — confirmed against the
* Postman collection's `register` response (`signedQR` decodes to a PNG magic-byte header),
* not a payload we encode ourselves. Wrapped in a data URL, nothing more.
*/
private renderEimsQr(signedQr: string): string {
return `data:image/png;base64,${signedQr}`;
}
/** Map a global invoice (+ lines) onto the source-agnostic document model. */
private toDocumentModel(
private async toDocumentModel(
invoice: Invoice & { lines: InvoiceLine[] },
kind: "INVOICE" | "RECEIPT",
): InvoiceDocumentModel {
): Promise<InvoiceDocumentModel> {
const title = invoice.source
? invoice.source.charAt(0).toUpperCase() + invoice.source.slice(1)
: "EDR";
@@ -423,6 +435,43 @@ export class BillingService {
totals.push({ label: "Paid", amount: Number(invoice.paidAmount) });
totals.push({ label: "Balance", amount: Number(invoice.balanceAmount) });
const summary: InvoiceDocumentModel["summary"] = [
// Buyer identity — was missing entirely; a MoR-registered invoice must show who it was
// filed against, not just the seller. VatNumber shown only when the company has one.
{ label: "Buyer", value: invoice.company?.name ?? null },
{ label: "Buyer TIN", value: invoice.company?.tin ?? null },
...(invoice.company?.vatNumber
? [{ label: "Buyer VAT No.", value: invoice.company.vatNumber }]
: []),
{ label: "Status", value: invoice.status },
{ label: "Type", value: invoice.type },
{ label: "Reference", value: invoice.sourceId },
{ label: "Currency", value: invoice.currency },
{
label: "Issued",
value: invoice.issuedAt
? new Date(invoice.issuedAt).toLocaleDateString("en-GB")
: null,
},
{
label: "Due",
value: invoice.dueAt
? new Date(invoice.dueAt).toLocaleDateString("en-GB")
: null,
},
];
// Seller identity — EDR's own legal TIN/VAT live only in EIMS config (nowhere else in this
// codebase). Shown only when actually configured, same as the buyer VAT row.
const eimsCfg = this.config.get<EimsConfig>("eims");
if (eimsCfg?.tin) summary.push({ label: "Seller TIN", value: eimsCfg.tin });
if (eimsCfg?.invoice?.sellerVatNumber) {
summary.push({ label: "Seller VAT No.", value: eimsCfg.invoice.sellerVatNumber });
}
// MoR EIMS reference — only once actually registered, never a placeholder row.
if (invoice.eimsIrn) summary.push({ label: "EIMS IRN", value: invoice.eimsIrn });
return {
kind,
title,
@@ -430,24 +479,7 @@ export class BillingService {
issuedAt: invoice.issuedAt ?? invoice.createdAt,
status: invoice.status,
currency: invoice.currency,
summary: [
{ label: "Status", value: invoice.status },
{ label: "Type", value: invoice.type },
{ label: "Reference", value: invoice.sourceId },
{ label: "Currency", value: invoice.currency },
{
label: "Issued",
value: invoice.issuedAt
? new Date(invoice.issuedAt).toLocaleDateString("en-GB")
: null,
},
{
label: "Due",
value: invoice.dueAt
? new Date(invoice.dueAt).toLocaleDateString("en-GB")
: null,
},
],
summary,
categoryHeader: "Charge type",
lines: invoice.lines.map((l) => ({
description: l.description ?? l.chargeType,
@@ -458,6 +490,7 @@ export class BillingService {
currency: l.currency,
})),
totals,
qrImageUrl: invoice.eimsSignedQr ? this.renderEimsQr(invoice.eimsSignedQr) : null,
};
}