diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts index 175e3e00c..9dc6e15e9 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -29,6 +29,7 @@ import { PaymentService } from "../payment/payment.service"; import { InitiateResponseDto, IntentStatusDto } from "../payment/payments.dto"; import { InvoiceDocumentModel, + sameCompanyName, InvoiceDocumentService, pngDataUrl, } from "./documents/invoice-document.service"; @@ -875,10 +876,21 @@ export class BillingService { totals.push({ label: "Paid", amount: Number(invoice.paidAmount) }); totals.push({ label: "Balance", amount: Number(invoice.balanceAmount) }); + const tradeName = invoice.companyProfile?.etradeBusiness?.tradeName?.trim(); + 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 }, + // The trade name of the eTrade licence THIS profile operates as. A TIN + // holds many licences and the invoiced role (importer/exporter/forwarder) + // is usually a different business from the one the company registered + // under, so the buyer's name alone doesn't say which one was billed. + // Suppressed when it just repeats the buyer name — most companies trade + // under their registered name and a duplicate row helps nobody. + ...(tradeName && !sameCompanyName(tradeName, invoice.company?.name) + ? [{ label: "Buyer trade name", value: tradeName }] + : []), { label: "Buyer TIN", value: invoice.company?.tin ?? null }, ...(invoice.company?.vatNumber ? [{ label: "Buyer VAT No.", value: invoice.company.vatNumber }] diff --git a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts index ebdf51be1..c443fad7e 100644 --- a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts +++ b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.spec.ts @@ -1,4 +1,4 @@ -import { InvoiceDocumentModel, InvoiceDocumentService } from "./invoice-document.service"; +import { InvoiceDocumentModel, InvoiceDocumentService, sameCompanyName } from "./invoice-document.service"; const model = (over: Partial = {}): InvoiceDocumentModel => ({ kind: "INVOICE", @@ -87,3 +87,38 @@ describe("InvoiceDocumentService.buildThermalHtml", () => { expect(html).not.toContain("right: 160px"); }); }); + +describe("sameCompanyName", () => { + it("treats eTrade's legal-suffix spellings as the same name", () => { + expect(sameCompanyName("ABIJOEL PLC", "ABIJOEL P L C")).toBe(true); + expect( + sameCompanyName( + "WISH TRADING PLC", + "WISH TRADING PRIVATE LIMITED COMPANY", + ), + ).toBe(true); + expect( + sameCompanyName("TUTA TRADING PLC", "TUTA TRADING ONE MEMBER PLC"), + ).toBe(true); + }); + + it("keeps a genuinely different trade name distinct", () => { + // Real pairs from eTrade: the licence trades under a different name than + // the company registered under, which is exactly the row worth printing. + expect( + sameCompanyName("Cozy Coffee Grower and Exporter", "ABIJOEL P L C"), + ).toBe(false); + expect(sameCompanyName("MENNA PRODUCTION", "ICOFFEE TRADING PLC")).toBe( + false, + ); + expect( + sameCompanyName("YUNABEK TRADING PLC", "YUNABEK INVESTMENT PLC"), + ).toBe(false); + }); + + it("is false when either side is missing, so no row is printed", () => { + expect(sameCompanyName("", "ABIJOEL P L C")).toBe(false); + expect(sameCompanyName(null, null)).toBe(false); + expect(sameCompanyName("ABIJOEL P L C", undefined)).toBe(false); + }); +}); diff --git a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts index 0f8e4ee71..78cea01b6 100644 --- a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts +++ b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts @@ -48,6 +48,34 @@ function formatDate(value: unknown): string { return value ? new Date(value as string | Date).toLocaleDateString("en-GB") : "-"; } +/** + * Is this trade name just the company name again? + * + * Compared loosely on purpose: eTrade spells the same legal suffix as "PLC", + * "P L C" and "PRIVATE LIMITED COMPANY", and pads names with double spaces, so + * an exact comparison would call two spellings of one name different and print + * a redundant row. Used only to decide whether a trade-name row is worth + * showing — never to decide that two businesses ARE the same. + */ +export function sameCompanyName( + a: string | null | undefined, + b: string | null | undefined, +): boolean { + const norm = (v: string | null | undefined) => + (v ?? "") + .toUpperCase() + .replace(/[.,]/g, "") + .replace(/\s+/g, " ") + .trim() + .replace(/\bPRIVATE LIMITED COMPANY\b/g, "PLC") + .replace(/\bP L C\b/g, "PLC") + .replace(/\bONE (MEMBER|PERSON) PLC\b/g, "PLC") + .replace(/\s+/g, " ") + .trim(); + const left = norm(a); + return left !== "" && left === norm(b); +} + /** One billed line on the document (charge type / fee type agnostic). */ export interface InvoiceDocumentLine { description: string | null; diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.service.ts index 558ffc1a9..e29eba2c2 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.service.ts @@ -22,6 +22,7 @@ import { InvoiceLine } from "../billing/entities/invoice-line.entity"; import { PayInvoiceDto as GatewayPayInvoiceDto } from "../billing/dto/pay-invoice.dto"; import { InvoiceDocumentModel, + sameCompanyName, InvoiceDocumentService, } from "../billing/documents/invoice-document.service"; import { NotificationsService } from "../notifications/notifications.service"; @@ -71,6 +72,11 @@ const ACTIVE_STATUSES: Freight.InvoiceStatus[] = [ export interface InvoiceDocumentDetails { bookingReference: string | null; customerName: string | null; + /** + * Trade name of the eTrade licence the billed company profile operates as. + * Null when nothing is attached, or for a company with no eTrade record. + */ + customerTradeName: string | null; inventoryReference: string | null; inventoryInfo: string | null; inventoryStatus: string | null; @@ -745,6 +751,17 @@ export class WarehouseInvoiceService { }, { label: "Booking reference", value: invoice.bookingReference ?? null }, { label: "Customer", value: invoice.customerName ?? null }, + // Which of the TIN's eTrade businesses was billed. Omitted when it just + // repeats the customer name — see sameCompanyName. + ...(invoice.customerTradeName && + !sameCompanyName(invoice.customerTradeName, invoice.customerName) + ? [ + { + label: "Customer trade name", + value: invoice.customerTradeName, + }, + ] + : []), { label: "Inventory reference", value: invoice.inventoryReference ?? null, @@ -795,6 +812,7 @@ export class WarehouseInvoiceService { const [row] = await this.dataSource.query( `SELECT b.reference AS "bookingReference", company.name AS "customerName", + cp.etrade_business->>'tradeName' AS "customerTradeName", COALESCE(inv.release_order_reference, b.reference) AS "inventoryReference", inv.status AS "inventoryStatus", inv.release_date AS "releaseDate", @@ -812,6 +830,7 @@ export class WarehouseInvoiceService { FROM freight.warehouse_inventory inv LEFT JOIN freight.bookings b ON b.id = inv.booking_id AND b.deleted_at IS NULL LEFT JOIN freight.companies company ON company.id = b.company_id + LEFT JOIN freight.company_profiles cp ON cp.id = b.company_profile_id AND cp.deleted_at IS NULL LEFT JOIN freight.containers container ON container.id = inv.container_id AND container.deleted_at IS NULL LEFT JOIN freight.booking_container booking_container ON ( booking_container.booking_id = b.id @@ -837,6 +856,7 @@ export class WarehouseInvoiceService { return { bookingReference: row?.bookingReference ?? null, customerName: row?.customerName ?? null, + customerTradeName: row?.customerTradeName ?? null, inventoryReference: row?.inventoryReference ?? null, inventoryInfo: row?.inventoryInfo ?? null, inventoryStatus: row?.inventoryStatus ?? null,