diff --git a/README.md b/README.md index a059a7f..a075a33 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ specific to this project. image — see `AGENTS.md` before touching anything version-specific, this Next.js release differs from older training-data conventions) - **React 19.2.4**, **Tailwind CSS 4**, **Motion** for animation +- **`@react-pdf/renderer`** for invoice PDF generation (see "Invoice + PDFs" below) — no headless-browser dependency - No local database — all editable content (and now orders/customer accounts) lives in the shared Payload CMS at `payload.mk360.de` (see below). Customer auth is Payload's own (a second, separate `auth: true` @@ -40,9 +42,11 @@ Preview components should ever point somewhere else). `DISCOUNT_SERVICE_SECRET` (no safe default — required for discount codes to validate/redeem at all; must match the value set on the Payload backend). `ORDER_SERVICE_SECRET` (no safe default — required for `/api/checkout` to persist an order in -Payload at all, and for `/api/account/verify-email` to look up a customer -by their verification token; must match the value set on the Payload -backend — also used there for the same header). `SMTP_USER`/`SMTP_PASSWORD` +Payload at all, for `/api/account/verify-email` to look up a customer +by their verification token, and for `getInvoiceSettings()` to read the +`invoice-settings` collection (seller data for invoice PDFs); must match +the value set on the Payload backend — also used there for the same +header). `SMTP_USER`/`SMTP_PASSWORD` (no safe default — required for `app/lib/alertAdmin.ts`'s critical-failure alerts and resend-verification emails; **does not** need to match anything on the Payload side — this app's SMTP connection is deliberately @@ -235,11 +239,62 @@ check against Payload's public API, unlike most content on this site. persisted" alert, since the order itself is safe either way). Content comes from the **published** `order-confirmation` row in Payload's `email-templates` collection — see "Email templates & Live Preview" - below for how that's edited/previewed. + below for how that's edited/previewed. As of the invoice PDF feature + (see below), this same send also carries the order's invoice PDF as an + attachment. - Still not built: real payment processing — the checkout button is labelled "zahlungspflichtig" but nothing actually captures a payment yet. See `project_backend_checkout_plan` in the assistant's own memory. +## Invoice PDFs + +Generated **synchronously at checkout** and attached to the order +confirmation email — not just an on-demand download — per an explicit +product decision that a customer should always have the invoice in their +inbox, not only in `/konto/bestellungen`. + +- **`app/lib/invoicePdf.tsx`** — a `@react-pdf/renderer` `Document` + (`InvoiceDocument`), not HTML-to-PDF or a headless browser (Puppeteer/ + Chromium would be a heavier footprint on a VPS already running several + other containers). Built-in Helvetica rather than a registered web font + — this renders inside a fire-and-forget checkout step, and a font-fetch + failure there would be one more way to silently lose the attachment for + no real design benefit; brand color/spacing still carries the visual + identity via `StyleSheet`. +- **`app/lib/invoiceData.ts`** — `generateInvoicePdf()`, the single place + both callers below go through: fetches `invoice-settings` (`getInvoiceSettings()` + in `app/lib/payload.ts`, service-secret authenticated — see below) for + seller data, then renders. Net/tax/gross are derived from each order's + already-stored gross line prices and the seller's `taxRatePercent` — not + a second, independently-tracked figure. +- **Called from two places, same render function:** `app/lib/orderEmail.ts` + (checkout attachment — a PDF-generation failure here does **not** sink + the confirmation email itself, it just sends without the attachment and + alerts admin) and `app/api/account/orders/[orderNumber]/invoice/route.ts` + (GET, customer's own order only, "Rechnung herunterladen" on + `/konto/bestellungen/[orderNumber]`) — a re-download always matches what + was originally emailed, since `invoiceNumber`/`invoiceIssuedAt` are + assigned exactly once, server-side, at order creation (Payload's + `orders.ts` `beforeChange` hook — see the Payload README) and never + regenerated. +- **`invoice-settings`** (Payload collection, structured seller data — + name/address/`vatId`/`taxRatePercent`/`bankDetails`) is fetched via + `getInvoiceSettings()`, authenticated the same way as order creation + (`x-order-service-secret` header, `ORDER_SERVICE_SECRET`) since it's not + public-read (holds bank details) but does need to be reachable from this + app's own server-side code, not just from inside Payload's admin. + **Currently seeded with placeholder data** ("Björn Wendt", "Musterstraße + 12", USt-IdNr. "DE123456789") mirroring the Impressum's own placeholder + content — real business details need to be entered in the Payload admin + before an invoice generated from this is legally valid. `taxRatePercent` + is deliberately a configurable admin field, not a hardcoded `19` in the + renderer, per an explicit decision to keep the VAT rate editable without + a code change. +- §14 UStG line items: seller/buyer address, invoice number + date, order + reference, per-item quantity/price, net subtotal, tax rate + amount, + gross total — all on the PDF, not just the summary the confirmation + email's HTML already shows. + ## Orders & customer accounts An account is required to buy — there is no guest checkout. Registration @@ -271,9 +326,10 @@ this check was skipped or raced. doesn't just trust the caller), `cart` (GET/POST, see below), `verify-email`, `resend-verification`, `delete`, `export` (see "Email verification" and "GDPR self-service" below), `forgot-password`, - `reset-password` (see "Password reset" below), and + `reset-password` (see "Password reset" below), `orders/[orderNumber]` (PATCH — cancel/return-request, see "Order - cancellation & returns" below). + cancellation & returns" below), and `orders/[orderNumber]/invoice` + (GET — invoice PDF download, see "Invoice PDFs" below). - **`/konto/bestellungen`** lists a customer's own orders (status shown as a colored `OrderStatusBadge.tsx`, plus an "Abmelden" link — `LogoutButton.tsx`); **`/konto/bestellungen/[orderNumber]`** shows one @@ -391,24 +447,27 @@ links to `/konto/passwort-vergessen`. ### Email templates & Live Preview -Both transactional emails (order confirmation, password reset) read their -subject/heading/body/footer wording from Payload's `email-templates` -collection — editable in the admin without a deploy, with a Live Preview -button using the exact same mechanism as Posts/LegalPages/Testimonials -(`useLivePreview()` from `@payloadcms/live-preview-react`, already a -dependency here for `LivePostContent.tsx`). +All 6 transactional emails (order confirmation, password reset, and the 4 +status-change types below) read their subject/heading/body/footer wording +from Payload's `email-templates` collection — editable in the admin +without a deploy, with a Live Preview button using the exact same +mechanism as Posts/LegalPages/Testimonials (`useLivePreview()` from +`@payloadcms/live-preview-react`, already a dependency here for +`LivePostContent.tsx`). - **`app/lib/emailTemplates.ts`** — pure string-building functions - (`renderOrderConfirmationHtml()`, `renderPasswordResetHtml()`), no - server-only or client-only imports. Used **both** server-side for the - real send (`orderEmail.ts`) **and** client-side for the Live Preview - page — same function, same inputs, so a Live Preview edit and the real - sent email are guaranteed to render identically for order-confirmation - (password-reset's actual send uses Payload's own simple inline template - instead — see that repo's README for why — so its Live Preview - approximates rather than pixel-matches). Inline-styled HTML (`` - layout, `style` attributes, no Tailwind/`