Redesign invoice PDFs, add correction-invoice downloads, return reasons, and per-product tax/bundle support

Invoice + Stornorechnung/Gutschrift PDFs get a modern header-band layout,
a "bereits beglichen" badge for immediately-paid orders, labelled bank
details, and a per-tax-rate summary breakdown. Correction invoices can
now be re-downloaded from the account (regenerated deterministically,
not stored as files, same approach as the original invoice). Return
requests capture a reason. Products can define bundles (bundleItems) and
a per-product VAT rate override, both snapshotted onto order items.
This commit is contained in:
Marco
2026-07-22 10:31:22 +00:00
parent 04cc69f98b
commit 179b59d73d
15 changed files with 723 additions and 222 deletions
+14 -4
View File
@@ -426,6 +426,8 @@ export type CustomerOrderDetail = CustomerOrder & {
id: number;
invoiceNumber: string | null;
invoiceIssuedAt: string | null;
correctionInvoiceNumber: string | null;
correctionInvoiceIssuedAt: string | null;
customerFirstName: string;
customerLastName: string;
customerEmail: string;
@@ -442,7 +444,8 @@ export type CustomerOrderDetail = CustomerOrder & {
paymentMethodTitle: string;
discountCode: string | null;
discountAmount: number;
items: { productName: string; quantity: number; unitPrice: number }[];
returnReason: string | null;
items: { productName: string; quantity: number; unitPrice: number; taxRatePercent: number; bundleContents: string | null }[];
};
// Access control (Orders.ts) already scopes a customer's own JWT to only
@@ -460,8 +463,7 @@ export async function getCustomerOrderDetail(token: string, customerId: number,
cache: "no-store",
});
if (!res.ok) return null;
const data: { docs?: (Omit<CustomerOrderDetail, "itemCount"> & { items: { productName: string; quantity: number; unitPrice: number }[] })[] } =
await res.json();
const data: { docs?: Omit<CustomerOrderDetail, "itemCount">[] } = await res.json();
const doc = data.docs?.[0];
if (!doc) return null;
return { ...doc, itemCount: doc.items.length };
@@ -475,12 +477,20 @@ export async function requestOrderStatusChange(
token: string,
orderId: number,
action: "cancel" | "request-return",
returnReason?: string,
): Promise<{ ok: true } | { ok: false; reason: string }> {
const status = action === "cancel" ? "cancelled" : "return_requested";
// Orders.ts's beforeChange hook only allows a customer-authenticated
// update to touch `status` (plus `returnReason`, but only together with
// this exact transition — see that hook's own comment) — omitting the
// key entirely for `cancel` rather than sending `returnReason: undefined`
// keeps that request shaped exactly like before this field existed.
const body: { status: string; returnReason?: string } = { status };
if (action === "request-return" && returnReason) body.returnReason = returnReason;
const res = await fetch(`${PAYLOAD_URL}/api/orders/${orderId}`, {
method: "PATCH",
headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ status }),
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => null);