fix(invoice): unify customer-facing remittance reference with the printed invoice number

Two related fixes around the order/invoice number:

1) The thank-you page and the customer-account order page were showing
   the bare Shopify order name (e.g. '#1034') as the payment reference,
   while the PDF (and its GiroCode QR) used the canonical invoice
   number (e.g. 'RE-1034'). Banks treat each unique reference as a
   separate payment, and several reject the '#' character outright \u2014
   so customers who pasted the thank-you reference into their banking
   app ended up with a payment the shop couldn't reconcile.

   New shared helper resolveOrderRemittance() (services/invoice/
   remittance.server.ts) returns the single source of truth for the
   reference: latest non-cancelled Invoice row for the order, falling
   back to '${prefix}${orderNumber}' when no PDF has been generated yet.
   Both /api/public/payment-info and /api/public/girocode.png now route
   through it, so the thank-you page, the customer-account page and the
   GiroCode QR are guaranteed to match the PDF byte-for-byte.

2) Drop the redundant '\u00b7 Bestellnummer: #1004' suffix from the PDF
   title when the invoice number's trailing digits already match the
   Shopify order name (default 'order_number' numbering mode). In that
   mode the two strings carry identical numeric content and the suffix
   only adds noise; sequential mode (RE-7 vs #1004) keeps the suffix.

- New smoke assertion verifies the suppression triggers on
  invoiceNumber='RE-1004' + orderName='#1004' and that the invoice
  number itself is still shown.
- Both endpoints now also query 'Order.number' (already covered by
  read_orders) so the fallback path can build the prefix+order-number
  string without requiring the Invoice row.
This commit is contained in:
Gerhard Scheikl
2026-05-15 15:51:10 +02:00
parent a2b3c14022
commit 2a4a7fd983
5 changed files with 98 additions and 3 deletions
+7 -1
View File
@@ -379,7 +379,13 @@ export function InvoiceDocument({ invoice }: DocProps) {
? t.offer
: t.invoice}{" "}
Nr. {invoice.number}
{invoice.kind === "invoice" && invoice.orderName
{invoice.kind === "invoice"
&& invoice.orderName
// Suppress the redundant "· Bestellnummer: #1004" suffix when
// the invoice number is just the Shopify order number with the
// configured prefix (default numbering mode) — they'd carry
// identical trailing digits and only confuse the customer.
&& invoice.number.replace(/\D+/g, "") !== invoice.orderName.replace(/\D+/g, "")
? ` · ${t.orderNumberLabel}: ${invoice.orderName}`
: ""}
</Text>
+47
View File
@@ -0,0 +1,47 @@
import db from "../../db.server";
import type { ShopSettings } from "@prisma/client";
/**
* Returns the canonical remittance reference for an order — i.e. the
* exact string that should appear:
* - on the printed invoice PDF (`invoice.number`),
* - in the GiroCode QR payload,
* - and in the customer-facing payment instructions on the
* thank-you / customer-account pages.
*
* Banking systems treat each unique reference string as a separate
* payment, so all three surfaces MUST use this single source of truth.
*
* Resolution order:
* 1. The latest non-cancelled `Invoice` row for the order — guaranteed
* to match what's printed on the PDF.
* 2. Predicted default-mode number (`${prefix}${orderNumber}`). Safe
* for the default `order_number` numbering mode and a sensible
* best-guess for `prefix_sequential` before the invoice has been
* generated (the customer just sees the order number with the
* shop's invoice prefix instead of the bare Shopify "#1004").
*/
export async function resolveOrderRemittance(args: {
shopDomain: string;
orderGid: string;
orderNumber: number | null | undefined;
settings: Pick<ShopSettings, "invoicePrefix">;
}): Promise<string> {
const invoice = await db.invoice.findFirst({
where: {
shopDomain: args.shopDomain,
orderId: args.orderGid,
kind: "invoice",
cancelledAt: null,
},
orderBy: [{ version: "desc" }, { createdAt: "desc" }],
select: { invoiceNumber: true },
});
if (invoice?.invoiceNumber) return invoice.invoiceNumber;
const prefix = args.settings.invoicePrefix || "";
if (args.orderNumber != null) return `${prefix}${args.orderNumber}`;
// Last-ditch: derive numeric tail from the GID.
const tail = args.orderGid.split("/").pop() ?? "";
return `${prefix}${tail}`;
}