Files
linumiq-invoice/app/services/invoice/i18n.ts
T
Gerhard Scheikl d742e75419 fix(invoice): detect pickup via DeliveryMethodType and show 'Abholort: <location>' meta row
- Use Order.fulfillmentOrders.deliveryMethod.methodType === 'PICK_UP' as the
  primary signal (Shopify Local Pickup app exposes this reliably; the
  shippingLine title is just the location name with no 'pickup' keyword).
  Keep the legacy shippingLine string heuristic as a fallback for custom
  shipping rates merchants name 'Abholung'/'Pickup'.
- Surface assignedLocation.name as pickupLocationName on the view model.
- Replace the 'Versandart: <location name>' row with 'Abholort: <location>'
  (DE) / 'Pick-up location: <location>' (EN); falls back to plain
  'Abholung'/'Pick-up' when the location name is unavailable.
2026-05-15 14:46:55 +02:00

264 lines
8.6 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;
paymentMethodLabel: string;
paymentStatusLabel: string;
paymentStatusPaid: string;
paymentStatusUnpaid: string;
paymentStatusPartial: string;
paymentStatusRefunded: string;
orderNumberLabel: string;
shippingAddressHeading: string;
shippingMethodLabel: string;
trackingLabel: string;
shippingItemPrefix: string;
discountCodeLabel: string;
pickupLabel: string;
/** 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;
}
/** Status displayed for the order's payment, derived from Shopify's
* `displayFinancialStatus`. */
export type PaymentStatus = "paid" | "unpaid" | "partial" | "refunded";
export function paymentStatusLabel(
status: PaymentStatus,
strings: InvoiceStrings,
): string {
switch (status) {
case "paid":
return strings.paymentStatusPaid;
case "partial":
return strings.paymentStatusPartial;
case "refunded":
return strings.paymentStatusRefunded;
default:
return strings.paymentStatusUnpaid;
}
}
/** Maps Shopify's `displayFinancialStatus` to our condensed enum. Values not
* signalling actual receipt of money map to "unpaid". */
export function derivePaymentStatus(
displayFinancialStatus: string | null | undefined,
): PaymentStatus {
const v = (displayFinancialStatus || "").toUpperCase();
if (v === "PAID") return "paid";
if (v === "PARTIALLY_PAID") return "partial";
if (v === "REFUNDED" || v === "PARTIALLY_REFUNDED") return "refunded";
return "unpaid";
}
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: "Deine USt-Id.",
position: "Pos.",
description: "Beschreibung",
quantity: "Menge",
unitPrice: "Einzelpreis",
totalPrice: "Gesamtpreis",
netTotal: "Gesamtbetrag netto",
vatLine: (r) => `zzgl. Umsatzsteuer ${r}`,
grossTotal: "Gesamtbetrag brutto",
salutationGeneric: "Hallo,",
thankYouLine:
"vielen Dank für deine Bestellung. Wir berechnen dir folgende Leistungen:",
closing: "Danke für deinen Einkauf",
paymentTerms: (days, due) =>
`Bitte überweise den Rechnungsbetrag innerhalb von ${days} Tagen, spätestens bis zum ${due}, auf das unten angegebene Konto. Bei Fragen zur Rechnung sind wir gerne für dich da.`,
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",
paymentMethodLabel: "Zahlart",
paymentStatusLabel: "Zahlstatus",
paymentStatusPaid: "Bezahlt",
paymentStatusUnpaid: "Offen",
paymentStatusPartial: "Teilweise bezahlt",
paymentStatusRefunded: "Erstattet",
orderNumberLabel: "Bestellnummer",
shippingAddressHeading: "Lieferadresse",
shippingMethodLabel: "Versandart",
trackingLabel: "Sendungsnummer",
shippingItemPrefix: "Versand",
discountCodeLabel: "Rabattcode",
pickupLabel: "Abholung",
pickupLocationLabel: "Abholort",
};
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",
paymentMethodLabel: "Payment method",
paymentStatusLabel: "Payment status",
paymentStatusPaid: "Paid",
paymentStatusUnpaid: "Outstanding",
paymentStatusPartial: "Partially paid",
paymentStatusRefunded: "Refunded",
orderNumberLabel: "Order no.",
shippingAddressHeading: "Shipping address",
shippingMethodLabel: "Shipping method",
trackingLabel: "Tracking no.",
shippingItemPrefix: "Shipping",
discountCodeLabel: "Discount code",
pickupLabel: "Pick-up",
pickupLocationLabel: "Pick-up location",
};
// 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;
}