diff --git a/app/services/invoice/i18n.ts b/app/services/invoice/i18n.ts index c371c41..a956b64 100644 --- a/app/services/invoice/i18n.ts +++ b/app/services/invoice/i18n.ts @@ -68,6 +68,16 @@ export interface InvoiceStrings { /** Used as the meta-row label when the order is a local pickup. The row * value is then the pickup location name (e.g. "Lager Graz"). */ pickupLocationLabel: string; + /** Localized labels for Shopify's built-in payment-gateway names. The + * Admin GraphQL API only ever returns the *English* template name + * (e.g. "Bank Deposit") in `Order.paymentGatewayNames`, even when the + * storefront / order-confirmation page renders the localized variant + * ("Banküberweisung"). We mirror Shopify's checkout copy here so the + * printed PDF matches what the customer saw at checkout. Lookup is + * case-insensitive on the normalized key (lowercased, separators + * collapsed). Unknown gateways fall back to a title-cased rendering + * of the raw name. */ + paymentGatewayLabels: Record; } /** Status displayed for the order's payment, derived from Shopify's @@ -171,6 +181,23 @@ const de: InvoiceStrings = { discountCodeLabel: "Rabattcode", pickupLabel: "Abholung", pickupLocationLabel: "Abholort", + paymentGatewayLabels: { + // Built-in Shopify manual payment methods (template names). + "bank deposit": "Banküberweisung", + "bank transfer": "Banküberweisung", + "money order": "Postanweisung", + "cash on delivery": "Nachnahme", + "cash on delivery (cod)": "Nachnahme", + // Generic / technical gateways. + manual: "Manuelle Zahlung", + bogus: "Bogus (Test)", + "shopify payments": "Shopify Payments", + paypal: "PayPal", + "paypal express checkout": "PayPal", + klarna: "Klarna", + sofort: "Sofort", + giropay: "Giropay", + }, }; const en: InvoiceStrings = { @@ -241,6 +268,21 @@ const en: InvoiceStrings = { discountCodeLabel: "Discount code", pickupLabel: "Pick-up", pickupLocationLabel: "Pick-up location", + paymentGatewayLabels: { + "bank deposit": "Bank deposit", + "bank transfer": "Bank transfer", + "money order": "Money order", + "cash on delivery": "Cash on delivery", + "cash on delivery (cod)": "Cash on delivery (COD)", + manual: "Manual", + bogus: "Bogus (Test)", + "shopify payments": "Shopify Payments", + paypal: "PayPal", + "paypal express checkout": "PayPal", + klarna: "Klarna", + sofort: "Sofort", + giropay: "Giropay", + }, }; // Locale → invoice language. We only render in German (`de`) when the diff --git a/app/services/invoice/pdf/InvoiceDocument.tsx b/app/services/invoice/pdf/InvoiceDocument.tsx index 638105b..bed32d0 100644 --- a/app/services/invoice/pdf/InvoiceDocument.tsx +++ b/app/services/invoice/pdf/InvoiceDocument.tsx @@ -311,7 +311,7 @@ export function InvoiceDocument({ invoice }: DocProps) { {t.paymentMethodLabel} - {invoice.paymentGatewayNames.map(prettifyGatewayName).join(", ")} + {invoice.paymentGatewayNames.map((n) => prettifyGatewayName(n, t.paymentGatewayLabels)).join(", ")} )} @@ -643,24 +643,24 @@ function normaliseWebUrl(url: string): string { /** * Turn a Shopify payment-gateway machine name (e.g. `shopify_payments`, - * `manual`, `bogus`) into something a customer can read on the invoice. We - * keep this purely cosmetic — the underlying value is preserved for any - * downstream automation. + * `manual`, `bogus`) or a built-in manual-payment template name (e.g. + * `Bank Deposit`, `Money Order`) into the localized customer-facing label + * shown on the invoice. The Shopify Admin API only exposes English + * template names — see `InvoiceStrings.paymentGatewayLabels` for the + * rationale. + * + * Lookup is keyed on the *normalized* name (lowercased, separators + * collapsed). Unknown gateways fall back to a title-cased rendering + * of the raw name so we never silently print empty meta-rows. */ -function prettifyGatewayName(name: string): string { - const known: Record = { - manual: "Manual", - bogus: "Bogus (Test)", - shopify_payments: "Shopify Payments", - paypal: "PayPal", - cash_on_delivery: "Cash on delivery", - "cash-on-delivery": "Cash on delivery", - }; - const key = name.trim().toLowerCase(); - if (known[key]) return known[key]; - // Replace separators and title-case each word. +function prettifyGatewayName( + name: string, + labels: Record, +): string { + const key = name.trim().toLowerCase().replace(/[_\-]+/g, " ").replace(/\s+/g, " "); + if (labels[key]) return labels[key]; return key - .split(/[_\s-]+/) + .split(" ") .filter(Boolean) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); diff --git a/scripts/render-sample.ts b/scripts/render-sample.ts index 6eaae78..d45f7dc 100644 --- a/scripts/render-sample.ts +++ b/scripts/render-sample.ts @@ -554,6 +554,14 @@ async function main() { 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")); + // The Shopify Admin GraphQL API returns the *English* template name for + // built-in manual payment gateways even on German-locale shops — we + // localize it ourselves via i18n.paymentGatewayLabels so the PDF matches + // what the customer saw on the order-confirmation page. + assert("DE PDF localizes 'manual' gateway to 'Manuelle Zahlung'", + deText.includes("Manuelle Zahlung")); + assert("DE PDF no longer shows raw English 'Manual' as gateway label", + !/Zahlart[\s\S]{0,20}Manual\b/.test(deText)); assert("EN PDF shows payment status row", enText.includes("Payment status")); assert("EN PDF shows payment status value 'Outstanding' for PENDING", enText.includes("Outstanding"));