Käufermails laufen über die Warteschlange des Terminplaners (G09/G12 mit Vorrang), G09 wird bei Überlänge um den AGB-Text gekürzt (Link statt Text). Der Mailteil des Minutenjobs und die Aufräumschritte des Tagesjobs laufen auch bei TERMINE_ENABLED=off, solange SHOP_ENABLED nicht off ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
391 lines
19 KiB
JavaScript
391 lines
19 KiB
JavaScript
import { config } from '../../config.js'
|
||
import { renderMail, loadTemplate } from '../mailTemplates.js'
|
||
import { sendCustomerMail } from '../mailer.js'
|
||
import { customerOf } from '../siteFeatures.js'
|
||
import { einreihen } from '../termine/outbox.js'
|
||
import { pruefeNow, zeitMs } from '../termine/util.js'
|
||
import { datumKurz, uhrzeit } from '../termine/zeit.js'
|
||
import { euro } from './geld.js'
|
||
import { oeffentlicheRechtstexte } from './kontext.js'
|
||
import { gewaehrleistungsHinweis } from './rechtstexte.js'
|
||
|
||
/**
|
||
* Mails des Webshops (Spezifikation 2026-10-05-webshop-design.md, Abschnitte 5.3, 5.4 und 8; Plan Task E4).
|
||
*
|
||
* Kaeufer (G09 Bestellbestaetigung, G10 versendet, G11 abholbereit, G12 Eingang Widerruf, G13 Erstattung, G14 Zahlung
|
||
* fehlgeschlagen) laufen durch die Mail-Warteschlange des Terminplaners (termine/outbox.js einreihen): Absendername = Shopname,
|
||
* Absenderadresse bleibt SMTP_FROM, Antworten an die E-Mail des Haendlers, nie BCC. featureId = ID des portalFeatures-Dokuments des
|
||
* Shops, bookingId = Bestell-ID. Der Betreff steht in der Vorlage (G-Vorlagen des Shops haben keinen {{betreff}}-Wert). Die
|
||
* Empfaenger-Kappe der Terminplaner-Mails (limits.darfEmpfaenger) gilt hier nicht: G09 ist Vertragspflicht.
|
||
* Haendler (K25 neue Bestellung, K26 Widerruf) gehen direkt per sendCustomerMail an die Kunden-E-Mail, ohne BCC ans Team
|
||
* (die Mails enthalten Kaeuferdaten).
|
||
*
|
||
* Beide Wege werfen nie wegen eines Mailfehlers: Rueckgabe { ok: true, ... } | { ok: false, fehler }; der Fehlertext steht im Log (ohne
|
||
* Adresse), die Bestellung bleibt unberuehrt.
|
||
*
|
||
* Erwartete Form der Bestellung (`order` = shopOrders-Dokument; der JSON-Schnappschuss steht in `bestellung`, ersatzweise `daten`,
|
||
* als Text oder Objekt; alle Felder sind optional, Fehlendes erscheint als "–"):
|
||
* order.bestellnummer, order.kaeuferEmail, order.bezahltAt | order.erstelltAt
|
||
* snap.positionen[] { name, varianteText, menge, summeCent, summeText?, widerruf ('gesetzlich'|'verderblich'|'individuell'|'hygiene') }
|
||
* snap.summen | snap { zwischensummeCent, versandCent, gesamtCent, texte?: { zwischensumme, versand, gesamt, steuerHinweis } }
|
||
* snap.lieferart ('versand'|'abholung'), snap.land ('DE'|'AT')
|
||
* snap.kaeufer { name, email, telefon } (oder snap.name)
|
||
* snap.lieferadresse { name?, line1|strasse, line2?, postal_code|plz, city|ort, country|land }
|
||
* snap.zahlungsart 'card' | 'klarna' | 'sepa_debit' | Text
|
||
* snap.einstellungenAuszug { anbieter, lieferzeit, abholung } (sonst die aktuellen Einstellungen des Shops)
|
||
*/
|
||
|
||
const LEER = '–'
|
||
const GAST_ARTEN = Object.freeze(['G09', 'G10', 'G11', 'G12', 'G13', 'G14'])
|
||
/** Grenzen des Outbox-Schemas (html 100000, text 50000) mit etwas Luft. */
|
||
const MAX_HTML = 95000
|
||
const MAX_TEXT = 47000
|
||
const LANDNAMEN = { DE: 'Deutschland', AT: 'Österreich' }
|
||
const ZAHLUNGSARTEN = { card: 'Kreditkarte, Apple Pay, Google Pay', klarna: 'Klarna', sepa_debit: 'SEPA-Lastschrift' }
|
||
// Wie WIDERRUF_TEXTE in produkte.js (dort nicht exportiert)
|
||
const WIDERRUF_AUSSCHLUSS = {
|
||
verderblich: 'Vom Widerruf ausgeschlossen: schnell verderbliche Ware',
|
||
individuell: 'Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt',
|
||
hygiene: 'Vom Widerruf ausgeschlossen: versiegelte Ware aus Hygienegründen, wenn die Versiegelung entfernt wurde',
|
||
}
|
||
const ABHOLUNG_HINWEIS = 'Bitte bringen Sie Ihre Bestellnummer mit.'
|
||
|
||
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
|
||
const hatText = (wert) => typeof wert === 'string' && wert.trim() !== ''
|
||
const text = (wert) => (typeof wert === 'string' ? wert.trim() : '')
|
||
const oderLeer = (wert) => (hatText(wert) ? wert.trim() : LEER)
|
||
const einzeilig = (wert) => text(wert).replace(/\s+/g, ' ')
|
||
|
||
// ---------------------------------------------------------------- Bausteine
|
||
|
||
/** Schnappschuss der Bestellung als Objekt ({} wenn es keinen oder keinen lesbaren gibt). */
|
||
function schnappschuss(order) {
|
||
for (const feld of ['bestellung', 'daten']) {
|
||
const roh = order?.[feld]
|
||
if (istObjekt(roh)) return roh
|
||
if (typeof roh === 'string' && roh !== '') {
|
||
try {
|
||
const geparst = JSON.parse(roh)
|
||
if (istObjekt(geparst)) return geparst
|
||
} catch {
|
||
// unlesbar: naechstes Feld
|
||
}
|
||
}
|
||
}
|
||
return {}
|
||
}
|
||
|
||
const einstellungenVon = (ctx) => ctx?.shop?.einstellungen ?? {}
|
||
const anbieterVon = (ctx, snap) => ({ ...(einstellungenVon(ctx).anbieter ?? {}), ...(istObjekt(snap?.einstellungenAuszug?.anbieter) ? snap.einstellungenAuszug.anbieter : {}) })
|
||
|
||
/** Name des Shops in Betreff, Kopf und Absender: Shopname, sonst Name des Anbieters. */
|
||
function shopName(ctx, snap = {}) {
|
||
return einzeilig(snap?.einstellungenAuszug?.shopname) || einzeilig(einstellungenVon(ctx).shopname) || einzeilig(anbieterVon(ctx, snap).name) || 'Webshop'
|
||
}
|
||
|
||
function anbieterZeile(a) {
|
||
const ort = [text(a.plz), text(a.ort)].filter(Boolean).join(' ')
|
||
const land = text(a.land) && a.land !== 'DE' ? (LANDNAMEN[a.land] ?? a.land) : ''
|
||
const anschrift = [text(a.strasse), ort, land].filter(Boolean).join(', ')
|
||
return [text(a.name), anschrift, text(a.email)].filter(Boolean).join(', ') || LEER
|
||
}
|
||
|
||
const siteKeyVon = (ctx) => ctx?.shop?.siteKey || ctx?.doc?.siteKey || ''
|
||
const shopSeite = (ctx) => `${config.publicBaseUrl}/shop/${siteKeyVon(ctx)}`
|
||
|
||
/** Akzentfarbe als #rrggbb (nur geprueft, damit nichts in das style-Attribut gelangt). */
|
||
const farbeVon = (e) => (/^#[0-9a-fA-F]{6}$/.test(e?.akzent ?? '') ? e.akzent : '#111111')
|
||
|
||
/** Menge × Name (Variante) – Summe, eine Zeile je Position. */
|
||
function positionenText(positionen) {
|
||
const zeilen = (Array.isArray(positionen) ? positionen : [])
|
||
.filter(istObjekt)
|
||
.map((p) => {
|
||
const variante = text(p.varianteText) ? ` (${einzeilig(p.varianteText)})` : ''
|
||
const summe = hatText(p.summeText) ? p.summeText.trim() : Number.isInteger(p.summeCent) ? euro(p.summeCent) : LEER
|
||
return `${Number.isInteger(p.menge) ? p.menge : LEER} × ${einzeilig(p.name) || LEER}${variante} – ${summe}`
|
||
})
|
||
return zeilen.length ? zeilen.join('\n') : LEER
|
||
}
|
||
|
||
function adresseText(a, fallbackName) {
|
||
if (!istObjekt(a)) return LEER
|
||
const feld = (...namen) => text(namen.map((n) => a[n]).find(hatText))
|
||
const land = feld('country', 'land')
|
||
const zeilen = [
|
||
feld('name') || text(fallbackName),
|
||
feld('line1', 'strasse'),
|
||
feld('line2'),
|
||
[feld('postal_code', 'plz'), feld('city', 'ort')].filter(Boolean).join(' '),
|
||
land && land !== 'DE' ? (LANDNAMEN[land] ?? land) : '',
|
||
].filter(Boolean)
|
||
return zeilen.length ? zeilen.join('\n') : LEER
|
||
}
|
||
|
||
function zahlungsartText(snap, e) {
|
||
const roh = text(snap.zahlungsart)
|
||
if (roh) return ZAHLUNGSARTEN[roh] ?? roh
|
||
return Array.isArray(e.zahlungsarten) && e.zahlungsarten.length === 1 ? (ZAHLUNGSARTEN[e.zahlungsarten[0]] ?? LEER) : 'Online-Zahlung'
|
||
}
|
||
|
||
function ausschlussText(positionen) {
|
||
return (Array.isArray(positionen) ? positionen : [])
|
||
.filter((p) => istObjekt(p) && Object.hasOwn(WIDERRUF_AUSSCHLUSS, p.widerruf))
|
||
.map((p) => `${einzeilig(p.name) || LEER}: ${WIDERRUF_AUSSCHLUSS[p.widerruf]}`)
|
||
.join('\n')
|
||
}
|
||
|
||
/** "TT.MM.JJJJ, HH:MM Uhr" (Berliner Zeit); ungueltige Zeitpunkte werfen. */
|
||
function eingangText(eingang) {
|
||
const ms = zeitMs(eingang)
|
||
if (!Number.isFinite(ms)) throw new TypeError('Eingang ist kein Zeitpunkt')
|
||
const d = new Date(ms)
|
||
return `${datumKurz(d)}, ${uhrzeit(d)} Uhr`
|
||
}
|
||
|
||
const datumText = (order, now) => {
|
||
const ms = zeitMs(order?.bezahltAt || order?.erstelltAt)
|
||
return datumKurz(Number.isFinite(ms) ? new Date(ms) : now)
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Werte
|
||
|
||
/**
|
||
* Gemeinsame Werte der Kaeufermails aus dem Schnappschuss der Bestellung und dem Shop-Kontext: Kopf und Fuss (farbe, betrieb_name,
|
||
* anbieter_zeile, widerruf_url, impressum_url, datenschutz_url) und alle Werte von G09 (bestellnummer, datum, positionen, Summen,
|
||
* Lieferart, Adresse, Zahlungsart, Rechtstexte). `order` darf null sein (G12 ohne passende Bestellung): dann sind die
|
||
* Bestellwerte "–". Es sind mehr Werte als eine einzelne Vorlage braucht; anKaeufer reicht nur die deklarierten weiter.
|
||
* @param {{ doc?: object, shop: object }} ctx
|
||
* @param {object|null} order
|
||
* @param {Date} [now] Ersatzdatum, wenn die Bestellung keinen Zeitpunkt hat
|
||
* @returns {Record<string, string>}
|
||
*/
|
||
export function kaeuferWerte(ctx, order, now = new Date()) {
|
||
if (!istObjekt(ctx?.shop)) throw new TypeError('kaeuferWerte: ctx.shop fehlt')
|
||
const snap = schnappschuss(order)
|
||
const e = einstellungenVon(ctx)
|
||
const auszug = istObjekt(snap.einstellungenAuszug) ? snap.einstellungenAuszug : {}
|
||
const anbieter = anbieterVon(ctx, snap)
|
||
const abholung = { ...(e.abholung ?? {}), ...(istObjekt(auszug.abholung) ? auszug.abholung : {}) }
|
||
const rechtstexte = oeffentlicheRechtstexte(ctx) ?? {}
|
||
const summen = istObjekt(snap.summen) ? snap.summen : snap
|
||
const texte = istObjekt(summen.texte) ? summen.texte : {}
|
||
const cent = (wert) => (Number.isInteger(wert) ? euro(wert) : LEER)
|
||
const lieferart = snap.lieferart === 'abholung' ? 'abholung' : snap.lieferart === 'versand' ? 'versand' : ''
|
||
const kaeufer = istObjekt(snap.kaeufer) ? snap.kaeufer : {}
|
||
const land = text(snap.land)
|
||
const shop = shopSeite(ctx)
|
||
|
||
return {
|
||
farbe: farbeVon(e),
|
||
betrieb_name: shopName(ctx, snap),
|
||
anbieter_zeile: anbieterZeile(anbieter),
|
||
widerruf_url: `${shop}/widerruf`,
|
||
impressum_url: text(rechtstexte.impressumUrl) || shop,
|
||
datenschutz_url: text(rechtstexte.datenschutzUrl) || shop,
|
||
bestellnummer: oderLeer(order?.bestellnummer),
|
||
datum: datumText(order, now),
|
||
positionen: positionenText(snap.positionen),
|
||
zwischensumme: text(texte.zwischensumme) || cent(summen.zwischensummeCent),
|
||
versand: text(texte.versand) || (lieferart === 'abholung' ? 'Abholung – kostenlos' : cent(summen.versandCent)),
|
||
gesamt: text(texte.gesamt) || cent(summen.gesamtCent ?? order?.summeCent),
|
||
steuer_hinweis: text(texte.steuerHinweis) || (e.kleinunternehmer === true ? 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.' : LEER),
|
||
lieferart_text: lieferart === 'abholung' ? 'Abholung' : lieferart === 'versand' ? `Versand${land ? ` nach ${LANDNAMEN[land] ?? land}` : ''}` : LEER,
|
||
lieferadresse: adresseText(snap.lieferadresse ?? kaeufer.lieferadresse ?? kaeufer.adresse, kaeufer.name ?? snap.name),
|
||
abholort: oderLeer(abholung.adresse),
|
||
lieferzeit: oderLeer(auszug.lieferzeit ?? e.lieferzeit),
|
||
zahlungsart: zahlungsartText(snap, e),
|
||
ausschluss_text: ausschlussText(snap.positionen),
|
||
agb: oderLeer(rechtstexte.agb),
|
||
widerrufsbelehrung: oderLeer(rechtstexte.widerrufsbelehrung),
|
||
widerrufsformular: oderLeer(rechtstexte.widerrufsformular),
|
||
gewaehrleistung: gewaehrleistungsHinweis(),
|
||
// fuer G11 (Standard, wenn der Haendler keinen Hinweis hinterlegt hat)
|
||
hinweis: text(abholung.hinweis) || ABHOLUNG_HINWEIS,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Werte und Bloecke einer Kaeufermail (alle Werte von kaeuferWerte plus die Werte der Aktion aus `extra`, gefiltert auf die
|
||
* Platzhalter der Vorlage).
|
||
* extra je Art:
|
||
* G10 versanddienst, sendungsnummer (beide optional; Block sendung, wenn eines da ist)
|
||
* G11 hinweis (ersetzt den Hinweis des Haendlers)
|
||
* G12 name, bestellnummer (Eingaben des Kaeufers; ohne Angabe die der Bestellung), eingang (Date | ISO, Pflicht), nachricht
|
||
* G13 betragCent (Pflicht, ganze Cent), grund
|
||
*/
|
||
function kaeuferMail(art, ctx, order, extra, now) {
|
||
const alle = kaeuferWerte(ctx, order, now)
|
||
const blocks = {}
|
||
switch (art) {
|
||
case 'G09':
|
||
blocks.versand = schnappschuss(order).lieferart === 'versand'
|
||
blocks.abholung = schnappschuss(order).lieferart === 'abholung'
|
||
blocks.ausschluss = alle.ausschluss_text !== ''
|
||
break
|
||
case 'G10':
|
||
alle.versanddienst = oderLeer(extra.versanddienst)
|
||
alle.sendungsnummer = oderLeer(extra.sendungsnummer)
|
||
blocks.sendung = hatText(extra.versanddienst) || hatText(extra.sendungsnummer)
|
||
break
|
||
case 'G11':
|
||
if (hatText(extra.hinweis)) alle.hinweis = extra.hinweis.trim()
|
||
break
|
||
case 'G12':
|
||
alle.name = text(extra.name)
|
||
if (hatText(extra.bestellnummer)) alle.bestellnummer = extra.bestellnummer.trim()
|
||
alle.eingang = eingangText(extra.eingang)
|
||
alle.nachricht = text(extra.nachricht)
|
||
blocks.nachricht = alle.nachricht !== ''
|
||
break
|
||
case 'G13':
|
||
if (!Number.isInteger(extra.betragCent) || extra.betragCent <= 0) throw new TypeError('extra.betragCent muss eine positive ganze Zahl sein')
|
||
alle.betrag = euro(extra.betragCent)
|
||
alle.grund = text(extra.grund)
|
||
blocks.grund = alle.grund !== ''
|
||
break
|
||
default:
|
||
break
|
||
}
|
||
const vorlage = loadTemplate(art)
|
||
const values = Object.fromEntries(Object.entries(alle).filter(([schluessel]) => vorlage.placeholders.includes(schluessel)))
|
||
return { values, blocks: Object.fromEntries(Object.entries(blocks).filter(([name]) => vorlage.blocks.includes(name))) }
|
||
}
|
||
|
||
const zuLang = (mail) => mail.html.length > MAX_HTML || mail.text.length > MAX_TEXT
|
||
|
||
// ---------------------------------------------------------------- Einreihen
|
||
|
||
/**
|
||
* Reiht eine Kaeufermail (G09-G14) in die Warteschlange ein. Empfaenger ist extra.an, sonst order.kaeuferEmail. bookingId ist
|
||
* order.$id, sonst extra.bookingId (z. B. die ID des Widerrufs bei G12), sonst "-". Ist G09 zu lang fuer die Warteschlange
|
||
* (AGB, Widerrufsbelehrung und Formular koennen zusammen sehr lang sein), ersetzt der Server den AGB-Text durch einen Satz mit
|
||
* Link auf die AGB im Shop; Widerrufsbelehrung, Formular und Gewaehrleistungshinweis bleiben immer vollstaendig.
|
||
* @param {'G09'|'G10'|'G11'|'G12'|'G13'|'G14'} art
|
||
* @param {{ doc: object, shop: object }} ctx Shop-Kontext (kontext.js)
|
||
* @param {object|null} order shopOrders-Dokument (bei G12 ohne passende Bestellung null)
|
||
* @param {object} [extra] siehe kaeuferMail
|
||
* @param {Date} [now]
|
||
* @returns {Promise<{ ok: true, eintrag: object, gekuerzt: boolean } | { ok: false, fehler: string }>}
|
||
*/
|
||
export async function anKaeufer(art, ctx, order, extra = {}, now = new Date()) {
|
||
try {
|
||
if (!GAST_ARTEN.includes(art)) throw new TypeError(`${String(art).slice(0, 20)} ist keine Käufermail`)
|
||
pruefeNow(now, 'anKaeufer')
|
||
extra = extra ?? {}
|
||
const { values, blocks } = kaeuferMail(art, ctx, order, extra, now)
|
||
const an = hatText(extra.an) ? extra.an.trim() : text(order?.kaeuferEmail)
|
||
if (an === '') throw new TypeError('Empfänger fehlt')
|
||
|
||
let gekuerzt = false
|
||
let mail = renderMail(art, values, blocks)
|
||
if (art === 'G09' && zuLang(mail)) {
|
||
values.agb = `Die vollständigen AGB finden Sie unter ${shopSeite(ctx)}?ansicht=agb`
|
||
mail = renderMail(art, values, blocks)
|
||
gekuerzt = true
|
||
}
|
||
if (zuLang(mail)) throw new TypeError('Die Mail ist zu lang für die Warteschlange')
|
||
|
||
const e = einstellungenVon(ctx)
|
||
const eintrag = await einreihen(
|
||
{
|
||
featureId: ctx.doc.$id,
|
||
bookingId: order?.$id || (hatText(extra.bookingId) ? extra.bookingId : '-'),
|
||
art,
|
||
an,
|
||
fromName: shopName(ctx, schnappschuss(order)).slice(0, 120),
|
||
replyTo: text(anbieterVon(ctx, schnappschuss(order)).email) || text(e.anbieter?.email) || null,
|
||
template: art,
|
||
values,
|
||
blocks,
|
||
},
|
||
now,
|
||
)
|
||
return { ok: true, eintrag, gekuerzt }
|
||
} catch (err) {
|
||
console.warn(`[shop-mails] ${String(art).slice(0, 20)} nicht eingereiht: ${err?.name}: ${String(err?.message).slice(0, 120)}`)
|
||
return { ok: false, fehler: 'Die E-Mail an den Käufer konnte nicht eingereiht werden.' }
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Haendler
|
||
|
||
/** Portal-Adresse der Webshop-Seite des Projekts. */
|
||
const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#webshop?projekt=${encodeURIComponent(ctx?.shop?.projectId || ctx?.doc?.projectId || '')}`
|
||
|
||
/**
|
||
* Werte und Bloecke von K25 "Neue Bestellung" (an den Haendler) aus der bezahlten Bestellung.
|
||
* @param {{ ueberverkauft?: boolean }} [optionen] ueberverkauft: der Bestand reichte nicht (Block ueberverkauft)
|
||
* @returns {{ values: Record<string, string>, blocks: Record<string, boolean> }}
|
||
*/
|
||
export function haendlerK25(ctx, order, { ueberverkauft = false } = {}) {
|
||
const snap = schnappschuss(order)
|
||
const alle = kaeuferWerte(ctx, order)
|
||
const kaeufer = istObjekt(snap.kaeufer) ? snap.kaeufer : {}
|
||
return {
|
||
values: {
|
||
shopname: alle.betrieb_name,
|
||
bestellnummer: alle.bestellnummer,
|
||
gesamt: alle.gesamt,
|
||
positionen: alle.positionen,
|
||
lieferart_text: alle.lieferart_text,
|
||
kaeufer_name: oderLeer(kaeufer.name ?? snap.name),
|
||
portal_url: portalUrl(ctx),
|
||
},
|
||
blocks: { ueberverkauft: ueberverkauft === true, sepa: text(snap.zahlungsart) === 'sepa_debit' },
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Werte und Bloecke von K26 "Widerruf eingegangen" (an den Haendler) aus dem Widerruf (shopWiderrufe-Dokument oder
|
||
* { name, email, bestellnummer, nachricht, eingang }); eingang = Date | ISO.
|
||
*/
|
||
export function haendlerK26(ctx, widerruf, now = new Date()) {
|
||
const nachricht = text(widerruf?.nachricht)
|
||
return {
|
||
values: {
|
||
shopname: shopName(ctx),
|
||
bestellnummer: oderLeer(widerruf?.bestellnummer),
|
||
kaeufer_name: oderLeer(widerruf?.name),
|
||
kaeufer_email: oderLeer(widerruf?.email),
|
||
eingang: eingangText(widerruf?.eingang ?? widerruf?.eingangAt ?? now),
|
||
nachricht,
|
||
portal_url: portalUrl(ctx),
|
||
},
|
||
blocks: { nachricht: nachricht !== '' },
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Schickt eine Haendlermail (K25, K26) an die E-Mail des Kundenkontos, ohne BCC ans Team. kunde_name, shopname und portal_url
|
||
* ergaenzt die Funktion, wenn die Vorlage sie kennt und values sie nicht setzt.
|
||
* @param {string} template 'K25' | 'K26'
|
||
* @param {{ doc: object, shop: object }} ctx
|
||
* @param {Record<string, string>} values
|
||
* @param {Record<string, boolean>} [blocks]
|
||
* @returns {Promise<{ ok: true } | { ok: false, fehler: string }>}
|
||
*/
|
||
export async function anHaendler(template, ctx, values = {}, blocks = {}) {
|
||
try {
|
||
const kunde = await customerOf(ctx?.shop?.customerId)
|
||
if (!hatText(kunde?.email)) {
|
||
console.warn(`[shop-mails] ${String(template).slice(0, 20)} nicht gesendet: Kunde ohne E-Mail-Adresse`)
|
||
return { ok: false, fehler: 'Der Kunde hat keine E-Mail-Adresse.' }
|
||
}
|
||
const erlaubt = loadTemplate(template).placeholders
|
||
const standard = { kunde_name: text(kunde.name) || 'Kunde', shopname: shopName(ctx), portal_url: portalUrl(ctx) }
|
||
const alle = { ...standard, ...values }
|
||
await sendCustomerMail(kunde.email, {
|
||
template,
|
||
values: Object.fromEntries(Object.entries(alle).filter(([schluessel]) => erlaubt.includes(schluessel))),
|
||
blocks,
|
||
bccTeam: false,
|
||
})
|
||
return { ok: true }
|
||
} catch (err) {
|
||
// Nur Art und Code des Fehlers: Meldungen des Mailservers koennen die Adresse enthalten
|
||
console.warn(`[shop-mails] ${String(template).slice(0, 20)} nicht gesendet: ${err?.name} ${String(err?.code ?? err?.responseCode ?? err?.status ?? '').slice(0, 40)}`.trim())
|
||
return { ok: false, fehler: 'Die E-Mail an den Händler konnte nicht gesendet werden.' }
|
||
}
|
||
}
|