2a4a7fd983
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.
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import type { LoaderFunctionArgs } from "react-router";
|
|
import { unauthenticated } from "../shopify.server";
|
|
import db from "../db.server";
|
|
import { buildGiroCodePngBuffer } from "../services/invoice/girocode";
|
|
import { verifyGiroCodeUrl } from "../services/invoice/signedUrl";
|
|
import { resolveOrderRemittance } from "../services/invoice/remittance.server";
|
|
|
|
/**
|
|
* Public PNG endpoint that returns the GiroCode QR image bytes for an order.
|
|
* Auth: short-lived HMAC-signed URL (issued by /api/public/payment-info).
|
|
*
|
|
* Required query params: shop, orderId, exp, sig.
|
|
*/
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const url = new URL(request.url);
|
|
const verified = verifyGiroCodeUrl(url.searchParams);
|
|
if (!verified.ok) {
|
|
return new Response(`unauthorized: ${verified.reason ?? "invalid"}`, { status: 401 });
|
|
}
|
|
const { shop, orderId } = verified;
|
|
if (!shop || !orderId) {
|
|
return new Response("bad request", { status: 400 });
|
|
}
|
|
|
|
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
|
|
if (!settings?.iban) {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
|
|
// Recompute payload server-side from order + settings (don't trust client).
|
|
const numericId = orderId.replace(/[^0-9]/g, "");
|
|
const orderGid = `gid://shopify/Order/${numericId}`;
|
|
|
|
const { admin } = await unauthenticated.admin(shop);
|
|
const res = await admin.graphql(
|
|
`#graphql
|
|
query GiroCodeOrderInfo($id: ID!) {
|
|
order(id: $id) {
|
|
name
|
|
number
|
|
currencyCode
|
|
totalPriceSet { shopMoney { amount } }
|
|
totalOutstandingSet { shopMoney { amount } }
|
|
}
|
|
}`,
|
|
{ variables: { id: orderGid } },
|
|
);
|
|
const json = (await res.json()) as {
|
|
data?: {
|
|
order?: {
|
|
name?: string;
|
|
number?: number | null;
|
|
currencyCode?: string;
|
|
totalPriceSet?: { shopMoney: { amount: string } };
|
|
totalOutstandingSet?: { shopMoney: { amount: string } };
|
|
} | null;
|
|
};
|
|
};
|
|
const o = json.data?.order;
|
|
if (!o) {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
|
|
const total = parseFloat(o.totalPriceSet?.shopMoney.amount ?? "0");
|
|
const outstanding = parseFloat(o.totalOutstandingSet?.shopMoney.amount ?? "0");
|
|
const amount = outstanding > 0 ? outstanding : total;
|
|
// Use the canonical invoice number printed on the PDF — keeping the QR
|
|
// and the customer-facing thank-you/account page in lockstep so the
|
|
// bank treats both as one and the same payment.
|
|
const remittance = await resolveOrderRemittance({
|
|
shopDomain: shop,
|
|
orderGid,
|
|
orderNumber: typeof o.number === "number" ? o.number : null,
|
|
settings,
|
|
});
|
|
|
|
const png = await buildGiroCodePngBuffer({
|
|
beneficiaryName: [settings.companyName, settings.legalForm].filter(Boolean).join(" "),
|
|
iban: settings.iban,
|
|
bic: settings.bic,
|
|
amount,
|
|
currency: o.currencyCode ?? "EUR",
|
|
remittance,
|
|
});
|
|
|
|
const body = new Uint8Array(png);
|
|
return new Response(body, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "image/png",
|
|
"Cache-Control": "private, max-age=300",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
});
|
|
};
|