From d7e7928dfc067025545b44be80e7598406389b93 Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 22 Jul 2026 14:38:01 +0000 Subject: [PATCH] Set Reply-To to sellerEmail, make From display name dynamic, fix stale footer docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer replies to order-confirmation and resend-verification mail now route to the seller's real address via Reply-To, and the From display name reflects sellerName — but the From address itself stays admin@mk360.de since sellerEmail's domain isn't confirmed SPF-authorized on the Hostinger account yet (see the "SMTP From address pending SPF" memory note for the follow-up). Also cleans up README references left over from the previous footer rewrite (stale "company line" wording, a dangling cross-reference to a renamed section). Co-Authored-By: Claude Sonnet 5 --- README.md | 23 +++++++++++++++++------ app/lib/alertAdmin.ts | 6 +++++- app/lib/orderEmail.ts | 8 +++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0873058..7308620 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ explains what does have access instead. | `customers` | Storefront accounts — register/login/order-history, a second `auth: true` collection separate from the Payload admin's own `users` login | `customerNumber`, `firstName`/`lastName`/`email`, one default address, `cart` (server-side mirror), `emailVerified` (non-blocking). **Not public-read** — see "Orders & customer accounts" below | | `number-ranges` | Admin-configurable prefix + running counter for customer/order/invoice numbers — one row per tenant | `customerPrefix`/`customerNext`/`customerPadding`, `orderPrefix`/`orderNext`/`orderPadding`, `invoicePrefix`/`invoiceNext`/`invoicePadding`. **Admin-only**, no frontend read at all — internal to the two `beforeChange` hooks that assign these numbers | | `email-templates` | Editable subject/heading/body/footer for all 6 transactional emails this shop sends (see "Email templates & Live Preview" and "Status-change emails" below) | `type` (`order-confirmation`/`password-reset`/`order-shipped`/`order-cancelled`/`order-return-requested`/`order-returned`), `subject`, `heading`, `bodyText`, `footerText`. Public-read, has a Live Preview button | -| `company-settings` | Structured business data for invoice PDFs *and* every email footer's company line (see "Invoice PDFs" below) — one row per tenant, own **Company** admin group (not Commerce — this is business identity, not a storefront concern) | `sellerName`/`sellerStreet`/`sellerZip`/`sellerCity`/`sellerCountry`/`sellerEmail`, `vatId`, `taxRatePercent` (admin-editable, not hardcoded), `bankDetails`. **Not public-read** — admin or `ORDER_SERVICE_SECRET`. Has a Live Preview button — see "Company Settings & Live Preview" below | +| `company-settings` | Structured business data for invoice PDFs *and* every email's legal footer (Anbieterkennzeichnung, see "Invoice PDFs" below) — one row per tenant, own **Company** admin group (not Commerce — this is business identity, not a storefront concern) | `sellerName`/`sellerStreet`/`sellerZip`/`sellerCity`/`sellerCountry`/`sellerEmail`, `vatId`, `taxRatePercent` (admin-editable, not hardcoded), `bankDetails`. **Not public-read** — admin or `ORDER_SERVICE_SECRET`. Has a Live Preview button — see "Company Settings & Live Preview" below | All of the above except `company-settings`, `media`, `users`, `tenants` are grouped in the Payload admin sidebar under **Commerce** (`products`, @@ -137,7 +137,7 @@ delivery-time disclosure (`shipping-settings`), order/customer/invoice numbering schemes (`number-ranges`), all 6 email wordings (`email-templates`, with Live Preview), and invoice seller data, bank details, and VAT rate (`company-settings` — also what every email's -footer company line is sourced from). What still requires a code change: +legal footer is sourced from). What still requires a code change: adding a new *field* to any collection (needs a migration), payment processing itself (not built), and anything structural in `orders`/`customers` beyond `status`/`returnReason` and the profile fields @@ -348,7 +348,7 @@ inbox, not only in `/konto/bestellungen`. entrypoints every caller below goes through. `seller` (`company-settings` data) is passed in rather than fetched inside these functions, so a caller that also needs it for something else in the same request (e.g. - `orderEmail.ts`'s email footer, see "Configurable company data" below) + `orderEmail.ts`'s legal email footer, see "Legal footer (Anbieterkennzeichnung) on every email" below) fetches it once via `getSellerForInvoice()`, not twice. - **Original invoice — called from two places, same render function:** `app/lib/orderEmail.ts` (checkout attachment — a PDF-generation failure @@ -654,15 +654,26 @@ mechanism as Posts/LegalPages/Testimonials (`useLivePreview()` from render functions, which call `buildLegalFooterLines()` themselves — one place composes the footer, not each call site. The Payload-side sends (password-reset, the 4 status-change emails, the *initial* verification - email) need the equivalent treatment via that repo's own - `src/lib/sellerInfo.ts`, not present in this checkout. Live Preview passes - `seller: null`, which falls back to `DEFAULT_LEGAL_FOOTER_LINES` (a + email) get the equivalent treatment via that repo's own + `src/lib/sellerInfo.ts`'s `buildLegalFooterLines()`/`getSellerFooterLines()` + — see that repo's README for its own copy of this section. Live Preview + passes `seller: null`, which falls back to `DEFAULT_LEGAL_FOOTER_LINES` (a placeholder Anbieterkennzeichnung) since there's no real order/tenant context there to fetch against. Note `company-settings` currently has no Handelsregister court/number or Geschäftsführer field — fine for a sole proprietorship, but would need adding if the business becomes a registered legal form (GmbH etc.), see that collection's own field list above. +- **`From` display name is dynamic (`seller.sellerName`), the address + itself stays `admin@mk360.de`.** `Reply-To` is set to `seller.sellerEmail` + so a customer's reply actually reaches the seller regardless of the From + address. The address isn't also switched to `sellerEmail` because that + domain isn't confirmed SPF-authorized on the Hostinger account backing + `admin@mk360.de` yet — doing so without that confirmation risks + order-confirmation/verification mail landing in spam or bouncing outright. + Both `orderEmail.ts` and `alertAdmin.ts`'s `sendVerificationEmail` set + this the same way; `sendCriticalAlert` (internal, admin@mk360.de to + itself) doesn't need it. ### GDPR self-service diff --git a/app/lib/alertAdmin.ts b/app/lib/alertAdmin.ts index 0a2c51a..fea5caf 100644 --- a/app/lib/alertAdmin.ts +++ b/app/lib/alertAdmin.ts @@ -40,7 +40,11 @@ export async function sendVerificationEmail(to: string, firstName: string, token const url = `https://einfach-produktiv.mk360.de/api/account/verify-email?token=${token}`; const seller = await getSellerForInvoice(); await transport.sendMail({ - from: '"einfach produktiv" ', + // See orderEmail.ts's own comment on why only the display name is + // dynamic — the address stays admin@mk360.de until sellerEmail's domain + // is confirmed SPF-authorized on the Hostinger account. + from: `"${seller?.sellerName ?? "einfach produktiv"}" `, + replyTo: seller?.sellerEmail || undefined, to, subject: "Bitte bestätige deine E-Mail-Adresse", html: renderVerificationEmailHtml(firstName, url, seller), diff --git a/app/lib/orderEmail.ts b/app/lib/orderEmail.ts index 30510ab..3979bc9 100644 --- a/app/lib/orderEmail.ts +++ b/app/lib/orderEmail.ts @@ -92,7 +92,13 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa } await transport.sendMail({ - from: '"einfach produktiv" ', + // Display name only, not the address — see the memory note on why the + // envelope stays admin@mk360.de (that's the domain the Hostinger SMTP + // account is actually authorized for; sellerEmail's domain isn't + // confirmed SPF-authorized on it yet). Reply-To is what actually routes + // a customer's reply to the seller, regardless of the From address. + from: `"${seller?.sellerName ?? "einfach produktiv"}" `, + replyTo: seller?.sellerEmail || undefined, to: customerEmail, subject: template.subject, html,