mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat(billing): 80mm thermal invoice layout (ADD-P001)
GET billing/invoices/:id/document?format=thermal renders a dedicated 80mm receipt template (72mm printable, 4mm margins each side), not a CSS variant of the A4 layout — the A4 CSS is absolutely-positioned/fixed-px, tuned for a 210mm page, and doesn't reflow at thermal width. No seal (not a thermal convention, renders badly on 1-bit thermal heads); line items stack (description, then qty x rate = amount) instead of a table, since a real table leaves ~10-14 chars for description at this width. PdfRenderService gains a thermal render path: full 80mm-width viewport, content height measured via page.evaluate after settle (continuous-roll receipts have no fixed page length), and a noFallback option — a Chromium failure throws a clear error instead of silently degrading to the generic A4/no-QR fallback, which would hand back a different document than what was asked for. The frontend surfaces that as a toast pointing at the existing A4 download. format is strictly validated (a4|thermal only, BadRequestException otherwise), not silently coerced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -927,6 +927,7 @@ describe("BillingService.document", () => {
|
||||
|
||||
const build = (invoice: Record<string, unknown>) => {
|
||||
const render = jest.fn().mockResolvedValue({ filename: "x.pdf", buffer: Buffer.from("") });
|
||||
const renderThermal = jest.fn().mockResolvedValue({ filename: "x-thermal.pdf", buffer: Buffer.from("") });
|
||||
const service = new BillingService(
|
||||
{} as never,
|
||||
{ findById: jest.fn().mockResolvedValue(invoice) } as never,
|
||||
@@ -934,7 +935,7 @@ describe("BillingService.document", () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{ render } as never,
|
||||
{ render, renderThermal } as never,
|
||||
{} as never,
|
||||
{
|
||||
get: (key: string) =>
|
||||
@@ -943,7 +944,7 @@ describe("BillingService.document", () => {
|
||||
: undefined,
|
||||
} as never, // config
|
||||
);
|
||||
return { service, render };
|
||||
return { service, render, renderThermal };
|
||||
};
|
||||
|
||||
it("adds no EIMS IRN row and no QR for an unregistered invoice", async () => {
|
||||
@@ -1000,4 +1001,24 @@ describe("BillingService.document", () => {
|
||||
expect(model.summary).toContainEqual({ label: "EIMS IRN", value: "IRN-123" });
|
||||
expect(model.qrImageUrl).toBe("data:image/png;base64,signed-payload");
|
||||
});
|
||||
|
||||
it("calls render (not renderThermal) for the default format", async () => {
|
||||
const { service, render, renderThermal } = build(invoiceRow());
|
||||
jest.spyOn(service as never, "toDocumentModel").mockResolvedValue({} as never);
|
||||
|
||||
await service.document("inv-1");
|
||||
|
||||
expect(render).toHaveBeenCalledTimes(1);
|
||||
expect(renderThermal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls renderThermal (not render) for format 'thermal'", async () => {
|
||||
const { service, render, renderThermal } = build(invoiceRow());
|
||||
jest.spyOn(service as never, "toDocumentModel").mockResolvedValue({} as never);
|
||||
|
||||
await service.document("inv-1", "thermal");
|
||||
|
||||
expect(renderThermal).toHaveBeenCalledTimes(1);
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user