Compare commits

..

3 Commits

Author SHA1 Message Date
Marco 07c70c86f5 Add legalForm-driven Pflichtangaben (register court/number, managing director)
Mirrors the Payload backend's new company-settings.legalForm field: when
present, registerCourt/registerNumber/managingDirector now appear in the
email footer (buildLegalFooterLines) and both invoice PDF footers,
matching §37a HGB / §35a GmbHG requirements for registered legal forms.
A sole proprietorship (the default) renders identically to before —
these fields are only appended when actually set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 15:09:43 +00:00
Marco d7e7928dfc 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>
2026-07-22 14:38:01 +00:00
Marco e61a62e579 Give every email a full legal footer (Anbieterkennzeichnung), not just a company line
Order confirmation, resend-verification, and the internal critical-alert
mail now render name, street, ZIP/city, email, and VAT ID from
company-settings instead of a bare "<sellerName> · <sellerEmail>" line,
so every email this app sends meets business-correspondence footer
requirements rather than just the customer-facing ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:15:43 +00:00
10 changed files with 180 additions and 48 deletions
+36 -13
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
@@ -641,16 +641,39 @@ mechanism as Posts/LegalPages/Testimonials (`useLivePreview()` from
option here). Not shared code with the React page — this is plain
inline-styled HTML built for email-client compatibility (nested
`<table>`s, no flexbox) — just matched by eye.
- **Footer company line is configurable, not hardcoded.** `emailShell()`
takes a required `companyLine` parameter ("<sellerName> · <sellerEmail>")
`orderEmail.ts` fetches `company-settings` once (`getSellerForInvoice()`)
and derives it from there, same admin-editable business data the invoice
PDFs already use, rather than a literal `"einfach produktiv ·
admin@mk360.de"` string. The Payload-side sends (password-reset, the 4
status-change emails, verification) do the same via that repo's
`src/lib/sellerInfo.ts`. Live Preview uses `DEFAULT_COMPANY_LINE` (a
fallback constant) since there's no real order/tenant context there to
fetch against.
- **Footer carries a full legal Anbieterkennzeichnung, not just a brand
line.** `emailShell()` takes a `footerLines: string[]` array built by
`buildLegalFooterLines(seller)``sellerName`, `sellerStreet`,
`sellerZip`/`sellerCity` (+ `sellerCountry` if not Germany), `E-Mail:
sellerEmail`, and `USt-IdNr.: vatId` when set — the same admin-editable
`company-settings` fields the invoice PDFs already use, rather than a
literal `"einfach produktiv · admin@mk360.de"` string. `orderEmail.ts`
and `alertAdmin.ts` (both the resend-verification mail and the plain-text
critical-alert mail) all fetch `company-settings` once
(`getSellerForInvoice()`) and pass the `seller` object straight into the
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) 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
+4
View File
@@ -11,6 +11,10 @@ export const metadata: Metadata = {
const FALLBACK: CompanySettings = {
sellerName: "",
legalForm: "sole-proprietorship",
registerCourt: null,
registerNumber: null,
managingDirector: null,
sellerStreet: "",
sellerZip: "",
sellerCity: "",
@@ -6,7 +6,6 @@ import {
renderPasswordResetHtml,
renderOrderStatusHtml,
ORDER_STATUS_EMAIL_ICON,
DEFAULT_COMPANY_LINE,
SAMPLE_ORDER,
type EmailTemplateContent,
} from "../../../lib/emailTemplates";
@@ -35,17 +34,21 @@ export function LiveEmailPreviewClient({
depth: 0,
});
// No real company-settings fetch in this preview context — passing null
// falls back to DEFAULT_LEGAL_FOOTER_LINES (placeholder Anbieterkennzeichnung)
// inside buildLegalFooterLines(), same shape as the real send just with
// placeholder business data.
const html =
type === "order-confirmation"
? renderOrderConfirmationHtml(data, SAMPLE_ORDER, DEFAULT_COMPANY_LINE)
? renderOrderConfirmationHtml(data, SAMPLE_ORDER, null)
: type === "password-reset"
? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token", DEFAULT_COMPANY_LINE)
? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token", null)
: renderOrderStatusHtml(
data,
ORDER_STATUS_EMAIL_ICON[type] ?? "✓",
SAMPLE_ORDER.orderNumber,
`https://einfach-produktiv.mk360.de/konto/bestellungen/${SAMPLE_ORDER.orderNumber}`,
DEFAULT_COMPANY_LINE,
null,
);
return (
+26 -10
View File
@@ -1,16 +1,25 @@
import { transport } from "./mailer";
import { buildLegalFooterLines, renderVerificationEmailHtml } from "./emailTemplates";
import { getSellerForInvoice } from "./invoiceData";
// Fire-and-forget by design — callers should not await this in a way that
// blocks or fails the actual error response the customer sees. Wrap
// everything in its own try/catch so a broken mail relay never becomes a
// second, worse failure on top of the one being reported.
// second, worse failure on top of the one being reported. Internal-only
// (sent to admin@mk360.de, this business's own inbox), but still carries
// the plain-text Anbieterkennzeichnung for consistency with every other
// email this app sends — see buildLegalFooterLines() in emailTemplates.ts.
export function sendCriticalAlert(subject: string, details: Record<string, unknown>): void {
transport
.sendMail({
from: '"einfach produktiv Alerts" <admin@mk360.de>',
to: "admin@mk360.de",
subject: `[einfach produktiv] ${subject}`,
text: JSON.stringify(details, null, 2),
getSellerForInvoice()
.catch(() => null)
.then((seller) => {
const footer = buildLegalFooterLines(seller).join("\n");
return transport.sendMail({
from: '"einfach produktiv Alerts" <admin@mk360.de>',
to: "admin@mk360.de",
subject: `[einfach produktiv] ${subject}`,
text: `${JSON.stringify(details, null, 2)}\n\n---\n${footer}`,
});
})
.catch((err) => {
console.error("sendCriticalAlert: failed to send alert email", err);
@@ -24,13 +33,20 @@ export function sendCriticalAlert(subject: string, details: Record<string, unkno
// route.ts), which updates the token via the customer's own session
// (app/lib/customerAuth.ts's resendVerificationEmail) but has no Payload
// hook to piggyback on for a plain update, so it sends directly instead —
// same Hostinger transport as the alert above, just a different template.
// same Hostinger transport as the alert above, and the same branded
// emailShell()/legal-footer template as every other email (see
// renderVerificationEmailHtml in emailTemplates.ts).
export async function sendVerificationEmail(to: string, firstName: string, token: string): Promise<void> {
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: `<p>Hallo ${firstName},</p><p>bitte bestätige deine E-Mail-Adresse für dein Konto bei einfach produktiv:</p><p><a href="${url}">${url}</a></p><p>Der Link ist 24 Stunden gültig.</p>`,
html: renderVerificationEmailHtml(firstName, url, seller),
});
}
+9
View File
@@ -121,6 +121,13 @@ export type InvoiceSeller = {
vatId: string;
taxRatePercent: number;
bankDetails?: string | null;
// Pflichtangaben in Geschäftsbriefen for registered legal forms (§37a
// HGB / §35a GmbHG) — optional because a sole proprietorship (the
// default legalForm in company-settings) has neither. Mirrors
// invoicePdf.tsx's own InvoiceSeller — see that file's comment.
registerCourt?: string | null;
registerNumber?: string | null;
managingDirector?: string | null;
};
// Stornorechnung: every item at its full ordered quantity (nothing
@@ -266,6 +273,8 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn
<Text>
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
{seller.vatId}
{seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""}
{seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""}
</Text>
{seller.bankDetails ? <Text style={{ marginTop: 4 }}>Bankverbindung (für Überweisung): {seller.bankDetails}</Text> : null}
</View>
+68 -14
View File
@@ -1,4 +1,5 @@
import { formatPrice, formatDate } from "./format";
import type { CompanySettings } from "./payload";
// Pure string-building functions, no server-only or client-only imports —
// used both server-side for the actual email send (app/lib/orderEmail.ts,
@@ -64,18 +65,48 @@ function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Live-Preview-only fallback (no real order/invoice-settings fetch there,
// Live-Preview-only fallback (no real order/company-settings fetch there,
// see /email-preview/[type]) — the actual send always passes the real
// "<sellerName> · <sellerEmail>" from invoice-settings (see orderEmail.ts).
export const DEFAULT_COMPANY_LINE = "einfach produktiv · admin@mk360.de";
// seller (company-settings) through buildLegalFooterLines() below.
export const DEFAULT_LEGAL_FOOTER_LINES: string[] = [
"einfach produktiv",
"Musterstraße 12",
"12345 Musterstadt",
"E-Mail: admin@mk360.de",
];
// Every business email needs an Anbieterkennzeichnung (§5 TMG-equivalent
// minimum for business correspondence: full name, postal address, contact,
// plus VAT ID once assigned) — not just a friendly "brand · email" line.
// Built from the same company-settings fields the invoice PDFs already
// render (app/lib/invoicePdf.tsx), so there is exactly one source of truth
// for this business's legal identity.
//
// registerCourt/registerNumber/managingDirector are conditionally required
// on the Payload side (CompanySettings.ts, gated by legalForm) — only
// appended here when actually present, so a sole proprietorship's footer
// stays exactly as short as before this field set existed. Keep this in
// sync with the Payload backend's own copy in src/lib/sellerInfo.ts.
export function buildLegalFooterLines(seller: CompanySettings | null): string[] {
if (!seller) return DEFAULT_LEGAL_FOOTER_LINES;
const lines = [
seller.sellerName,
seller.sellerStreet,
`${seller.sellerZip} ${seller.sellerCity}${seller.sellerCountry && seller.sellerCountry !== "Deutschland" ? `, ${seller.sellerCountry}` : ""}`,
`E-Mail: ${seller.sellerEmail}`,
];
if (seller.vatId) lines.push(`USt-IdNr.: ${seller.vatId}`);
if (seller.registerCourt && seller.registerNumber) lines.push(`${seller.registerCourt} · ${seller.registerNumber}`);
if (seller.managingDirector) lines.push(`Geschäftsführung: ${seller.managingDirector}`);
return lines;
}
// `icon`: a single glyph rendered inside the brand-tinted circle up top —
// "✓" for order-confirmation, "✉" for password-reset. Same circular
// treatment as the confirmation page's own success icon and its delivery-
// status panel icon. `companyLine`: sourced from Payload's invoice-settings
// (sellerName/sellerEmail), not hardcoded — same admin-editable business
// data the invoice PDFs already use, see orderEmail.ts.
function emailShell(icon: string, headingHtml: string, bodyHtml: string, footerText: string | null, companyLine: string): string {
// status panel icon. `footerLines`: the legal Anbieterkennzeichnung from
// buildLegalFooterLines() above — required on every email, not optional.
function emailShell(icon: string, headingHtml: string, bodyHtml: string, footerText: string | null, footerLines: string[]): string {
return `<body style="margin:0;padding:32px 16px;background:${BG_BASE};font-family:${FONT_SANS};">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;margin:0 auto;">
<tr>
@@ -116,7 +147,7 @@ function emailShell(icon: string, headingHtml: string, bodyHtml: string, footerT
<tr>
<td style="padding-top:24px;text-align:center;">
${footerText ? `<p style="margin:0 0 8px;font-size:13px;color:${TEXT_MUTED};">${escapeHtml(footerText)}</p>` : ""}
<p style="margin:0;font-size:12px;color:${TEXT_MUTED};">${escapeHtml(companyLine)}</p>
${footerLines.map((line) => `<p style="margin:0;font-size:11px;line-height:1.6;color:${TEXT_MUTED};">${escapeHtml(line)}</p>`).join("")}
</td>
</tr>
</table>
@@ -163,7 +194,7 @@ export const SAMPLE_ORDER: OrderConfirmationData = {
total: 37.7,
};
export function renderOrderConfirmationHtml(template: EmailTemplateContent, order: OrderConfirmationData, companyLine: string): string {
export function renderOrderConfirmationHtml(template: EmailTemplateContent, order: OrderConfirmationData, seller: CompanySettings | null): string {
const rows = order.items
.map(
(item) => `<tr>
@@ -202,7 +233,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde
</table>
`;
return emailShell("✓", escapeHtml(template.heading), body, template.footerText, companyLine);
return emailShell("✓", escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));
}
// Icon shown per status — matches the Payload-side send exactly (see
@@ -222,7 +253,7 @@ export function renderOrderStatusHtml(
icon: string,
orderNumber: string,
orderUrl: string,
companyLine: string,
seller: CompanySettings | null,
): string {
const body = `
${paragraphs(template.bodyText, "center")}
@@ -236,10 +267,10 @@ export function renderOrderStatusHtml(
</table>
`;
return emailShell(icon, escapeHtml(template.heading), body, template.footerText, companyLine);
return emailShell(icon, escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));
}
export function renderPasswordResetHtml(template: EmailTemplateContent, resetUrl: string, companyLine: string): string {
export function renderPasswordResetHtml(template: EmailTemplateContent, resetUrl: string, seller: CompanySettings | null): string {
const body = `
${paragraphs(template.bodyText, "center")}
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:20px auto 8px;">
@@ -253,5 +284,28 @@ export function renderPasswordResetHtml(template: EmailTemplateContent, resetUrl
<p style="text-align:center;font-size:12px;color:${TEXT_MUTED};">Der Link ist 1 Stunde gültig.</p>
`;
return emailShell("✉", escapeHtml(template.heading), body, template.footerText, companyLine);
return emailShell("✉", escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));
}
// Not a Payload-editable template like the others above (see alertAdmin.ts's
// own comment on why this one send path exists outside the email-templates
// collection) — wording is fixed in code, but it still goes through the
// same emailShell()/buildLegalFooterLines() so the resend-verification email
// carries the same branding and legally required footer as every other
// email this app sends.
export function renderVerificationEmailHtml(firstName: string, verifyUrl: string, seller: CompanySettings | null): string {
const body = `
${paragraphs(`Hallo ${firstName},\n\nbitte bestätige deine E-Mail-Adresse für dein Konto bei einfach produktiv.`, "center")}
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:20px auto 8px;">
<tr>
<td style="background:${BRAND};border-radius:6px;">
<a href="${verifyUrl}" style="display:inline-block;padding:13px 28px;font-weight:700;font-size:15px;color:${TEXT_PRIMARY};text-decoration:none;">E-Mail-Adresse bestätigen</a>
</td>
</tr>
</table>
<p style="text-align:center;font-size:12px;color:${TEXT_MUTED};word-break:break-all;">${verifyUrl}</p>
<p style="text-align:center;font-size:12px;color:${TEXT_MUTED};">Der Link ist 24 Stunden gültig.</p>
`;
return emailShell("✉", "Bitte bestätige deine E-Mail-Adresse", body, null, buildLegalFooterLines(seller));
}
+4 -3
View File
@@ -5,9 +5,10 @@ import { renderCorrectionInvoicePdf, type CorrectionInvoiceKind, type Correction
export type { CompanySettings };
// Fetched once by callers that need seller data for more than one purpose
// in the same request (e.g. orderEmail.ts also needs it for the email
// footer's companyLine) — avoids a second identical company-settings round
// trip, unlike calling getCompanySettings() again inside each generator.
// in the same request (e.g. orderEmail.ts also needs it for the email's
// legal footer, see buildLegalFooterLines() in emailTemplates.ts) — avoids
// a second identical company-settings round trip, unlike calling
// getCompanySettings() again inside each generator.
export async function getSellerForInvoice(): Promise<CompanySettings | null> {
return getCompanySettings();
}
+10
View File
@@ -119,6 +119,14 @@ export type InvoiceSeller = {
vatId: string;
taxRatePercent: number;
bankDetails?: string | null;
// Pflichtangaben in Geschäftsbriefen for registered legal forms (§37a
// HGB / §35a GmbHG) — optional because a sole proprietorship (the
// default legalForm in company-settings) has neither. See
// buildLegalFooterLines() in emailTemplates.ts for the same fields'
// equivalent treatment in the email footer.
registerCourt?: string | null;
registerNumber?: string | null;
managingDirector?: string | null;
};
// Used only by /company-settings-preview's Live Preview — a fixed sample
@@ -305,6 +313,8 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller
<Text>
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
{seller.vatId}
{seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""}
{seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""}
</Text>
{seller.bankDetails ? <Text style={{ marginTop: 4 }}>Bankverbindung (für Überweisung): {seller.bankDetails}</Text> : null}
</View>
+9 -4
View File
@@ -1,6 +1,6 @@
import { transport } from "./mailer";
import { getEmailTemplate } from "./payload";
import { renderOrderConfirmationHtml, DEFAULT_COMPANY_LINE, type OrderConfirmationData } from "./emailTemplates";
import { renderOrderConfirmationHtml, type OrderConfirmationData } from "./emailTemplates";
import { generateInvoicePdf, getSellerForInvoice } from "./invoiceData";
import { sendCriticalAlert } from "./alertAdmin";
@@ -47,8 +47,7 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
};
const seller = await getSellerForInvoice();
const companyLine = seller ? `${seller.sellerName} · ${seller.sellerEmail}` : DEFAULT_COMPANY_LINE;
const html = renderOrderConfirmationHtml(template, order, companyLine);
const html = renderOrderConfirmationHtml(template, order, seller);
let attachments: { filename: string; content: Buffer }[] | undefined;
try {
@@ -93,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,
+7
View File
@@ -645,6 +645,13 @@ export async function getEmailTemplate(
export type CompanySettings = {
sellerName: string;
// Drives whether registerCourt/registerNumber/managingDirector are
// populated — mirrors Payload's CompanySettings.ts collection exactly
// (same option values), see buildLegalFooterLines() in emailTemplates.ts.
legalForm: "sole-proprietorship" | "e-k" | "gbr" | "ohg" | "kg" | "gmbh" | "ug" | "ag";
registerCourt: string | null;
registerNumber: string | null;
managingDirector: string | null;
sellerStreet: string;
sellerZip: string;
sellerCity: string;