Automatische Mails im neuen Design (HTML mit Textfassung)
Alle 22 automatischen Mails gehen jetzt als HTML nach den Vorlagen aus
server/mail-templates raus, dazu eine aus dem HTML erzeugte Textfassung
(multipart/alternative). Werte werden HTML-sicher eingesetzt, optionale
Blöcke (WENN/ENDE) je nach Fall ein- oder ausgeblendet.
- Neu: services/mailTemplates.js (Vorlagen lesen, Platzhalter, Blöcke,
Textfassung); mailer.js nimmt { template, values, blocks }.
- K11 "Passwort geändert" war nicht im Design-Export: aus den Bausteinen
von K02 zusammengesetzt.
- T09 (Widerruf, intern): Fälle "kein Abo gefunden" und "Stripe-Kündigung
fehlgeschlagen" ergänzt; bisher stand dort immer "auf Kündigung gesetzt".
- Einheitlich: Datum TT.MM.JJJJ, Beträge deutsch (T04 "50,00 €"),
"WEBklar" statt "Webklar", Anrede mit Kundennamen, Team-Absender
"WEBklar Kundenportal", Betreff T05 "Kündigung (Stripe)".
- Logo für die Mails: public/webklar-logo.png (192 px, aus dem Design-SVG).
Tests: 52 neue (jede Vorlage: Kopf, Platzhalter, Blöcke, Rendern,
Escaping, Textfassung), npm test 97/97 auf Node 20.19.5; alle 40 Auslöser
einmal durch den Server geschickt (Appwrite-Mock, Stripe-Double,
SMTP-Falle mit STARTTLS).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3daffffa3
commit
18c3cdf3e5
11 files changed
+657
-316
No files matched your search
+24
-37
@@ -23,7 +23,8 @@ const COLLECTION = () => config.collections.aboOffers
|
||||
|
||||
export const OFFER_STATUS = { open: 'offen', active: 'aktiv', withdrawn: 'zurueckgezogen' }
|
||||
|
||||
const FOOTER = 'WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler · Telefon 0176 23726355 · support@webklar.com'
|
||||
// Anzeige im Team-Hinweis (wie im Admin-Bereich)
|
||||
const OFFER_STATUS_LABEL = { offen: 'offen', aktiv: 'aktiv (schon eingelöst)', zurueckgezogen: 'zurückgezogen' }
|
||||
|
||||
function httpError(status, message) {
|
||||
const err = new Error(message)
|
||||
@@ -256,40 +257,21 @@ export function validateOfferInput(body) {
|
||||
*/
|
||||
async function mailOffer(doc, customer, nextChargeAt = null) {
|
||||
const running = doc.scope === 'laufend'
|
||||
const lines = [
|
||||
`Guten Tag ${customer.name || ''}`.trim() + ',',
|
||||
'',
|
||||
running
|
||||
? `wie besprochen gilt für Ihr WEBklar Wartungs-Abo ab der nächsten Abbuchung${nextChargeAt ? ` am ${fmtDate(nextChargeAt)}` : ''}:`
|
||||
: 'wie besprochen haben wir Ihnen im Kundenportal ein persönliches Angebot für das WEBklar Wartungs-Abo hinterlegt:',
|
||||
'',
|
||||
` Angebot: ${offerText(doc)}`,
|
||||
` Ihre Ersparnis: ${euro(savingsOf(doc))}`,
|
||||
...(running ? [] : [` Gültig bis: ${fmtDate(doc.validUntil)}`]),
|
||||
...(doc.note ? [` Hinweis: ${doc.note}`] : []),
|
||||
'',
|
||||
...(running
|
||||
? ['Der Rabatt wird bei den Abbuchungen automatisch berücksichtigt und erscheint auf Ihren Rechnungen.']
|
||||
: [
|
||||
`Sie können das Abo zu diesem Preis bis zum ${fmtDate(doc.validUntil)} im Kundenportal abschließen:`,
|
||||
`${config.publicBaseUrl}/dashboard.html#abo`,
|
||||
'',
|
||||
'Es gelten unsere Vertragsbedingungen (Mindestlaufzeit 12 Monate, danach monatlich kündbar; gemäß § 19 UStG',
|
||||
`ohne Umsatzsteuer): ${config.publicBaseUrl}/abo-bedingungen.html`,
|
||||
]),
|
||||
'',
|
||||
'Viele Grüße',
|
||||
'Ihr WEBklar-Team',
|
||||
'',
|
||||
FOOTER,
|
||||
]
|
||||
const values = {
|
||||
name: customer.name || '',
|
||||
angebot_text: offerText(doc),
|
||||
ersparnis: euro(savingsOf(doc)),
|
||||
hinweis: doc.note || '',
|
||||
// K06 (laufendes Abo): ab wann der Rabatt gilt; K05: bis wann das Angebot angenommen werden kann
|
||||
...(running ? { naechste_abbuchung: fmtDate(nextChargeAt) } : { gueltig_bis: fmtDate(doc.validUntil) }),
|
||||
}
|
||||
const ref = shortRef(doc.$id)
|
||||
try {
|
||||
const info = await sendCustomerMail(
|
||||
customer.email,
|
||||
running ? 'Ihr Angebot für das WEBklar Wartungs-Abo ist eingerichtet' : 'Ihr persönliches Angebot für das WEBklar Wartungs-Abo',
|
||||
lines
|
||||
)
|
||||
const info = await sendCustomerMail(customer.email, {
|
||||
template: running ? 'K06' : 'K05',
|
||||
values,
|
||||
blocks: { hinweis: Boolean(doc.note) },
|
||||
})
|
||||
// Lehnt der Mailserver nur den Kunden ab, geht die BCC-Kopie trotzdem raus und sendMail meldet Erfolg
|
||||
const to = String(customer.email).toLowerCase()
|
||||
if ((info?.rejected || []).some((r) => String(r?.address || r).toLowerCase() === to)) {
|
||||
@@ -435,10 +417,15 @@ export async function activateOfferFromCheckout(session) {
|
||||
const doc = await getDocument(COLLECTION(), offerId).catch(() => null)
|
||||
if (!doc || doc.status === OFFER_STATUS.active) return doc
|
||||
if (doc.status !== OFFER_STATUS.open) {
|
||||
await sendAdminNotice(`Angebot ${shortRef(offerId)} war beim Abschluss nicht mehr offen`, [
|
||||
`Abo ${subscriptionId} wurde mit dem Angebot „${offerText(doc)}“ abgeschlossen, das Angebot ist aber „${doc.status}“.`,
|
||||
'Bitte in Stripe prüfen, ob der Rabatt am Abo hängt, und ggf. korrigieren.',
|
||||
])
|
||||
await sendAdminNotice({
|
||||
template: 'T03',
|
||||
values: {
|
||||
angebot_nr: shortRef(offerId),
|
||||
stripe_abo_id: subscriptionId,
|
||||
angebot_text: offerText(doc),
|
||||
angebot_status: OFFER_STATUS_LABEL[doc.status] || doc.status,
|
||||
},
|
||||
})
|
||||
return null
|
||||
}
|
||||
if (session.metadata?.offerDeferred === '1') {
|
||||
|
||||
Reference in new issue
Block a user