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:
@@ -66,13 +66,13 @@ export interface InvoiceStrings {
|
||||
emailLabel: string;
|
||||
webLabel: string;
|
||||
phoneLabel: string;
|
||||
paidStamp: string;
|
||||
paymentMethodLabel: string;
|
||||
paymentStatusLabel: string;
|
||||
paymentStatusPaid: string;
|
||||
paymentStatusUnpaid: string;
|
||||
paymentStatusPartial: string;
|
||||
paymentStatusRefunded: string;
|
||||
paymentStatusVoided: string;
|
||||
orderNumberLabel: string;
|
||||
shippingAddressHeading: string;
|
||||
shippingMethodLabel: string;
|
||||
@@ -97,7 +97,12 @@ export interface InvoiceStrings {
|
||||
|
||||
/** Status displayed for the order's payment, derived from Shopify's
|
||||
* `displayFinancialStatus`. */
|
||||
export type PaymentStatus = "paid" | "unpaid" | "partial" | "refunded";
|
||||
export type PaymentStatus =
|
||||
| "paid"
|
||||
| "unpaid"
|
||||
| "partial"
|
||||
| "refunded"
|
||||
| "voided";
|
||||
|
||||
export function paymentStatusLabel(
|
||||
status: PaymentStatus,
|
||||
@@ -110,13 +115,27 @@ export function paymentStatusLabel(
|
||||
return strings.paymentStatusPartial;
|
||||
case "refunded":
|
||||
return strings.paymentStatusRefunded;
|
||||
case "voided":
|
||||
return strings.paymentStatusVoided;
|
||||
default:
|
||||
return strings.paymentStatusUnpaid;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps Shopify's `displayFinancialStatus` to our condensed enum. Values not
|
||||
* signalling actual receipt of money map to "unpaid". */
|
||||
/** Maps Shopify's `displayFinancialStatus` to our condensed enum.
|
||||
*
|
||||
* - PAID → paid
|
||||
* - PARTIALLY_PAID → partial
|
||||
* - REFUNDED / PARTIALLY_REFUNDED → refunded
|
||||
* (composeInvoice further reclassifies PARTIALLY_REFUNDED with a
|
||||
* refund < gross back to "paid".)
|
||||
* - VOIDED → voided
|
||||
* (authorisation cancelled before capture; no money was ever
|
||||
* received and none is owed — distinct from "unpaid".)
|
||||
* - PENDING / AUTHORIZED / EXPIRED / unknown → unpaid
|
||||
*
|
||||
* Unknown values log a warning so we notice when Shopify adds a new
|
||||
* enum member. */
|
||||
export function derivePaymentStatus(
|
||||
displayFinancialStatus: string | null | undefined,
|
||||
): PaymentStatus {
|
||||
@@ -124,6 +143,14 @@ export function derivePaymentStatus(
|
||||
if (v === "PAID") return "paid";
|
||||
if (v === "PARTIALLY_PAID") return "partial";
|
||||
if (v === "REFUNDED" || v === "PARTIALLY_REFUNDED") return "refunded";
|
||||
if (v === "VOIDED") return "voided";
|
||||
if (v && v !== "PENDING" && v !== "AUTHORIZED" && v !== "EXPIRED") {
|
||||
console.warn(
|
||||
`[invoice] derivePaymentStatus: unknown displayFinancialStatus ${JSON.stringify(
|
||||
displayFinancialStatus,
|
||||
)} — falling back to "unpaid".`,
|
||||
);
|
||||
}
|
||||
return "unpaid";
|
||||
}
|
||||
|
||||
@@ -184,13 +211,13 @@ const de: InvoiceStrings = {
|
||||
emailLabel: "E-Mail",
|
||||
webLabel: "Web",
|
||||
phoneLabel: "Tel.",
|
||||
paidStamp: "BEZAHLT",
|
||||
paymentMethodLabel: "Zahlart",
|
||||
paymentStatusLabel: "Zahlstatus",
|
||||
paymentStatusPaid: "Bezahlt",
|
||||
paymentStatusUnpaid: "Offen",
|
||||
paymentStatusPartial: "Teilweise bezahlt",
|
||||
paymentStatusRefunded: "Erstattet",
|
||||
paymentStatusVoided: "Annulliert",
|
||||
orderNumberLabel: "Bestellnummer",
|
||||
shippingAddressHeading: "Lieferadresse",
|
||||
shippingMethodLabel: "Versandart",
|
||||
@@ -274,13 +301,13 @@ const en: InvoiceStrings = {
|
||||
emailLabel: "E-mail",
|
||||
webLabel: "Web",
|
||||
phoneLabel: "Tel.",
|
||||
paidStamp: "PAID",
|
||||
paymentMethodLabel: "Payment method",
|
||||
paymentStatusLabel: "Payment status",
|
||||
paymentStatusPaid: "Paid",
|
||||
paymentStatusUnpaid: "Outstanding",
|
||||
paymentStatusPartial: "Partially paid",
|
||||
paymentStatusRefunded: "Refunded",
|
||||
paymentStatusVoided: "Voided",
|
||||
orderNumberLabel: "Order no.",
|
||||
shippingAddressHeading: "Shipping address",
|
||||
shippingMethodLabel: "Shipping method",
|
||||
|
||||
Reference in New Issue
Block a user