fix(invoice): suppress GiroCode + payment terms for refunded (and paid) orders
User reported that fully refunded orders still rendered a SEPA GiroCode
QR asking the customer to wire the original total. The existing gate
("!viewModel.paid") only excluded literally-PAID orders; REFUNDED,
PARTIALLY_REFUNDED, VOIDED, AUTHORIZED, etc. all sneaked through and
produced a confusing payment request for an order whose outstanding
balance is in fact 0.
Root cause: the view model exposed two related but ambiguous flags
("paid" and "paymentStatus") and the renderer mixed them
inconsistently. Both the GiroCode generation step in
generateInvoice.server.tsx AND the GiroCode/payment-terms render gates
in InvoiceDocument.tsx checked the wrong one.
Fix: introduce a single derived "requiresPayment" flag on the view
model (composeInvoice.ts) that is true only when:
- the document is a regular invoice (not a storno or an offer), AND
- paymentStatus is neither "paid" nor "refunded".
That single flag now drives:
- GiroCode QR generation (skip QR fetch for paid/refunded)
- GiroCode block render in the PDF
- payment-terms paragraph render in the PDF
The existing "Zahlstatus: Erstattet" / "Payment status: Refunded"
meta-row continues to communicate the refund visually — the change
just removes the contradictory call-to-pay.
Side benefits:
- Storno (cancellation invoice) PDFs no longer emit the German
"Bitte überweise …" payment-terms paragraph (kind=storno was
falling through the same not-paid branch).
- Same suppression for fully PAID orders (covered by a second smoke
fixture) so the QR doesn't suggest re-payment after the fact.
Verification: new smoke fixtures (REFUNDED + PAID) build a real
GiroCode data URL and verify the PDF text neither contains the
"GiroCode" caption nor the "Bitte überweise" German payment terms,
while still showing the corresponding "Erstattet" / "Bezahlt" status
row. tsc / smoke / tests / build all green.
This commit is contained in:
@@ -507,6 +507,61 @@ async function main() {
|
||||
assert("storno PDF > 5 KB", stornoBuf.length > 5_000, `actual ${stornoBuf.length}`);
|
||||
console.log(` → wrote ${stornoOut} (${stornoBuf.length} bytes)`);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Refunded order: GiroCode + payment-terms must be suppressed
|
||||
// ----------------------------------------------------------------
|
||||
console.log("• Refunded order (REFUNDED) suppresses GiroCode + payment terms");
|
||||
{
|
||||
const refundedOrder = { ...buildAtB2BOrder(), displayFinancialStatus: "REFUNDED" };
|
||||
const refundedVm = composeInvoice({
|
||||
order: refundedOrder, settings: settings as never, invoiceNumber: "RE-1014",
|
||||
});
|
||||
assertEq("paymentStatus=refunded", refundedVm.paymentStatus, "refunded");
|
||||
assert("requiresPayment=false for refunded", refundedVm.requiresPayment === false);
|
||||
refundedVm.issuer.logoDataUrl = vm.issuer.logoDataUrl;
|
||||
// The orchestrator gates GiroCode generation on requiresPayment too —
|
||||
// simulate that here by NOT attaching giroCodePngDataUrl. The PDF
|
||||
// render-gate must independently refuse to render even if a stale
|
||||
// QR data URL were attached, so set one anyway and verify both
|
||||
// suppression layers.
|
||||
refundedVm.giroCodePngDataUrl = await buildGiroCodeDataUrl({
|
||||
beneficiaryName: settings.companyName,
|
||||
iban: settings.iban,
|
||||
bic: settings.bic,
|
||||
amount: refundedVm.totals.gross,
|
||||
remittance: refundedVm.number,
|
||||
});
|
||||
const refundedText = await pdfToText(await renderInvoicePdf(refundedVm));
|
||||
assert("Refunded PDF does NOT show GiroCode caption",
|
||||
!refundedText.includes("GiroCode"));
|
||||
assert("Refunded PDF does NOT show DE payment terms",
|
||||
!refundedText.includes("Bitte überweise"));
|
||||
assert("Refunded PDF still shows the 'Erstattet' status row",
|
||||
refundedText.includes("Erstattet"));
|
||||
}
|
||||
|
||||
// Same gating must apply to PAID orders.
|
||||
console.log("• Paid order (PAID) suppresses GiroCode + payment terms");
|
||||
{
|
||||
const paidOrder = { ...buildAtB2BOrder(), displayFinancialStatus: "PAID" };
|
||||
const paidVm = composeInvoice({
|
||||
order: paidOrder, settings: settings as never, invoiceNumber: "RE-1015",
|
||||
});
|
||||
assert("requiresPayment=false for paid", paidVm.requiresPayment === false);
|
||||
paidVm.issuer.logoDataUrl = vm.issuer.logoDataUrl;
|
||||
paidVm.giroCodePngDataUrl = await buildGiroCodeDataUrl({
|
||||
beneficiaryName: settings.companyName,
|
||||
iban: settings.iban,
|
||||
bic: settings.bic,
|
||||
amount: paidVm.totals.gross,
|
||||
remittance: paidVm.number,
|
||||
});
|
||||
const paidText = await pdfToText(await renderInvoicePdf(paidVm));
|
||||
assert("Paid PDF does NOT show GiroCode caption", !paidText.includes("GiroCode"));
|
||||
assert("Paid PDF does NOT show DE payment terms", !paidText.includes("Bitte überweise"));
|
||||
assert("Paid PDF shows the 'Bezahlt' status row", paidText.includes("Bezahlt"));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Footer note translation
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user