L11 Kundenversand: Mail-Anhänge per Dokument-Referenz und Template craftvia_report_customer
- MailJob/EnqueueInput: attachments als { documentId } (keine Bytes in Redis, nur mit tenantId)
- deliverMail: Anhänge mandantengebunden aus MailLog.tenantId laden, SHA-256 prüfen,
Größenlimit MAIL_MAX_ATTACHMENT_BYTES (Default 10 MB); Fehler -> failed ohne Versand
- SMTP-Provider reicht Anhänge an nodemailer durch; optionaler Provider für Tests
- Template craftvia_report_customer (de/en) ohne App-Link, eigene CUSTOMER_TEMPLATE_KEYS
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,10 @@ export type TemplateVars = {
|
||||
craftvia_notification: {
|
||||
name: string; subject: string; body: string; actionUrl?: string; footer?: CraftviaFooter;
|
||||
};
|
||||
// ---- Craftvia customer mail (lane L11). No app link: the customer has no account.
|
||||
craftvia_report_customer: {
|
||||
customerName: string; tenantName: string; reportTitle: string; reportDate: string; message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Why the recipient gets a Craftvia notification — controls the footer line. */
|
||||
@@ -90,6 +94,9 @@ export const CRAFTVIA_TEMPLATE_KEYS = [
|
||||
"craftvia_notification",
|
||||
] as const satisfies readonly TemplateKey[];
|
||||
|
||||
/** Customer-facing Craftvia mails (lane L11) — separate list, recipients are external customers. */
|
||||
export const CUSTOMER_TEMPLATE_KEYS = ["craftvia_report_customer"] as const satisfies readonly TemplateKey[];
|
||||
|
||||
/** Abmelde-/Präferenzhinweis — nur für Benachrichtigungen, nie für Transaktionsmails. */
|
||||
const FOOTER_NOTE: Record<Locale, string> = {
|
||||
de: "Sie erhalten diese Benachrichtigung aufgrund Ihrer Rolle in Ihrem Betrieb. Die Einstellungen dazu finden Sie in Ihrem Profil.",
|
||||
@@ -261,6 +268,45 @@ const craftviaEn: { [K in CraftviaKey]: Builder<K> } = {
|
||||
}),
|
||||
};
|
||||
|
||||
// ---- Customer mails (lane L11) ----
|
||||
type CustomerKey = (typeof CUSTOMER_TEMPLATE_KEYS)[number];
|
||||
|
||||
/** Subject lines must never carry CR/LF (header injection); free text is HTML-escaped by email-brand. */
|
||||
const oneLine = (s: string) => s.replace(/[\r\n]+/g, " ").trim();
|
||||
const messageParagraphs = (m?: string) =>
|
||||
(m ?? "")
|
||||
.split(/\r?\n\s*\r?\n|\r?\n/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const customerDe: { [K in CustomerKey]: Builder<K> } = {
|
||||
craftvia_report_customer: (v) => ({
|
||||
subject: oneLine(`${v.reportTitle} – ${v.tenantName}`),
|
||||
heading: oneLine(v.reportTitle),
|
||||
paragraphs: [
|
||||
"Guten Tag,",
|
||||
`anbei erhalten Sie von ${v.tenantName} den ${v.reportTitle} vom ${v.reportDate} für ${v.customerName}.`,
|
||||
...messageParagraphs(v.message),
|
||||
"Der Arbeitsnachweis ist als PDF angehängt.",
|
||||
],
|
||||
footerNote: `Diese Nachricht wurde von ${v.tenantName} über ${BRAND.name} versendet.`,
|
||||
}),
|
||||
};
|
||||
|
||||
const customerEn: { [K in CustomerKey]: Builder<K> } = {
|
||||
craftvia_report_customer: (v) => ({
|
||||
subject: oneLine(`${v.reportTitle} – ${v.tenantName}`),
|
||||
heading: oneLine(v.reportTitle),
|
||||
paragraphs: [
|
||||
"Hello,",
|
||||
`please find attached the ${v.reportTitle} dated ${v.reportDate} from ${v.tenantName} for ${v.customerName}.`,
|
||||
...messageParagraphs(v.message),
|
||||
"The work record is attached as a PDF.",
|
||||
],
|
||||
footerNote: `This message was sent by ${v.tenantName} via ${BRAND.name}.`,
|
||||
}),
|
||||
};
|
||||
|
||||
const de: { [K in TemplateKey]: Builder<K> } = {
|
||||
invitation: (v) => ({
|
||||
subject: `Ihr Zugang zu ${BRAND.name}`,
|
||||
@@ -338,6 +384,7 @@ const de: { [K in TemplateKey]: Builder<K> } = {
|
||||
],
|
||||
}),
|
||||
...craftviaDe,
|
||||
...customerDe,
|
||||
};
|
||||
|
||||
const en: { [K in TemplateKey]: Builder<K> } = {
|
||||
@@ -417,6 +464,7 @@ const en: { [K in TemplateKey]: Builder<K> } = {
|
||||
],
|
||||
}),
|
||||
...craftviaEn,
|
||||
...customerEn,
|
||||
};
|
||||
|
||||
const CATALOG: Record<Locale, { [K in TemplateKey]: Builder<K> }> = { de, en };
|
||||
|
||||
Reference in New Issue
Block a user