From ff5c435bd4ed402e6bd9a79ee17307c0334b4553 Mon Sep 17 00:00:00 2001
From: Gerhard Scheikl
Die Rechnung befindet sich im Anhang.
-
-Bei Überweisung bitte die Rechnungs-Nummer als Referenz verwenden:
-{{invoiceNumber}}Danke für deinen Einkauf!
-Besten Dank!
-
+Bei Überweisung bitte die Rechnungs-Nummer als Referenz verwenden:
+{{invoiceNumber}}
+Besten Dank!
+
+Besten Dank! +
`; + +const EN_HEAD = `\Please find the invoice attached. -
-
-When paying by bank transfer, please use the invoice number as the reference:
-{{invoiceNumber}}
-Thanks a lot!
-
+When paying by bank transfer, please use the invoice number as the reference:
+{{invoiceNumber}}
+Thanks a lot!
+
+Thanks a lot! +
`; + +// Unpaid (outstanding) bodies — keep the bank-transfer reference hint. +const DE_HTML_UNPAID = `${DE_HEAD}\n${DE_PAYMENT_HINT}\n${DE_FOOT}`; +const EN_HTML_UNPAID = `${EN_HEAD}\n${EN_PAYMENT_HINT}\n${EN_FOOT}`; + +// Paid bodies — bank-transfer hint removed. +const DE_HTML_PAID = `${DE_HEAD}\n${DE_PAID_NOTE}\n${DE_FOOT}`; +const EN_HTML_PAID = `${EN_HEAD}\n${EN_PAID_NOTE}\n${EN_FOOT}`; + export const DEFAULT_EMAIL_SUBJECT_DE = "Rechnung {{invoiceNumber}} – {{companyName}}"; export const DEFAULT_EMAIL_SUBJECT_EN = "Invoice {{invoiceNumber}} – {{companyName}}"; -export const DEFAULT_EMAIL_BODY_DE = DE_HTML; -export const DEFAULT_EMAIL_BODY_EN = EN_HTML; + +// Backwards-compatible aliases (these are the "unpaid" variants, matching the +// historical single-template behaviour). +export const DEFAULT_EMAIL_BODY_DE = DE_HTML_UNPAID; +export const DEFAULT_EMAIL_BODY_EN = EN_HTML_UNPAID; + +export const DEFAULT_EMAIL_BODY_DE_UNPAID = DE_HTML_UNPAID; +export const DEFAULT_EMAIL_BODY_EN_UNPAID = EN_HTML_UNPAID; +export const DEFAULT_EMAIL_BODY_DE_PAID = DE_HTML_PAID; +export const DEFAULT_EMAIL_BODY_EN_PAID = EN_HTML_PAID; diff --git a/app/services/invoice/generateInvoice.server.tsx b/app/services/invoice/generateInvoice.server.tsx index 8633aba..ac04de4 100644 --- a/app/services/invoice/generateInvoice.server.tsx +++ b/app/services/invoice/generateInvoice.server.tsx @@ -131,6 +131,7 @@ export async function generateInvoice( const version = latest ? latest.version + 1 : 1; const totalsJson = JSON.stringify(viewModel.totals); + const paymentStatus = viewModel.paymentStatus; const customerJson = JSON.stringify({ recipient: viewModel.recipient, isB2B: viewModel.isB2B, @@ -148,6 +149,7 @@ export async function generateInvoice( pdfUrl: upload.url, totalsJson, customerJson, + paymentStatus, issuedAt: new Date(), status: "issued", lastError: "", @@ -167,6 +169,7 @@ export async function generateInvoice( pdfUrl: upload.url, totalsJson, customerJson, + paymentStatus, status: "issued", }, }); diff --git a/prisma/migrations/20260824000000_add_invoice_payment_status/migration.sql b/prisma/migrations/20260824000000_add_invoice_payment_status/migration.sql new file mode 100644 index 0000000..105187c --- /dev/null +++ b/prisma/migrations/20260824000000_add_invoice_payment_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Invoice" ADD COLUMN "paymentStatus" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5f77eed..4c5bcd0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -141,7 +141,12 @@ model Invoice { // Snapshots (JSON strings on sqlite) totalsJson String @default("{}") - customerJson String @default("{}") + customerJson String @default("") + + // Payment state at generation time ("paid" | "partial" | "unpaid" | + // "refunded" | "voided"). Used to pick the paid/unpaid email template at + // send time. Empty for legacy rows → treated as "unpaid". + paymentStatus String @default("") // Lifecycle issuedAt DateTime @default(now()) diff --git a/tests/email-templates.test.ts b/tests/email-templates.test.ts new file mode 100644 index 0000000..96b67c2 --- /dev/null +++ b/tests/email-templates.test.ts @@ -0,0 +1,39 @@ +import { strict as assert } from "node:assert"; +import { describe, it } from "node:test"; + +import { + DEFAULT_EMAIL_BODY_DE_PAID, + DEFAULT_EMAIL_BODY_DE_UNPAID, + DEFAULT_EMAIL_BODY_EN_PAID, + DEFAULT_EMAIL_BODY_EN_UNPAID, +} from "../app/services/invoice/emailTemplates"; + +describe("invoice email templates (paid vs unpaid)", () => { + const HINT_EN = /bank transfer/i; + const HINT_DE = /Überweisung/i; + + it("unpaid defaults keep the bank-transfer reference hint", () => { + assert.match(DEFAULT_EMAIL_BODY_EN_UNPAID, HINT_EN); + assert.match(DEFAULT_EMAIL_BODY_EN_UNPAID, /\{\{invoiceNumber\}\}/); + assert.match(DEFAULT_EMAIL_BODY_DE_UNPAID, HINT_DE); + assert.match(DEFAULT_EMAIL_BODY_DE_UNPAID, /\{\{invoiceNumber\}\}/); + }); + + it("paid defaults omit the bank-transfer reference hint", () => { + assert.doesNotMatch(DEFAULT_EMAIL_BODY_EN_PAID, HINT_EN); + assert.doesNotMatch(DEFAULT_EMAIL_BODY_DE_PAID, HINT_DE); + }); + + it("paid and unpaid variants otherwise share header/footer structure", () => { + for (const tpl of [ + DEFAULT_EMAIL_BODY_EN_PAID, + DEFAULT_EMAIL_BODY_EN_UNPAID, + DEFAULT_EMAIL_BODY_DE_PAID, + DEFAULT_EMAIL_BODY_DE_UNPAID, + ]) { + assert.match(tpl, /cid:invoice-logo/); + assert.match(tpl, /\{\{companyName\}\}/); + assert.match(tpl, /\{\{shopEmail\}\}/); + } + }); +});