mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
feat(billing): show the buyer's trade name on invoice documents
An invoice is billed to one company profile, and that profile's eTrade licence is usually a different business from the one the company registered under — so the buyer's name alone does not say which business was billed. Both document paths gain a row: the shared invoice/receipt model reads it off the already-loaded `companyProfile` relation, and the warehouse fee invoice joins `company_profiles` through the booking. `sameCompanyName` suppresses the row when it merely repeats the buyer name, which is the common case. It compares loosely because eTrade spells one legal suffix three ways (PLC / P L C / PRIVATE LIMITED COMPANY) and pads names with double spaces; it decides whether a row is worth printing and nothing else. The EIMS buyer `LegalName` is deliberately untouched — a MoR filing carries the registered entity, same rule as the seller side.
This commit is contained in:
@@ -29,6 +29,7 @@ import { PaymentService } from "../payment/payment.service";
|
|||||||
import { InitiateResponseDto, IntentStatusDto } from "../payment/payments.dto";
|
import { InitiateResponseDto, IntentStatusDto } from "../payment/payments.dto";
|
||||||
import {
|
import {
|
||||||
InvoiceDocumentModel,
|
InvoiceDocumentModel,
|
||||||
|
sameCompanyName,
|
||||||
InvoiceDocumentService,
|
InvoiceDocumentService,
|
||||||
pngDataUrl,
|
pngDataUrl,
|
||||||
} from "./documents/invoice-document.service";
|
} from "./documents/invoice-document.service";
|
||||||
@@ -875,10 +876,21 @@ export class BillingService {
|
|||||||
totals.push({ label: "Paid", amount: Number(invoice.paidAmount) });
|
totals.push({ label: "Paid", amount: Number(invoice.paidAmount) });
|
||||||
totals.push({ label: "Balance", amount: Number(invoice.balanceAmount) });
|
totals.push({ label: "Balance", amount: Number(invoice.balanceAmount) });
|
||||||
|
|
||||||
|
const tradeName = invoice.companyProfile?.etradeBusiness?.tradeName?.trim();
|
||||||
|
|
||||||
const summary: InvoiceDocumentModel["summary"] = [
|
const summary: InvoiceDocumentModel["summary"] = [
|
||||||
// Buyer identity — was missing entirely; a MoR-registered invoice must show who it was
|
// 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.
|
// filed against, not just the seller. VatNumber shown only when the company has one.
|
||||||
{ label: "Buyer", value: invoice.company?.name ?? null },
|
{ 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 },
|
{ label: "Buyer TIN", value: invoice.company?.tin ?? null },
|
||||||
...(invoice.company?.vatNumber
|
...(invoice.company?.vatNumber
|
||||||
? [{ label: "Buyer VAT No.", value: invoice.company.vatNumber }]
|
? [{ label: "Buyer VAT No.", value: invoice.company.vatNumber }]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InvoiceDocumentModel, InvoiceDocumentService } from "./invoice-document.service";
|
import { InvoiceDocumentModel, InvoiceDocumentService, sameCompanyName } from "./invoice-document.service";
|
||||||
|
|
||||||
const model = (over: Partial<InvoiceDocumentModel> = {}): InvoiceDocumentModel => ({
|
const model = (over: Partial<InvoiceDocumentModel> = {}): InvoiceDocumentModel => ({
|
||||||
kind: "INVOICE",
|
kind: "INVOICE",
|
||||||
@@ -87,3 +87,38 @@ describe("InvoiceDocumentService.buildThermalHtml", () => {
|
|||||||
expect(html).not.toContain("right: 160px");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,6 +48,34 @@ function formatDate(value: unknown): string {
|
|||||||
return value ? new Date(value as string | Date).toLocaleDateString("en-GB") : "-";
|
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). */
|
/** One billed line on the document (charge type / fee type agnostic). */
|
||||||
export interface InvoiceDocumentLine {
|
export interface InvoiceDocumentLine {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { InvoiceLine } from "../billing/entities/invoice-line.entity";
|
|||||||
import { PayInvoiceDto as GatewayPayInvoiceDto } from "../billing/dto/pay-invoice.dto";
|
import { PayInvoiceDto as GatewayPayInvoiceDto } from "../billing/dto/pay-invoice.dto";
|
||||||
import {
|
import {
|
||||||
InvoiceDocumentModel,
|
InvoiceDocumentModel,
|
||||||
|
sameCompanyName,
|
||||||
InvoiceDocumentService,
|
InvoiceDocumentService,
|
||||||
} from "../billing/documents/invoice-document.service";
|
} from "../billing/documents/invoice-document.service";
|
||||||
import { NotificationsService } from "../notifications/notifications.service";
|
import { NotificationsService } from "../notifications/notifications.service";
|
||||||
@@ -71,6 +72,11 @@ const ACTIVE_STATUSES: Freight.InvoiceStatus[] = [
|
|||||||
export interface InvoiceDocumentDetails {
|
export interface InvoiceDocumentDetails {
|
||||||
bookingReference: string | null;
|
bookingReference: string | null;
|
||||||
customerName: 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;
|
inventoryReference: string | null;
|
||||||
inventoryInfo: string | null;
|
inventoryInfo: string | null;
|
||||||
inventoryStatus: string | null;
|
inventoryStatus: string | null;
|
||||||
@@ -745,6 +751,17 @@ export class WarehouseInvoiceService {
|
|||||||
},
|
},
|
||||||
{ label: "Booking reference", value: invoice.bookingReference ?? null },
|
{ label: "Booking reference", value: invoice.bookingReference ?? null },
|
||||||
{ label: "Customer", value: invoice.customerName ?? 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",
|
label: "Inventory reference",
|
||||||
value: invoice.inventoryReference ?? null,
|
value: invoice.inventoryReference ?? null,
|
||||||
@@ -795,6 +812,7 @@ export class WarehouseInvoiceService {
|
|||||||
const [row] = await this.dataSource.query(
|
const [row] = await this.dataSource.query(
|
||||||
`SELECT b.reference AS "bookingReference",
|
`SELECT b.reference AS "bookingReference",
|
||||||
company.name AS "customerName",
|
company.name AS "customerName",
|
||||||
|
cp.etrade_business->>'tradeName' AS "customerTradeName",
|
||||||
COALESCE(inv.release_order_reference, b.reference) AS "inventoryReference",
|
COALESCE(inv.release_order_reference, b.reference) AS "inventoryReference",
|
||||||
inv.status AS "inventoryStatus",
|
inv.status AS "inventoryStatus",
|
||||||
inv.release_date AS "releaseDate",
|
inv.release_date AS "releaseDate",
|
||||||
@@ -812,6 +830,7 @@ export class WarehouseInvoiceService {
|
|||||||
FROM freight.warehouse_inventory inv
|
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.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.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.containers container ON container.id = inv.container_id AND container.deleted_at IS NULL
|
||||||
LEFT JOIN freight.booking_container booking_container ON (
|
LEFT JOIN freight.booking_container booking_container ON (
|
||||||
booking_container.booking_id = b.id
|
booking_container.booking_id = b.id
|
||||||
@@ -837,6 +856,7 @@ export class WarehouseInvoiceService {
|
|||||||
return {
|
return {
|
||||||
bookingReference: row?.bookingReference ?? null,
|
bookingReference: row?.bookingReference ?? null,
|
||||||
customerName: row?.customerName ?? null,
|
customerName: row?.customerName ?? null,
|
||||||
|
customerTradeName: row?.customerTradeName ?? null,
|
||||||
inventoryReference: row?.inventoryReference ?? null,
|
inventoryReference: row?.inventoryReference ?? null,
|
||||||
inventoryInfo: row?.inventoryInfo ?? null,
|
inventoryInfo: row?.inventoryInfo ?? null,
|
||||||
inventoryStatus: row?.inventoryStatus ?? null,
|
inventoryStatus: row?.inventoryStatus ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user