Set Reply-To to sellerEmail, make From display name dynamic, fix stale footer docs

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 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 14:38:01 +00:00
parent e61a62e579
commit d7e7928dfc
3 changed files with 29 additions and 8 deletions
+17 -6
View File
@@ -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
+5 -1
View File
@@ -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" <admin@mk360.de>',
// 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"}" <admin@mk360.de>`,
replyTo: seller?.sellerEmail || undefined,
to,
subject: "Bitte bestätige deine E-Mail-Adresse",
html: renderVerificationEmailHtml(firstName, url, seller),
+7 -1
View File
@@ -92,7 +92,13 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
}
await transport.sendMail({
from: '"einfach produktiv" <admin@mk360.de>',
// 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"}" <admin@mk360.de>`,
replyTo: seller?.sellerEmail || undefined,
to: customerEmail,
subject: template.subject,
html,