dde53319e5
- New custom server.js (replaces react-router-serve): ISO timestamps on all console.* output and on access logs, and skip successful /healthz polls so real traffic stays visible. - New ProcessedWebhook table + dedupe helper keyed on X-Shopify-Webhook-Id; stops Shopify retries from triggering a second invoice email when the original delivery exceeded the 5s ack timeout. - orders/create + orders/fulfilled now respond 200 immediately and run the PDF/email work in the background so we stay under that timeout. - pickLanguage(): non-German locales (it, fr, es, ...) now default to English instead of falling back to German. Empty/unknown still maps to 'de' so the per-shop defaultLanguage chain keeps working. - Tests for pickLanguage and dedupe via node --test + tsx.
188 lines
6.1 KiB
TypeScript
188 lines
6.1 KiB
TypeScript
// Translatable strings for invoice rendering. Two languages: de (default), en.
|
|
|
|
export type InvoiceLanguage = "de" | "en";
|
|
|
|
export interface InvoiceStrings {
|
|
invoice: string;
|
|
stornoInvoice: string;
|
|
offer: string;
|
|
offerNumber: string;
|
|
offerDate: string;
|
|
offerValidUntil: (until: string) => string;
|
|
stornoReference: (originalNumber: string) => string;
|
|
invoiceNumber: string;
|
|
invoiceDate: string;
|
|
deliveryDate: string;
|
|
customerVatId: string;
|
|
position: string;
|
|
description: string;
|
|
quantity: string;
|
|
unitPrice: string;
|
|
totalPrice: string;
|
|
netTotal: string;
|
|
vatLine: (ratePct: string) => string;
|
|
grossTotal: string;
|
|
salutationGeneric: string;
|
|
thankYouLine: string;
|
|
closing: string;
|
|
paymentTerms: (days: number, dueDate: string) => string;
|
|
paymentTermsImmediate: string;
|
|
giroCodeCaption: string;
|
|
reverseChargeNotice: string;
|
|
exportNotice: string;
|
|
kleinunternehmerNotice: string;
|
|
pieceUnit: string;
|
|
page: (current: number, total: number) => string;
|
|
legalCourtLabel: string;
|
|
fnLabel: string;
|
|
vatIdLabel: string;
|
|
taxNumberLabel: string;
|
|
ownerLabel: string;
|
|
ibanLabel: string;
|
|
bicLabel: string;
|
|
bankLabel: string;
|
|
recipientLabel: string;
|
|
amountLabel: string;
|
|
referenceLabel: string;
|
|
addressHeading: string;
|
|
contactHeading: string;
|
|
legalHeading: string;
|
|
bankHeading: string;
|
|
emailLabel: string;
|
|
webLabel: string;
|
|
phoneLabel: string;
|
|
paidStamp: string;
|
|
}
|
|
|
|
const de: InvoiceStrings = {
|
|
invoice: "Rechnung",
|
|
stornoInvoice: "Stornorechnung",
|
|
offer: "Angebot",
|
|
offerNumber: "Angebots-Nr.",
|
|
offerDate: "Angebotsdatum",
|
|
offerValidUntil: (d) => `Dieses Angebot ist gültig bis ${d}.`,
|
|
stornoReference: (n) => `Storno zu Rechnung Nr. ${n}`,
|
|
invoiceNumber: "Rechnungs-Nr.",
|
|
invoiceDate: "Rechnungsdatum",
|
|
deliveryDate: "Lieferdatum",
|
|
customerVatId: "Ihre USt-Id.",
|
|
position: "Pos.",
|
|
description: "Beschreibung",
|
|
quantity: "Menge",
|
|
unitPrice: "Einzelpreis",
|
|
totalPrice: "Gesamtpreis",
|
|
netTotal: "Gesamtbetrag netto",
|
|
vatLine: (r) => `zzgl. Umsatzsteuer ${r}`,
|
|
grossTotal: "Gesamtbetrag brutto",
|
|
salutationGeneric: "Sehr geehrte Damen und Herren,",
|
|
thankYouLine:
|
|
"vielen Dank für Ihren Auftrag. Wir erlauben uns, Ihnen folgende Leistungen in Rechnung zu stellen:",
|
|
closing: "Danke für deinen Einkauf",
|
|
paymentTerms: (days, due) =>
|
|
`Bitte überweisen Sie den Rechnungsbetrag innerhalb von ${days} Tagen, spätestens bis zum ${due}, auf das unten angegebene Konto. Bei Fragen zur Rechnung stehen wir Ihnen gerne zur Verfügung.`,
|
|
paymentTermsImmediate:
|
|
"Der Rechnungsbetrag ist sofort nach Erhalt zur Zahlung fällig.",
|
|
giroCodeCaption: "GiroCode",
|
|
reverseChargeNotice:
|
|
"Steuerschuldnerschaft des Leistungsempfängers gemäß Art. 196 MwStSystRL (Reverse Charge).",
|
|
exportNotice: "Steuerfreie Ausfuhrlieferung gemäß § 7 UStG.",
|
|
kleinunternehmerNotice:
|
|
"Gemäß § 6 Abs. 1 Z 27 UStG wird keine Umsatzsteuer ausgewiesen (Kleinunternehmer).",
|
|
pieceUnit: "Stk",
|
|
page: (c, t) => `${c}/${t}`,
|
|
legalCourtLabel: "Amtsgericht",
|
|
fnLabel: "FN",
|
|
vatIdLabel: "UID",
|
|
taxNumberLabel: "St.Nr.",
|
|
ownerLabel: "Inhaber",
|
|
ibanLabel: "IBAN",
|
|
bicLabel: "BIC",
|
|
bankLabel: "Bank",
|
|
recipientLabel: "Empfänger",
|
|
amountLabel: "Betrag",
|
|
referenceLabel: "Referenz",
|
|
addressHeading: "Adresse",
|
|
contactHeading: "Kontakt",
|
|
legalHeading: "Rechtliches",
|
|
bankHeading: "Bankverbindung",
|
|
emailLabel: "E-Mail",
|
|
webLabel: "Web",
|
|
phoneLabel: "Tel.",
|
|
paidStamp: "BEZAHLT",
|
|
};
|
|
|
|
const en: InvoiceStrings = {
|
|
invoice: "Invoice",
|
|
stornoInvoice: "Cancellation invoice",
|
|
offer: "Offer",
|
|
offerNumber: "Offer no.",
|
|
offerDate: "Offer date",
|
|
offerValidUntil: (d) => `This offer is valid until ${d}.`,
|
|
stornoReference: (n) => `Cancels invoice no. ${n}`,
|
|
invoiceNumber: "Invoice no.",
|
|
invoiceDate: "Invoice date",
|
|
deliveryDate: "Delivery date",
|
|
customerVatId: "Your VAT ID",
|
|
position: "Pos.",
|
|
description: "Description",
|
|
quantity: "Qty",
|
|
unitPrice: "Unit price",
|
|
totalPrice: "Total",
|
|
netTotal: "Net total",
|
|
vatLine: (r) => `plus VAT ${r}`,
|
|
grossTotal: "Gross total",
|
|
salutationGeneric: "Dear Sir or Madam,",
|
|
thankYouLine:
|
|
"Thank you for your order. We hereby invoice you for the following:",
|
|
closing: "Thank you for your purchase.",
|
|
paymentTerms: (days, due) =>
|
|
`Please transfer the invoice amount within ${days} days, no later than ${due}, to the bank account shown below.`,
|
|
paymentTermsImmediate: "The invoice amount is due immediately upon receipt.",
|
|
giroCodeCaption: "GiroCode",
|
|
reverseChargeNotice:
|
|
"Reverse charge: VAT to be accounted for by the recipient pursuant to Art. 196 of Council Directive 2006/112/EC.",
|
|
exportNotice: "Tax-exempt export delivery pursuant to § 7 UStG.",
|
|
kleinunternehmerNotice:
|
|
"VAT is not charged pursuant to § 6 (1) 27 UStG (small-business exemption).",
|
|
pieceUnit: "pcs",
|
|
page: (c, t) => `${c}/${t}`,
|
|
legalCourtLabel: "Commercial court",
|
|
fnLabel: "FN",
|
|
vatIdLabel: "VAT ID",
|
|
taxNumberLabel: "Tax no.",
|
|
ownerLabel: "Owner",
|
|
ibanLabel: "IBAN",
|
|
bicLabel: "BIC",
|
|
bankLabel: "Bank",
|
|
recipientLabel: "Recipient",
|
|
amountLabel: "Amount",
|
|
referenceLabel: "Reference",
|
|
addressHeading: "Address",
|
|
contactHeading: "Contact",
|
|
legalHeading: "Legal",
|
|
bankHeading: "Bank details",
|
|
emailLabel: "E-mail",
|
|
webLabel: "Web",
|
|
phoneLabel: "Tel.",
|
|
paidStamp: "PAID",
|
|
};
|
|
|
|
// Locale → invoice language. We only render in German (`de`) when the
|
|
// caller is explicitly German-speaking (de, de-AT, de-DE, de_CH, …).
|
|
// Everything else (it, fr, es, en, …) falls back to English so that
|
|
// non-German-speaking customers don't receive a German invoice. Callers
|
|
// that have a per-shop default fall back to it via
|
|
// `pickLanguage(customerLocale ?? settings.defaultLanguage)`, which is why
|
|
// `null`/`undefined` still maps to `de` (the legacy default for the
|
|
// Austrian shops this app was built for).
|
|
export function pickLanguage(input: string | null | undefined): InvoiceLanguage {
|
|
if (!input) return "de";
|
|
const v = input.toLowerCase();
|
|
if (v.startsWith("de")) return "de";
|
|
return "en";
|
|
}
|
|
|
|
export function getStrings(language: InvoiceLanguage): InvoiceStrings {
|
|
return language === "en" ? en : de;
|
|
}
|