mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-31 10:47:38 +00:00
eims-receipt-document.mapper.ts maps an EimsReceipt onto the shared InvoiceDocumentModel layout, reading amounts back out of the stored request body. Refuses to render anything not REGISTERED. GET invoices/:id/eims/receipts/:receiptId/document, scoped to the invoice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
108 lines
4.4 KiB
TypeScript
108 lines
4.4 KiB
TypeScript
import { Invoice } from "../billing/entities/invoice.entity";
|
|
import {
|
|
InvoiceDocumentModel,
|
|
pngDataUrl,
|
|
} from "../billing/documents/invoice-document.service";
|
|
import { EimsReceipt, EimsReceiptStatus } from "./entities/eims-receipt.entity";
|
|
import { EimsSalesReceiptRequest, EimsWithholdReceiptRequest } from "./eims-receipt.types";
|
|
|
|
/**
|
|
* Maps a filed `EimsReceipt` onto the shared invoice/receipt document layout — mirrors
|
|
* `eims-invoice.mapper.ts`'s role for `/v1/register`: a pure function, no I/O.
|
|
*
|
|
* The amounts (collected amount, mode of payment, withholding amount) live only in
|
|
* `receipt.request` — the exact body this app sent, typed and written in exactly one place
|
|
* (`EimsReceiptService`). Reading it back is a cast, not a new source of truth; real columns
|
|
* would mean a migration + backfill for data already present in a stable shape.
|
|
*
|
|
* Throws rather than returning a model for anything not actually filed: a sealed, stamped PDF
|
|
* for a receipt MoR rejected, never acknowledged, or whose request was somehow never recorded
|
|
* would read as a genuine tax document. Callers (`EimsReceiptService.document`) let this throw
|
|
* surface as a 400 — there is nothing sensible to render instead.
|
|
*/
|
|
export function toReceiptDocumentModel(receipt: EimsReceipt, invoice: Invoice): InvoiceDocumentModel {
|
|
if (receipt.status !== EimsReceiptStatus.Registered) {
|
|
throw new Error(
|
|
`Receipt ${receipt.receiptNumber} is ${receipt.status}, not REGISTERED — refusing to print an unfiled receipt.`,
|
|
);
|
|
}
|
|
if (!receipt.request) {
|
|
throw new Error(`Receipt ${receipt.receiptNumber} has no stored request body — cannot render its amounts.`);
|
|
}
|
|
|
|
const isSales = receipt.kind === "SALES";
|
|
|
|
if (isSales) {
|
|
const req = receipt.request as unknown as EimsSalesReceiptRequest;
|
|
return build(receipt, invoice, {
|
|
title: "Sales Receipt",
|
|
currency: req.ReceiptCurrency,
|
|
amountLabel: "Collected",
|
|
lineDescription: `Payment received against invoice ${invoice.invoiceNumber}`,
|
|
amount: req.CollectedAmount,
|
|
// A sales receipt is a real payment — this is the one case the shared layout's own default
|
|
// ("EDR PAID" for kind RECEIPT) is already correct, but set it explicitly so it never drifts
|
|
// if that default changes for an unrelated reason.
|
|
sealText: "EDR PAID",
|
|
extraSummary: [{ label: "Mode of payment", value: req.TransactionDetails.ModeOfPayment }],
|
|
});
|
|
}
|
|
|
|
const req = receipt.request as unknown as EimsWithholdReceiptRequest;
|
|
return build(receipt, invoice, {
|
|
title: "Withholding Receipt",
|
|
currency: req.InvoiceDetail.Currency,
|
|
amountLabel: "Withheld",
|
|
lineDescription: `Withholding (${req.WithholdDetail.Type}) against invoice ${invoice.invoiceNumber}`,
|
|
amount: req.WithholdDetail.WithholdingAmount,
|
|
// A withholding receipt is not a payment — the shared layout's "EDR PAID" default would be
|
|
// wrong here, so this is the one case that MUST override it.
|
|
sealText: "EDR",
|
|
extraSummary: [{ label: "Withholding type", value: req.WithholdDetail.Type }],
|
|
});
|
|
}
|
|
|
|
function build(
|
|
receipt: EimsReceipt,
|
|
invoice: Invoice,
|
|
opts: {
|
|
title: string;
|
|
currency: string;
|
|
amountLabel: string;
|
|
lineDescription: string;
|
|
amount: number;
|
|
sealText: string;
|
|
extraSummary: Array<{ label: string; value: string | null }>;
|
|
},
|
|
): InvoiceDocumentModel {
|
|
return {
|
|
kind: "RECEIPT",
|
|
title: opts.title,
|
|
documentNumber: receipt.receiptNumber,
|
|
issuedAt: receipt.submittedAt ?? null,
|
|
status: receipt.status,
|
|
currency: opts.currency,
|
|
summary: [
|
|
{ label: "Invoice", value: invoice.invoiceNumber },
|
|
{ label: "Invoice IRN", value: invoice.eimsIrn ?? null },
|
|
{ label: "RRN", value: receipt.rrn ?? null },
|
|
{ label: "Ack status", value: receipt.ackStatus ?? null },
|
|
...opts.extraSummary,
|
|
],
|
|
// No line items on a receipt — one synthetic line, since buildHtml renders the line table
|
|
// unconditionally and an empty `lines: []` would print a header-only empty table.
|
|
lines: [
|
|
{
|
|
description: opts.lineDescription,
|
|
quantity: 1,
|
|
unitRate: opts.amount,
|
|
amount: opts.amount,
|
|
currency: opts.currency,
|
|
},
|
|
],
|
|
totals: [{ label: opts.amountLabel, amount: opts.amount, grand: true }],
|
|
sealText: opts.sealText,
|
|
qrImageUrl: receipt.qr ? pngDataUrl(receipt.qr) : null,
|
|
};
|
|
}
|