refactor(invoice): drop dead paid/paidStamp; classify VOIDED; warn on unknown payment status

Audit cleanup of payment-status code paths uncovered while shipping
the partial-refund fix:

#1 Drop `viewModel.paid` (boolean). It was set from
   `displayFinancialStatus === "PAID"` and never read anywhere. With
   refunds in the picture it had become a footgun: a fully refunded
   order that started PAID would still satisfy `paid === true`, but
   `paymentStatus === "refunded"`. Callers should use `paymentStatus`
   / `requiresPayment` exclusively.

#2 Remove the unused `paidStamp` translation ("BEZAHLT" / "PAID").
   Defined in both locales but never rendered.

#3 Classify VOIDED orders as a distinct `"voided"` payment status
   (rendered "Annulliert" / "Voided") instead of "unpaid". A voided
   order had its authorisation cancelled before capture — no money
   was received and none is owed. The previous "Offen" / "Outstanding"
   label combined with a GiroCode would have invited the customer to
   pay an order that's already been called off. `requiresPayment`
   now also excludes `"voided"`, so GiroCode + payment-terms
   paragraph are suppressed (mirrors the `"refunded"` treatment).
   "Annulliert" is used in German rather than "Storniert" to avoid
   confusion with our storno cancellation document concept.

#6 `derivePaymentStatus` now logs a `console.warn` when it
   encounters a non-empty `displayFinancialStatus` value that isn't
   one of the documented Shopify enum members (PAID, PARTIALLY_PAID,
   REFUNDED, PARTIALLY_REFUNDED, VOIDED, PENDING, AUTHORIZED,
   EXPIRED). Future Shopify enum additions will surface in logs
   instead of silently mapping to "unpaid".

EXPIRED stays mapped to "unpaid" — abandoned-checkout-style edge
case left intentionally for a separate decision (#4 in the audit).

Verification: render-sample now also exercises a VOIDED fixture
(status row "Annulliert", no GiroCode, no payment terms). tsc /
smoke / tests / build all green.
This commit is contained in:
Gerhard Scheikl
2026-05-15 18:12:06 +02:00
parent 91c1a74c1b
commit c5b6bfc20d
4 changed files with 61 additions and 10 deletions
+5 -3
View File
@@ -86,7 +86,6 @@ export function composeInvoice({
? addDays(invoiceDate, settings.paymentTermDays)
: undefined;
const paid = (order.displayFinancialStatus || "").toUpperCase() === "PAID";
// Refunded gross amount, mirrored from Shopify's `totalRefundedSet`.
// Storno/offer documents don't carry a refund row — a storno *is*
// already the cancellation document, and offers have no payments yet.
@@ -114,7 +113,11 @@ export function composeInvoice({
// paid orders both have a 0 outstanding balance — the difference is
// just whether the money was kept (`paid`) or returned (`refunded`).
const requiresPayment =
!storno && !offer && paymentStatus !== "paid" && paymentStatus !== "refunded";
!storno &&
!offer &&
paymentStatus !== "paid" &&
paymentStatus !== "refunded" &&
paymentStatus !== "voided";
const paymentGatewayNames = (order.paymentGatewayNames ?? []).filter(
(n) => typeof n === "string" && n.trim().length > 0,
);
@@ -157,7 +160,6 @@ export function composeInvoice({
lines,
totals,
notices,
paid,
paymentStatus,
requiresPayment,
refundedAmount,