feat(invoice): informal German tone + show payment method and status

- i18n.de: switch Sie/Ihren to du/dein for salutation, thank-you line,
  customer-VAT label and payment-terms paragraph. Closing line was
  already informal.
- i18n: add paymentMethodLabel/paymentStatusLabel + per-status labels
  (paid/unpaid/partial/refunded) for both DE and EN, plus
  derivePaymentStatus helper that condenses Shopify's
  displayFinancialStatus (PAID, PARTIALLY_PAID, REFUNDED, …) into a
  4-value enum.
- loadOrderForInvoice: query Order.paymentGatewayNames and propagate it
  on the raw view-model.
- composeInvoice + types: expose paymentStatus + paymentGatewayNames on
  InvoiceViewModel (filtered/trimmed). loadDraftOrderForOffer keeps
  paymentGatewayNames empty (drafts have no gateway yet).
- InvoiceDocument: render two new meta rows on real invoices —
  'Zahlart / Payment method' (joined, prettified gateway names) and
  'Zahlstatus / Payment status' (translated label). Storno + offer kinds
  intentionally omit them.
- scripts/render-sample.ts: extend smoke checks to assert the informal
  DE wording, the new payment-method/status rows and the
  paymentStatus/paymentGatewayNames composer outputs.
This commit is contained in:
Gerhard Scheikl
2026-05-15 11:26:26 +02:00
parent dde53319e5
commit 55a0dd03f2
7 changed files with 133 additions and 7 deletions
+18
View File
@@ -134,6 +134,7 @@ function buildAtB2BOrder(): RawOrderForInvoice {
processedAt: "2026-04-15T10:00:00Z",
currencyCode: "EUR",
displayFinancialStatus: "PENDING",
paymentGatewayNames: ["manual"],
taxesIncluded: false,
customer: {
firstName: "Lukas",
@@ -278,6 +279,8 @@ async function main() {
assertEq("no notices for AT B2B with VAT charged", vm.notices.length, 0);
assert("due date 14 days after invoice date", !!vm.dueDate
&& Math.round((vm.dueDate.getTime() - vm.invoiceDate.getTime()) / 86400000) === 14);
assertEq("paymentGatewayNames propagated", vm.paymentGatewayNames.join(","), "manual");
assertEq("paymentStatus derived from displayFinancialStatus=PENDING", vm.paymentStatus, "unpaid");
console.log("• EU B2B reverse-charge notice");
const euOrder = buildEuB2BReverseChargeOrder();
@@ -398,6 +401,21 @@ async function main() {
!/Thank you for your purchase\.[\s\S]{0,40}Gerhard Berger/.test(enText),
);
// Informal German tone (du/dein) — make sure no formal "Sie/Ihren" remains
// in the strings we control (footer / signature lines come from settings).
assert("DE PDF uses informal salutation 'Hallo,'", deText.includes("Hallo,"));
assert("DE PDF no longer uses 'Sehr geehrte Damen und Herren'", !deText.includes("Sehr geehrte Damen und Herren"));
assert("DE PDF uses informal 'deine Bestellung'", deText.includes("deine Bestellung"));
assert(
"DE PDF payment-terms uses informal 'überweise … für dich'",
deText.includes("Bitte überweise") && deText.includes("für dich"),
);
assert("DE PDF shows payment status row", deText.includes("Zahlstatus"));
assert("DE PDF shows payment status value 'Offen' for PENDING", deText.includes("Offen"));
assert("DE PDF shows payment method row", deText.includes("Zahlart"));
assert("EN PDF shows payment status row", enText.includes("Payment status"));
assert("EN PDF shows payment status value 'Outstanding' for PENDING", enText.includes("Outstanding"));
// Fallback: when footerNoteEn is empty, English uses the German note.
console.log("• Footer note fallback (en → de when EN empty)");
const settingsNoEn = { ...(settings as object), footerNoteEn: "" } as never;