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:
@@ -11,7 +11,7 @@ import type {
|
||||
VatBreakdownEntry,
|
||||
} from "./types";
|
||||
import { addDays } from "./format";
|
||||
import { pickLanguage, type InvoiceLanguage } from "./i18n";
|
||||
import { derivePaymentStatus, pickLanguage, type InvoiceLanguage } from "./i18n";
|
||||
|
||||
interface ComposeArgs {
|
||||
order: RawOrderForInvoice;
|
||||
@@ -69,6 +69,10 @@ export function composeInvoice({
|
||||
: undefined;
|
||||
|
||||
const paid = (order.displayFinancialStatus || "").toUpperCase() === "PAID";
|
||||
const paymentStatus = derivePaymentStatus(order.displayFinancialStatus);
|
||||
const paymentGatewayNames = (order.paymentGatewayNames ?? []).filter(
|
||||
(n) => typeof n === "string" && n.trim().length > 0,
|
||||
);
|
||||
|
||||
if (storno) {
|
||||
lines = lines.map((l) => ({
|
||||
@@ -107,6 +111,8 @@ export function composeInvoice({
|
||||
totals,
|
||||
notices,
|
||||
paid,
|
||||
paymentStatus,
|
||||
paymentGatewayNames,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,44 @@ export interface InvoiceStrings {
|
||||
webLabel: string;
|
||||
phoneLabel: string;
|
||||
paidStamp: string;
|
||||
paymentMethodLabel: string;
|
||||
paymentStatusLabel: string;
|
||||
paymentStatusPaid: string;
|
||||
paymentStatusUnpaid: string;
|
||||
paymentStatusPartial: string;
|
||||
paymentStatusRefunded: 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 = {
|
||||
@@ -65,7 +103,7 @@ const de: InvoiceStrings = {
|
||||
invoiceNumber: "Rechnungs-Nr.",
|
||||
invoiceDate: "Rechnungsdatum",
|
||||
deliveryDate: "Lieferdatum",
|
||||
customerVatId: "Ihre USt-Id.",
|
||||
customerVatId: "Deine USt-Id.",
|
||||
position: "Pos.",
|
||||
description: "Beschreibung",
|
||||
quantity: "Menge",
|
||||
@@ -74,12 +112,12 @@ const de: InvoiceStrings = {
|
||||
netTotal: "Gesamtbetrag netto",
|
||||
vatLine: (r) => `zzgl. Umsatzsteuer ${r}`,
|
||||
grossTotal: "Gesamtbetrag brutto",
|
||||
salutationGeneric: "Sehr geehrte Damen und Herren,",
|
||||
salutationGeneric: "Hallo,",
|
||||
thankYouLine:
|
||||
"vielen Dank für Ihren Auftrag. Wir erlauben uns, Ihnen folgende Leistungen in Rechnung zu stellen:",
|
||||
"vielen Dank für deine Bestellung. Wir berechnen dir folgende Leistungen:",
|
||||
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.`,
|
||||
`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",
|
||||
@@ -109,6 +147,12 @@ const de: InvoiceStrings = {
|
||||
webLabel: "Web",
|
||||
phoneLabel: "Tel.",
|
||||
paidStamp: "BEZAHLT",
|
||||
paymentMethodLabel: "Zahlart",
|
||||
paymentStatusLabel: "Zahlstatus",
|
||||
paymentStatusPaid: "Bezahlt",
|
||||
paymentStatusUnpaid: "Offen",
|
||||
paymentStatusPartial: "Teilweise bezahlt",
|
||||
paymentStatusRefunded: "Erstattet",
|
||||
};
|
||||
|
||||
const en: InvoiceStrings = {
|
||||
@@ -165,6 +209,12 @@ const en: InvoiceStrings = {
|
||||
webLabel: "Web",
|
||||
phoneLabel: "Tel.",
|
||||
paidStamp: "PAID",
|
||||
paymentMethodLabel: "Payment method",
|
||||
paymentStatusLabel: "Payment status",
|
||||
paymentStatusPaid: "Paid",
|
||||
paymentStatusUnpaid: "Outstanding",
|
||||
paymentStatusPartial: "Partially paid",
|
||||
paymentStatusRefunded: "Refunded",
|
||||
};
|
||||
|
||||
// Locale → invoice language. We only render in German (`de`) when the
|
||||
|
||||
@@ -160,6 +160,7 @@ export async function loadDraftOrderForOffer(
|
||||
processedAt: null,
|
||||
currencyCode: draft.currencyCode,
|
||||
displayFinancialStatus: null,
|
||||
paymentGatewayNames: [],
|
||||
taxesIncluded: draft.taxesIncluded,
|
||||
customer: draft.customer,
|
||||
billingAddress: draft.billingAddress,
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface RawOrderForInvoice {
|
||||
processedAt: string | null;
|
||||
currencyCode: string;
|
||||
displayFinancialStatus: string | null;
|
||||
paymentGatewayNames: string[];
|
||||
customer: {
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
@@ -77,6 +78,7 @@ const QUERY = `#graphql
|
||||
processedAt
|
||||
currencyCode
|
||||
displayFinancialStatus
|
||||
paymentGatewayNames
|
||||
taxesIncluded
|
||||
customer {
|
||||
firstName
|
||||
@@ -161,6 +163,7 @@ interface RawAdminResponse {
|
||||
processedAt: string | null;
|
||||
currencyCode: string;
|
||||
displayFinancialStatus: string | null;
|
||||
paymentGatewayNames: string[] | null;
|
||||
taxesIncluded: boolean;
|
||||
customer: {
|
||||
firstName: string | null;
|
||||
@@ -213,6 +216,7 @@ export async function loadOrderForInvoice(
|
||||
processedAt: order.processedAt,
|
||||
currencyCode: order.currencyCode,
|
||||
displayFinancialStatus: order.displayFinancialStatus,
|
||||
paymentGatewayNames: order.paymentGatewayNames ?? [],
|
||||
taxesIncluded: order.taxesIncluded,
|
||||
customer: order.customer,
|
||||
billingAddress: order.billingAddress,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import React from "react";
|
||||
|
||||
import { formatDate, formatMoney, formatQuantity, formatTaxRate } from "../format";
|
||||
import { getStrings } from "../i18n";
|
||||
import { getStrings, paymentStatusLabel as getPaymentStatusLabel } from "../i18n";
|
||||
import type { InvoiceLanguage } from "../i18n";
|
||||
import type { InvoiceViewModel, InvoiceLine, IssuerData, RecipientData } from "../types";
|
||||
|
||||
@@ -287,6 +287,22 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
<Text style={styles.metaValue}>{invoice.recipientVatId}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{invoice.kind === "invoice" && invoice.paymentGatewayNames.length > 0 && (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.paymentMethodLabel}</Text>
|
||||
<Text style={styles.metaValue}>
|
||||
{invoice.paymentGatewayNames.map(prettifyGatewayName).join(", ")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{invoice.kind === "invoice" && (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.paymentStatusLabel}</Text>
|
||||
<Text style={styles.metaValue}>
|
||||
{getPaymentStatusLabel(invoice.paymentStatus, t)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -545,3 +561,28 @@ function normaliseWebUrl(url: string): string {
|
||||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||||
return `https://${trimmed.replace(/^\/\//, "")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function prettifyGatewayName(name: string): string {
|
||||
const known: Record<string, string> = {
|
||||
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.
|
||||
return key
|
||||
.split(/[_\s-]+/)
|
||||
.filter(Boolean)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InvoiceLanguage } from "./i18n";
|
||||
import type { InvoiceLanguage, PaymentStatus } from "./i18n";
|
||||
|
||||
/**
|
||||
* The view model passed into the PDF renderer. Decouples the PDF layer from
|
||||
@@ -40,6 +40,12 @@ export interface InvoiceViewModel {
|
||||
|
||||
// Status flags
|
||||
paid: boolean;
|
||||
/** Condensed payment status derived from Shopify's
|
||||
* `displayFinancialStatus`. */
|
||||
paymentStatus: PaymentStatus;
|
||||
/** Names of the payment gateways used (e.g. ["bogus"], ["manual",
|
||||
* "shopify_payments"]). Empty when unknown / draft. */
|
||||
paymentGatewayNames: string[];
|
||||
}
|
||||
|
||||
export interface IssuerData {
|
||||
|
||||
Reference in New Issue
Block a user