Webshop E4: Mails (G09-G14 an Käufer, K25/K26 an Händler), shop/mails.js, Mailversand der Warteschlange auch bei Terminplaner off
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>
This commit is contained in:
1 parent
38bd56fe8b
commit
0ebd2e6663
15 files changed
+1570
-26
No files matched your search
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G09 · Bestellbestätigung (Webshop, dauerhafter Datenträger nach § 312f Abs. 2 BGB)
|
||||
Betreff: Ihre Bestellung {{bestellnummer}} bei {{betrieb_name}}
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{bestellnummer}}, {{datum}}, {{positionen}}, {{zwischensumme}}, {{versand}}, {{gesamt}}, {{steuer_hinweis}}, {{lieferart_text}}, {{lieferadresse}}, {{abholort}}, {{lieferzeit}}, {{zahlungsart}}, {{ausschluss_text}}, {{agb}}, {{widerrufsbelehrung}}, {{widerrufsformular}}, {{gewaehrleistung}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Optionale Blöcke (vom Server gesetzt, versand und abholung schließen sich aus): versand, abholung, ausschluss
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
positionen = eine Zeile je Position „Menge × Name (Variante) – Summe“; lieferadresse mehrzeilig; agb, widerrufsbelehrung, widerrufsformular und gewaehrleistung sind die vollen Texte (Zeilenumbrüche werden zu <br>).
|
||||
Block versand: Lieferadresse. Block abholung: Abholort. Block ausschluss: Hinweis auf Artikel ohne Widerrufsrecht (ausschluss_text).
|
||||
Ist die Mail zu lang für die Warteschlange, ersetzt der Server den AGB-Text durch einen Link auf die AGB im Shop.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Ihre Bestellung {{bestellnummer}} bei {{betrieb_name}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Vielen Dank für Ihre Bestellung</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag,<br>wir haben Ihre Bestellung bei {{betrieb_name}} erhalten und Ihre Zahlung ist eingegangen. Bitte bewahren Sie diese E-Mail auf: Sie enthält alle Angaben zu Ihrem Vertrag.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestelldatum</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{datum}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Anbieter</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{anbieter_zeile}}</td></tr>
|
||||
</table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Ihre Artikel</td></tr>
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;">{{positionen}}</td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Zwischensumme</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{zwischensumme}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Versand</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{versand}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Gesamt</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{gesamt}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Steuer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{steuer_hinweis}}</td></tr>
|
||||
</table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Lieferung und Zahlung</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Lieferart</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{lieferart_text}}</td></tr>
|
||||
<!-- WENN versand -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Lieferadresse</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{lieferadresse}}</td></tr>
|
||||
<!-- ENDE versand -->
|
||||
<!-- WENN abholung -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Abholort</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{abholort}}</td></tr>
|
||||
<!-- ENDE abholung -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Lieferzeit</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{lieferzeit}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Zahlungsart</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{zahlungsart}}</td></tr>
|
||||
</table></td></tr>
|
||||
<!-- WENN ausschluss -->
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><strong>Hinweis zum Widerrufsrecht:</strong><br>{{ausschluss_text}}</td></tr></table></td></tr>
|
||||
<!-- ENDE ausschluss -->
|
||||
<tr><td class="px ink" style="padding:30px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Hinweis zur gesetzlichen Gewährleistung</td></tr>
|
||||
<tr><td class="px" style="padding:10px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="mut" style="padding:14px 18px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;color:#4a4843;">{{gewaehrleistung}}</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:30px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Widerrufsbelehrung</td></tr>
|
||||
<tr><td class="px" style="padding:10px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="mut" style="padding:14px 18px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;color:#4a4843;">{{widerrufsbelehrung}}</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:30px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Muster-Widerrufsformular</td></tr>
|
||||
<tr><td class="px" style="padding:10px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="mut" style="padding:14px 18px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;color:#4a4843;">{{widerrufsformular}}</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:30px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:24px;font-weight:bold;color:#111111;">Allgemeine Geschäftsbedingungen</td></tr>
|
||||
<tr><td class="px" style="padding:10px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="mut" style="padding:14px 18px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;color:#4a4843;">{{agb}}</td></tr></table></td></tr>
|
||||
<tr><td class="px mut" style="padding:26px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Sie können Ihren Vertrag jederzeit über die Schaltfläche „Vertrag widerrufen“ am Ende dieser E-Mail widerrufen, solange Ihnen ein Widerrufsrecht zusteht.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G10 · Bestellung versendet (Webshop)
|
||||
Betreff: Ihre Bestellung {{bestellnummer}} wurde versendet
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{bestellnummer}}, {{sendungsnummer}}, {{versanddienst}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Optionale Blöcke (vom Server gesetzt, nur wenn Versanddienst oder Sendungsnummer bekannt ist): sendung
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
Block sendung: Zeilen Versanddienst und Sendungsnummer (der Server setzt beide Werte, fehlende als „–“).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Ihre Bestellung {{bestellnummer}} wurde versendet</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihre Bestellung ist unterwegs</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag,<br>{{betrieb_name}} hat Ihre Bestellung versendet.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
<!-- WENN sendung -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Versanddienst</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{versanddienst}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Sendungsnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{sendungsnummer}}</td></tr>
|
||||
<!-- ENDE sendung -->
|
||||
</table></td></tr>
|
||||
<tr><td class="px mut" style="padding:20px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Bei Fragen zu Ihrer Bestellung antworten Sie einfach auf diese E-Mail.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G11 · Bestellung abholbereit (Webshop)
|
||||
Betreff: Ihre Bestellung {{bestellnummer}} ist abholbereit
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{bestellnummer}}, {{abholort}}, {{hinweis}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
abholort = Adresse der Abholung; hinweis = Hinweis des Händlers zur Abholung (der Server setzt einen Standardtext, wenn der Händler keinen hinterlegt hat).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Ihre Bestellung {{bestellnummer}} ist abholbereit</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihre Bestellung ist abholbereit</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag,<br>Ihre Bestellung liegt bei {{betrieb_name}} zur Abholung bereit.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Abholort</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{abholort}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Hinweis</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{hinweis}}</td></tr>
|
||||
</table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G12 · Eingang Widerruf (Eingangsbestätigung nach § 356a BGB, Webshop)
|
||||
Betreff: Eingangsbestätigung Ihres Widerrufs bei {{betrieb_name}}
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{name}}, {{bestellnummer}}, {{eingang}}, {{nachricht}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Optionale Blöcke (vom Server gesetzt, nur wenn der Käufer eine Erklärung eingegeben hat): nachricht
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
Empfänger ist die bei der Erklärung angegebene Adresse (ungeprüft). name, bestellnummer und nachricht sind Eingaben des Käufers. eingang = „TT.MM.JJJJ, HH:MM Uhr“ (Berliner Zeit).
|
||||
Die Mail enthält den Inhalt der Erklärung, Datum und Uhrzeit des Eingangs und gilt als Eingangsbestätigung.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Eingangsbestätigung Ihres Widerrufs bei {{betrieb_name}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Eingangsbestätigung Ihres Widerrufs</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag {{name}},<br>wir bestätigen den Eingang Ihrer Widerrufserklärung bei {{betrieb_name}}.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Eingegangen am</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{eingang}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{bestellnummer}}</td></tr>
|
||||
<!-- WENN nachricht -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Ihre Erklärung</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{nachricht}}</td></tr>
|
||||
<!-- ENDE nachricht -->
|
||||
</table></td></tr>
|
||||
<tr><td class="px mut" style="padding:20px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">{{betrieb_name}} prüft Ihre Erklärung und meldet sich bei Ihnen.</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Sie haben diese E-Mail erhalten, weil mit Ihrer E-Mail-Adresse ein Widerruf erklärt wurde. Waren Sie das nicht, können Sie diese Nachricht ignorieren.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G13 · Erstattung (Webshop)
|
||||
Betreff: Erstattung zu Ihrer Bestellung {{bestellnummer}}
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{bestellnummer}}, {{betrag}}, {{grund}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Optionale Blöcke (vom Server gesetzt, nur wenn der Händler einen Grund angegeben hat): grund
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
betrag = erstatteter Betrag, z. B. „12,50 €“.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Erstattung zu Ihrer Bestellung {{bestellnummer}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihre Erstattung</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag,<br>{{betrieb_name}} hat Ihnen einen Betrag zu Ihrer Bestellung erstattet.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Erstatteter Betrag</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{betrag}}</strong></td></tr>
|
||||
<!-- WENN grund -->
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Grund</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{grund}}</td></tr>
|
||||
<!-- ENDE grund -->
|
||||
</table></td></tr>
|
||||
<tr><td class="px mut" style="padding:20px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Gutschrift erfolgt über die ursprüngliche Zahlungsart. Je nach Zahlungsart kann es einige Tage dauern, bis der Betrag bei Ihnen sichtbar ist.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
G14 · Zahlung fehlgeschlagen (Webshop)
|
||||
Betreff: Ihre Bestellung {{bestellnummer}} wurde nicht abgeschlossen
|
||||
Absender: „<Shopname>“ <Portal-Postfach> · Antworten an die E-Mail-Adresse des Händlers
|
||||
Empfänger: Käufer (Adresse aus der Bestellung bzw. der Widerrufserklärung) · ohne BCC
|
||||
Platzhalter: {{bestellnummer}}, {{farbe}}, {{betrieb_name}}, {{anbieter_zeile}}, {{impressum_url}}, {{datenschutz_url}}, {{widerruf_url}}
|
||||
Sie-Form, keine Anrede-Blöcke. Footer: Anbieter-Zeile mit Name, Anschrift und E-Mail, Impressum, Datenschutz, Link zur Widerrufsfunktion.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Ihre Bestellung {{bestellnummer}} wurde nicht abgeschlossen</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.btn2{border-color:#f2f1ee!important;}.btn2 a{color:#f2f1ee!important;}.rowline{border-color:#3a3a37!important;}.pill{background:#1f1f1e!important;}.pill span{color:#f2f1ee!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" bgcolor="{{farbe}}" style="background:{{farbe}};border-radius:16px 16px 0 0;padding:18px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="pill" bgcolor="#ffffff" style="background:#ffffff;border-radius:8px;padding:8px 14px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:22px;font-weight:bold;color:#111111;"><span style="color:#111111;">{{betrieb_name}}</span></td></tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:0 0 16px 16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px ink" style="padding:36px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:26px;line-height:32px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihre Bestellung wurde nicht abgeschlossen</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag,<br>die Zahlung für Ihre Bestellung bei {{betrieb_name}} ist fehlgeschlagen. Ihre Bestellung wurde nicht abgeschlossen; es wurde nichts berechnet.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="150" valign="top" style="width:150px;padding:11px 12px 11px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:11px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
</table></td></tr>
|
||||
<tr><td class="px mut" style="padding:20px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Sie können jederzeit erneut bestellen. Bei Fragen antworten Sie einfach auf diese E-Mail.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 40px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>{{betrieb_name}}</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><div><strong>{{betrieb_name}}</strong></div><div>{{anbieter_zeile}}</div></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="{{impressum_url}}" style="color:#4a4843;text-decoration:underline;">Impressum</a> · <a class="mut" href="{{datenschutz_url}}" style="color:#4a4843;text-decoration:underline;">Datenschutz</a> · <a class="mut" href="{{widerruf_url}}" style="color:#4a4843;text-decoration:underline;">Vertrag widerrufen</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Webshop von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K25 · Neue Bestellung im Webshop (an den Händler)
|
||||
Betreff: Neue Bestellung {{bestellnummer}} in Deinem Webshop
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben von Käufern)
|
||||
Platzhalter: {{kunde_name}}, {{shopname}}, {{bestellnummer}}, {{gesamt}}, {{positionen}}, {{lieferart_text}}, {{kaeufer_name}}, {{portal_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): ueberverkauft, sepa
|
||||
Block ueberverkauft: der Bestand reichte nicht (die Bestellung war schon bezahlt). Block sepa: Zahlung per Lastschrift.
|
||||
positionen = eine Zeile je Position „Menge × Name (Variante) – Summe“; portal_url = Webshop-Seite des Projekts im Kundenportal.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Neue Bestellung {{bestellnummer}} in Deinem Webshop</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">In Deinem Webshop ist eine neue Bestellung eingegangen. ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Webshop</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Neue Bestellung</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Neue Bestellung {{bestellnummer}}</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>in Deinem Webshop „{{shopname}}“ ist eine bezahlte Bestellung eingegangen.</td></tr>
|
||||
<!-- WENN ueberverkauft -->
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><strong>Achtung: Bestand reichte nicht.</strong> Die Bestellung ist bezahlt, aber von mindestens einem Artikel war nicht mehr genug da. Bitte melde Dich beim Käufer oder erstatte die Bestellung im Portal.</td></tr></table></td></tr>
|
||||
<!-- ENDE ueberverkauft -->
|
||||
<!-- WENN sepa -->
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><strong>Zahlung per Lastschrift:</strong> Stripe hat die Zahlung bestätigt. Eine Lastschrift kann aber noch einige Wochen zurückgebucht werden. Bedenke das vor dem Versand teurer Ware.</td></tr></table></td></tr>
|
||||
<!-- ENDE sepa -->
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{bestellnummer}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Käufer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kaeufer_name}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Lieferart</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{lieferart_text}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Gesamt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{gesamt}}</strong></td></tr>
|
||||
</table></td></tr>
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;">{{positionen}}</td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Bestellung ansehen →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K26 · Widerruf im Webshop eingegangen (an den Händler)
|
||||
Betreff: Widerruf in Deinem Webshop eingegangen
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben von Käufern)
|
||||
Platzhalter: {{kunde_name}}, {{shopname}}, {{bestellnummer}}, {{kaeufer_name}}, {{kaeufer_email}}, {{eingang}}, {{nachricht}}, {{portal_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): nachricht
|
||||
Block nachricht: der Käufer hat eine Erklärung eingegeben. bestellnummer, kaeufer_name, kaeufer_email und nachricht sind Eingaben des Käufers (ungeprüft). eingang = „TT.MM.JJJJ, HH:MM Uhr“ (Berliner Zeit).
|
||||
Der Käufer hat die Eingangsbestätigung (G12) schon automatisch bekommen.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Widerruf in Deinem Webshop eingegangen</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Ein Käufer hat in Deinem Webshop einen Vertrag widerrufen. ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Webshop</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Widerruf</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Widerruf in Deinem Webshop eingegangen</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>ein Käufer hat in Deinem Webshop „{{shopname}}“ einen Vertrag widerrufen. Den Eingang haben wir ihm schon automatisch bestätigt.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Eingegangen am</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{eingang}}</strong></td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Bestellnummer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{bestellnummer}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Käufer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kaeufer_name}}</td></tr>
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">E-Mail</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kaeufer_email}}</td></tr>
|
||||
<!-- WENN nachricht -->
|
||||
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Erklärung</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{nachricht}}</td></tr>
|
||||
<!-- ENDE nachricht -->
|
||||
</table></td></tr>
|
||||
<tr><td class="px" style="padding:20px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:16px 20px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><strong>Was jetzt zu tun ist:</strong> Bitte bestätige dem Käufer den Widerruf persönlich und erstatte den Kaufpreis innerhalb von 14 Tagen nach Eingang des Widerrufs, wenn der Widerruf wirksam ist. Die Erstattung machst Du im Portal bei der Bestellung. Die Rücksendung der Ware klärst Du mit dem Käufer.</td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Widerrufe ansehen →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,390 @@
|
||||
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.' }
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { LOESCHFRIST_TAGE } from './frist.js'
|
||||
import { kontextPerFeature, ladeEinstellung } from './kontext.js'
|
||||
import { anGast } from './mails.js'
|
||||
import { beendeFestgefahrene, raeumeAuf, verarbeite } from './outbox.js'
|
||||
import { shopModus } from '../shop/schalter.js'
|
||||
import { termineAus } from './schalter.js'
|
||||
import { buchungsNummer } from './token.js'
|
||||
import { pruefeNow, zeitMs } from './util.js'
|
||||
@@ -57,6 +58,12 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
||||
* (`eingeschraenkt: true`): nur der Abo-Abgleich und die Loeschung abgeschalteter Funktionen, damit ein beendetes Abo auch bei
|
||||
* TERMINE_ENABLED=off zum Abschalten und zur Loeschung nach 30 Tagen fuehrt. Loeschfristen der Buchungen, Warteschlange und Zellen
|
||||
* bleiben aus.
|
||||
* - Webshop (SHOP_ENABLED != off): die Mail-Warteschlange gehoert auch den Kaeufermails des Shops (G09-G14, shop/mails.js). Deshalb
|
||||
* laufen bei Modus off des Terminplaners, aber aktivem Shop, der Mailversand-Teil des Minutenjobs (Warteschlange senden und
|
||||
* Betriebspruefung) und im Tagesjob das Beenden festgefahrener und das Loeschen alter Warteschlangen-Eintraege weiter; alles
|
||||
* andere bleibt an den Terminplaner-Modus gebunden. Der Tagesjob ist dann eingeschraenkt (`eingeschraenkt: true`, mit
|
||||
* `festgefahren` und `aufgeraeumt` im Ergebnis); gibt es keine bezahlten Terminplaner, laufen Abo-Abgleich und Loeschung
|
||||
* abgeschalteter Funktionen nicht.
|
||||
* - Keine ueberlappenden Laeufe: laeuft ein Job noch, wird der naechste Takt uebersprungen ({ uebersprungen: true }, einmal im
|
||||
* Log je Ueberlappung). Die Sperre gilt je `job:teil` (fuehreAus mit { teil }): haengt beim Minutenjob der Verfall, versendet der
|
||||
* naechste Takt trotzdem die Warteschlange und umgekehrt; uebersprungen gilt der Takt erst, wenn beide Teile uebersprungen sind
|
||||
@@ -127,18 +134,21 @@ async function schritt(lauf, was, fn, ersatz) {
|
||||
* Rahmen jedes Jobs: Schalter, Ueberlappung, Laufdaten. => Ergebnis von arbeit(lauf) | { aus: true } | { uebersprungen: true }
|
||||
* `teil` (Name eines Teiljobs, z. B. „Verfall“) gibt dem Teil eine eigene Ueberlappungssperre `job:teil` und nennt ihn in der
|
||||
* Warnung (`job (teil)`); `lauf.job` bleibt der Jobname (Fehlerlog, Warnung bei fehlender Collection).
|
||||
* `trotzAus` (async, => wahr/falsch): im Modus off laeuft der Job nur, wenn es wahr liefert; dann steht `lauf.eingeschraenkt` auf true
|
||||
* und der Job beschraenkt sich auf das, was auch bei abgeschaltetem Terminplaner gelten muss. Wirft es, wird gewarnt (ohne Kennungen)
|
||||
* `trotzAus` (async, => falsch/wahr/Objekt): im Modus off laeuft der Job nur, wenn es etwas Wahres liefert; dann steht
|
||||
* `lauf.eingeschraenkt` auf true, `lauf.freigabe` traegt die Antwort (Tagesjob: { termine, mails }) und der Job beschraenkt sich auf
|
||||
* das, was auch bei abgeschaltetem Terminplaner gelten muss. Wirft es, wird gewarnt (ohne Kennungen)
|
||||
* und { aus: true } geliefert; ohne `trotzAus` gilt Modus off wie bisher (sofort { aus: true }, ohne Abfrage). Die Ueberlappungssperre
|
||||
* gilt unveraendert je Job (bzw. `job:teil`), der eingeschraenkte Lauf teilt sie mit dem vollen.
|
||||
*/
|
||||
async function fuehreAus(job, now, arbeit, { teil = null, trotzAus = null } = {}) {
|
||||
const nowMs = pruefeNow(now, job)
|
||||
let eingeschraenkt = false
|
||||
let freigabe = null
|
||||
if (termineAus()) {
|
||||
if (trotzAus === null) return { aus: true }
|
||||
try {
|
||||
if (!(await trotzAus())) return { aus: true }
|
||||
freigabe = await trotzAus()
|
||||
if (!freigabe) return { aus: true }
|
||||
} catch (err) {
|
||||
console.warn(`[termine-jobs] ${job}: Modus off, die Prüfung auf bezahlte Terminplaner ist fehlgeschlagen, der Job bleibt aus: ${fehlerText(err)}`)
|
||||
return { aus: true }
|
||||
@@ -155,13 +165,16 @@ async function fuehreAus(job, now, arbeit, { teil = null, trotzAus = null } = {}
|
||||
}
|
||||
laufend.add(sperre)
|
||||
try {
|
||||
return await arbeit({ job, nowMs, fehler: 0, gedeckelt: 0, eingeschraenkt })
|
||||
return await arbeit({ job, nowMs, fehler: 0, gedeckelt: 0, eingeschraenkt, freigabe })
|
||||
} finally {
|
||||
laufend.delete(sperre)
|
||||
ueberlappGemeldet.delete(sperre)
|
||||
}
|
||||
}
|
||||
|
||||
/** Guard des Mailversand-Teils (fuehreAus: trotzAus): bei Modus off des Terminplaners laeuft er, solange der Webshop aktiv ist. */
|
||||
const shopMailsLaufen = async () => shopModus() !== 'off'
|
||||
|
||||
/** Laufdaten fuer einen Baustein, der ohne Job-Rahmen aufgerufen wird (Tests). */
|
||||
const einzelLauf = (job, now) => ({ job, nowMs: pruefeNow(now, job), fehler: 0, gedeckelt: 0 })
|
||||
|
||||
@@ -285,10 +298,15 @@ export async function minutenJob(now) {
|
||||
await schritt(lauf, 'Betriebsprüfung', () => pruefeBetrieb(now), [])
|
||||
return { faellig, gesendet, fehler: lauf.fehler }
|
||||
},
|
||||
{ teil: 'Mailversand' },
|
||||
{ teil: 'Mailversand', trotzAus: shopMailsLaufen },
|
||||
),
|
||||
])
|
||||
if (verfall.aus || mail.aus) return { aus: true }
|
||||
if (verfall.aus && mail.aus) return { aus: true }
|
||||
// Terminplaner aus, Webshop aktiv: nur der Mailversand-Teil lief
|
||||
if (verfall.aus) {
|
||||
if (mail.uebersprungen) return { uebersprungen: true }
|
||||
return { eingeschraenkt: true, verfallen: 0, faellig: mail.faellig ?? 0, gesendet: mail.gesendet ?? 0, fehler: mail.fehler ?? 0 }
|
||||
}
|
||||
if (verfall.uebersprungen && mail.uebersprungen) return { uebersprungen: true }
|
||||
return {
|
||||
verfallen: verfall.verfallen ?? 0,
|
||||
@@ -792,6 +810,23 @@ async function gibtBezahlteTerminplaner() {
|
||||
return treffer.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Freigabe des Tagesjobs im Modus off (fuehreAus: trotzAus): false (aus) oder { termine, mails }. termine = es gibt ein bezahltes
|
||||
* Termine-Dokument (Abo-Abgleich und Loeschung abgeschalteter Funktionen), mails = der Webshop ist aktiv (Warteschlange aufraeumen).
|
||||
* Eine einzige Abfrage; scheitert sie bei aktivem Shop, laeuft nur der Mail-Teil (Warnung), sonst wirft sie wie bisher.
|
||||
*/
|
||||
async function tagesjobFreigabe() {
|
||||
const mails = shopModus() !== 'off'
|
||||
let termine = false
|
||||
try {
|
||||
termine = await gibtBezahlteTerminplaner()
|
||||
} catch (err) {
|
||||
if (!mails) throw err
|
||||
console.warn(`[termine-jobs] taeglicherJob: die Prüfung auf bezahlte Terminplaner ist fehlgeschlagen, es läuft nur die Mail-Warteschlange: ${fehlerText(err)}`)
|
||||
}
|
||||
return termine || mails ? { termine, mails } : false
|
||||
}
|
||||
|
||||
/**
|
||||
* Abo-Abgleich (aboAbgleich.js) als Schritt des Tagesjobs: ein Fehler stoppt weder die Loeschung noch den Rest. Die Fehler des
|
||||
* Abgleichs (Lese- und Versandfehler, schon im Log) kommen in lauf.fehler. => Zaehler des Abgleichs
|
||||
@@ -864,7 +899,7 @@ export async function taeglicherJob(now) {
|
||||
await schritt(lauf, 'Tagesjob-Meldung', () => meldeTagesjob(ergebnis, now), [])
|
||||
return ergebnis
|
||||
},
|
||||
{ trotzAus: gibtBezahlteTerminplaner },
|
||||
{ trotzAus: tagesjobFreigabe },
|
||||
)
|
||||
} catch (err) {
|
||||
// Abbruch ausserhalb der Schritte: das Team erfaehrt es; der Fehler (Text im Log des Aufrufers) geht unveraendert weiter
|
||||
@@ -880,19 +915,28 @@ export async function taeglicherJob(now) {
|
||||
* und wird wie im vollen Lauf gemeldet (Fehler gehen als Vorfall ans Team).
|
||||
*/
|
||||
async function eingeschraenkterTagesjob(lauf, now) {
|
||||
const aboAbgleich = await aboAbgleichSchritt(lauf, now)
|
||||
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), {
|
||||
funktionen: 0,
|
||||
buchungen: 0,
|
||||
gedeckelt: 0,
|
||||
})
|
||||
// Ohne Freigabe-Angabe (Aufruf ohne tagesjobFreigabe) gilt das bisherige Verhalten: nur Terminplaner-Teile
|
||||
const termine = lauf.freigabe?.termine !== false
|
||||
const mails = lauf.freigabe?.mails === true
|
||||
const aboAbgleich = termine ? await aboAbgleichSchritt(lauf, now) : aboErsatz()
|
||||
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = termine
|
||||
? await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), { funktionen: 0, buchungen: 0, gedeckelt: 0 })
|
||||
: { funktionen: 0, buchungen: 0, gedeckelt: 0 }
|
||||
// Webshop aktiv: festgefahrene Mails beenden (sie bleiben 48 Stunden als Zustellfehler sichtbar), dann alte Eintraege loeschen
|
||||
const mailErgebnis = mails
|
||||
? {
|
||||
festgefahren: await schritt(lauf, 'Festgefahrene Mails', () => beendeFestgefahrene(now), 0),
|
||||
aufgeraeumt: await schritt(lauf, 'Warteschlange aufräumen', () => raeumeAuf(now), 0),
|
||||
}
|
||||
: {}
|
||||
const ergebnis = {
|
||||
eingeschraenkt: true,
|
||||
aboAbgleich,
|
||||
abgeschaltet,
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: abgeschaltetGedeckelt, zellen: 0 },
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: abgeschaltetGedeckelt ?? 0, zellen: 0 },
|
||||
geloescht: abgeschaltet.buchungen,
|
||||
aktiveBetriebe: null,
|
||||
...mailErgebnis,
|
||||
fehler: lauf.fehler,
|
||||
}
|
||||
await schritt(lauf, 'Tagesjob-Meldung', () => meldeTagesjob(ergebnis, now), [])
|
||||
@@ -926,9 +970,12 @@ export function tagesbericht(ergebnis) {
|
||||
? ''
|
||||
: `Abo-Abgleich ${zahl(abo.geprueft)} geprüft / ${zahl(abo.abgeschaltet)} abgeschaltet / ${zahl(abo.wiederEin)} wieder ein / ${zahl(abo.mails)} Mails / ${zahl(abo.ausgenommen)} ausgenommen, `
|
||||
if (ergebnis.eingeschraenkt) {
|
||||
// Webshop aktiv: dazu die Warteschlange (festgefahrene Mails beenden, alte Eintraege loeschen)
|
||||
const mitMails = Number.isInteger(ergebnis.aufgeraeumt)
|
||||
const mailSegment = mitMails ? `${ergebnis.aufgeraeumt} Outbox, ${zahl(ergebnis.festgefahren)} Mails nicht zugestellt, ` : ''
|
||||
return (
|
||||
`[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung): ${a.buchungen} Buchungen gelöscht (abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||
`${a.funktionen} Funktionen bereinigt, ${deckel}${aboSegment}${ergebnis.fehler} Fehler`
|
||||
`[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung${mitMails ? ' sowie Mail-Warteschlange' : ''}): ${a.buchungen} Buchungen gelöscht (abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||
`${a.funktionen} Funktionen bereinigt, ${mailSegment}${deckel}${aboSegment}${ergebnis.fehler} Fehler`
|
||||
)
|
||||
}
|
||||
const l = ergebnis.loeschfristen
|
||||
|
||||
@@ -39,7 +39,7 @@ import { pruefeNow, pruefeText, zeitMs } from './util.js'
|
||||
* pause Anmeldepause laeuft
|
||||
* budget Plaetze im Fenster >= mailsProStunde
|
||||
* reserve nachrangige Mails (G05, K20, K21) im Fenster >= max(1, floor(mailsProStunde x (1 - mailsReserve))): so bleibt
|
||||
* ein Teil der Stunde den Vorrang-Mails (G01, G02, G08) und den normalen Mails vorbehalten
|
||||
* ein Teil der Stunde den Vorrang-Mails (G01, G02, G08, Webshop G09, G12) und den normalen Mails vorbehalten
|
||||
* kontingent Mails dieser Funktion im Fenster >= mailsProFunktion: eine Funktion kann die Stunde nicht allein fuellen
|
||||
*
|
||||
* Minutenjob (verarbeite): liest nie alle wartenden Eintraege. Bei Pause oder leerem Gesamtbudget endet er ohne Abfrage.
|
||||
@@ -71,8 +71,12 @@ const ID_BLOCK = 50
|
||||
|
||||
const iso = (wertMs) => new Date(wertMs).toISOString()
|
||||
|
||||
/** Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). */
|
||||
export const MAIL_VORRANG = Object.freeze(['G01', 'G02', 'G08'])
|
||||
/**
|
||||
* Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). Dazu die beiden
|
||||
* Pflichtmails des Webshops: G09 Bestellbestaetigung (dauerhafter Datentraeger) und G12 Eingangsbestaetigung des Widerrufs. Die
|
||||
* uebrigen Shop-Mails G10, G11, G13, G14 sind 'normal'.
|
||||
*/
|
||||
export const MAIL_VORRANG = Object.freeze(['G01', 'G02', 'G08', 'G09', 'G12'])
|
||||
/** Mails, die zuerst warten (Erinnerung, Hinweise an den Betrieb): sie nehmen nie die ganze Stunde. */
|
||||
export const MAIL_NACHRANGIG = Object.freeze(['G05', 'K20', 'K21'])
|
||||
|
||||
@@ -115,7 +119,7 @@ function gesamtBremse(nowMs) {
|
||||
* 'pause' | 'budget' | 'reserve' | 'kontingent'. Nur lesen: belegt keinen Platz.
|
||||
* @param {number} nowMs
|
||||
* @param {string} featureId
|
||||
* @param {string} art Vorlagen-ID der Mail ('G01' ... 'K21')
|
||||
* @param {string} art Vorlagen-ID der Mail ('G01' ... 'K21', Shop: 'G09' ... 'G14')
|
||||
* @returns {null|'pause'|'budget'|'reserve'|'kontingent'}
|
||||
*/
|
||||
export function pruefePlatz(nowMs, featureId, art) {
|
||||
@@ -438,7 +442,7 @@ async function verarbeiteKlasse(klasse, nowMs, now, lauf) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Minutenjob: sendet faellige wartende Eintraege, Vorrang-Mails (G01, G02, G08) vor normalen vor nachrangigen (G05, K20, K21),
|
||||
* Minutenjob: sendet faellige wartende Eintraege, Vorrang-Mails (G01, G02, G08, G09, G12) vor normalen vor nachrangigen (G05, K20, K21),
|
||||
* in jeder Klasse die faelligsten zuerst, bis Pause oder Stundenbudget bremsen. Er liest begrenzte Listen statt aller Eintraege
|
||||
* (siehe Kopfkommentar); Eintraege, die Vorrang-Reserve oder Kontingent ihrer Funktion bremsen, bleiben `wartet`, und die uebrigen
|
||||
* kommen trotzdem dran. Fehler bei einzelnen Eintraegen (z. B. Appwrite) stehen im Log und stoppen den Lauf nicht.
|
||||
|
||||
+192
-2
@@ -6,10 +6,12 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
|
||||
|
||||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||||
// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9)
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9)
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 24), ...ids('T', 19), 'T20', 'T21']
|
||||
const EXPECTED = [...ids('G', 14), ...ids('K', 26), ...ids('T', 19), 'T20', 'T21']
|
||||
// GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten)
|
||||
const GUEST = ids('G', 8)
|
||||
const SHOP_GUEST = ['G09', 'G10', 'G11', 'G12', 'G13', 'G14']
|
||||
|
||||
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
|
||||
const allBlocks = (tpl, on) => Object.fromEntries(tpl.blocks.map((b) => [b, on]))
|
||||
@@ -1073,3 +1075,191 @@ test('K24 abgelehnt: nennt die Begründung (maskiert, Zeilenumbruch), keinen Pre
|
||||
assert.match(mail.html, /Wir verkaufen keine <Waffen>\.<br>Bitte melde Dich\./)
|
||||
assert.doesNotMatch(mail.text, /299 €|UStG|angenommen/)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webshop E4: Käufermails G09–G14 (Sie-Form, Betreff in der Vorlage, Fuß mit Anbieter und Widerrufsfunktion)
|
||||
// und K25, K26 an den Händler (Du-Form)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SHOP_GEMEIN = ['farbe', 'betrieb_name', 'anbieter_zeile', 'impressum_url', 'datenschutz_url', 'widerruf_url']
|
||||
|
||||
test('G09–G14: Platzhalter, Blöcke und Betreff wie im Plan (G-Vorlagen des Webshops haben keine Anrede-Blöcke)', () => {
|
||||
const erwartet = {
|
||||
G09: {
|
||||
s: 'Ihre Bestellung {{bestellnummer}} bei {{betrieb_name}}',
|
||||
p: ['bestellnummer', 'datum', 'positionen', 'zwischensumme', 'versand', 'gesamt', 'steuer_hinweis', 'lieferart_text', 'lieferadresse', 'abholort', 'lieferzeit', 'zahlungsart', 'ausschluss_text', 'agb', 'widerrufsbelehrung', 'widerrufsformular', 'gewaehrleistung'],
|
||||
b: ['versand', 'abholung', 'ausschluss'],
|
||||
},
|
||||
G10: { s: 'Ihre Bestellung {{bestellnummer}} wurde versendet', p: ['bestellnummer', 'sendungsnummer', 'versanddienst'], b: ['sendung'] },
|
||||
G11: { s: 'Ihre Bestellung {{bestellnummer}} ist abholbereit', p: ['bestellnummer', 'abholort', 'hinweis'], b: [] },
|
||||
G12: { s: 'Eingangsbestätigung Ihres Widerrufs bei {{betrieb_name}}', p: ['name', 'bestellnummer', 'eingang', 'nachricht'], b: ['nachricht'] },
|
||||
G13: { s: 'Erstattung zu Ihrer Bestellung {{bestellnummer}}', p: ['bestellnummer', 'betrag', 'grund'], b: ['grund'] },
|
||||
G14: { s: 'Ihre Bestellung {{bestellnummer}} wurde nicht abgeschlossen', p: ['bestellnummer'], b: [] },
|
||||
}
|
||||
assert.deepEqual(Object.keys(erwartet), SHOP_GUEST)
|
||||
for (const [id, { s, p, b }] of Object.entries(erwartet)) {
|
||||
const tpl = loadTemplate(id)
|
||||
assert.equal(tpl.subject, s, id)
|
||||
assert.deepEqual([...tpl.placeholders].sort(), [...p, ...SHOP_GEMEIN].sort(), `${id}: Platzhalter`)
|
||||
assert.deepEqual([...tpl.blocks].sort(), [...b].sort(), `${id}: Blöcke`)
|
||||
}
|
||||
})
|
||||
|
||||
test('G09–G14: neutrales Layout in der Shop-Farbe, kein Logo, Fuß mit Anbieter, Impressum, Datenschutz und Widerrufsfunktion', () => {
|
||||
for (const id of SHOP_GUEST) {
|
||||
const tpl = loadTemplate(id)
|
||||
const source = sourceOf(id)
|
||||
assert.match(source, /<td[^>]*style="[^"]*background:\{\{farbe\}\}/, `${id}: Kopfbalken in {{farbe}}`)
|
||||
assert.doesNotMatch(source, /<img|webklar-logo|project\.webklar\.com/i, `${id}: kein Bild, kein WEBklar-Logo, keine Portal-Adresse`)
|
||||
assert.doesNotMatch(source, /https?:\/\/(?!www\.w3\.org)|google|tracking|pixel/i, `${id}: keine festen Adressen, kein Tracking`)
|
||||
for (const on of [true, false]) {
|
||||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, on))
|
||||
assert.deepEqual(mail.problems, [], id)
|
||||
assert.ok(mail.text.includes('Wert-anbieter_zeile'), `${id}: Anbieter im Fuß`)
|
||||
assert.ok(mail.text.includes('Diese E-Mail wurde im Auftrag von Wert-betrieb_name über den Webshop von WEBklar versendet.'), `${id}: Fußsatz`)
|
||||
assert.match(mail.html, /href="Wert-impressum_url"[^>]*>Impressum</, id)
|
||||
assert.match(mail.html, /href="Wert-datenschutz_url"[^>]*>Datenschutz</, id)
|
||||
assert.match(mail.html, /href="Wert-widerruf_url"[^>]*>Vertrag widerrufen</, id)
|
||||
assert.match(mail.html, /background:Wert-farbe/, id)
|
||||
assert.doesNotMatch(mail.text, DU_FORM, `${id}: Sie-Form, nicht Du\n${mail.text}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const G_WERTE = { bestellnummer: '26-K7QM2X', betrieb_name: 'Töpferei <Muster>', farbe: '#112233', anbieter_zeile: 'Töpferei Muster, Hauptstr. 1, 12345 Musterstadt, info@example.com', impressum_url: 'https://shop.example/impressum', datenschutz_url: 'https://shop.example/datenschutz', widerruf_url: 'https://project.webklar.com/shop/sk/widerruf' }
|
||||
const g = (id, werte, blocks = {}) => renderMail(id, { ...G_WERTE, ...werte }, blocks)
|
||||
|
||||
const G09_WERTE = {
|
||||
datum: '05.10.2026',
|
||||
positionen: '2 × Tasse (blau) – 24,00 €\n1 × Teller – 12,50 €',
|
||||
zwischensumme: '36,50 €',
|
||||
versand: '4,90 €',
|
||||
gesamt: '41,40 €',
|
||||
steuer_hinweis: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
||||
lieferart_text: 'Versand nach Deutschland',
|
||||
lieferadresse: 'Erika Beispiel\nMusterweg 3\n12345 Musterstadt',
|
||||
abholort: 'Werkstatt, Hauptstr. 1',
|
||||
lieferzeit: '2–4 Werktage',
|
||||
zahlungsart: 'Kreditkarte',
|
||||
ausschluss_text: 'Teller: Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt',
|
||||
agb: 'AGB-Text Zeile 1\nAGB-Text Zeile 2',
|
||||
widerrufsbelehrung: 'Widerrufsbelehrung Zeile 1\nZeile 2',
|
||||
widerrufsformular: 'An Töpferei Muster\nHiermit widerrufe ich',
|
||||
gewaehrleistung: 'GESETZLICHE GEWÄHRLEISTUNG\n\nMindestens zwei Jahre',
|
||||
}
|
||||
|
||||
test('G09 Versand: Betreff mit Bestellnummer und Shop, Positionen, Summen, Lieferadresse, alle Rechtstexte, Links', () => {
|
||||
const mail = g('G09', G09_WERTE, { versand: true, abholung: false, ausschluss: true })
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X bei Töpferei <Muster>')
|
||||
assert.match(mail.html, /Töpferei <Muster>/, 'Shopname maskiert')
|
||||
assert.match(mail.html, /2 × Tasse \(blau\) – 24,00 €<br>1 × Teller – 12,50 €/)
|
||||
assert.match(mail.html, /Erika Beispiel<br>Musterweg 3<br>12345 Musterstadt/)
|
||||
for (const teil of ['26-K7QM2X', '05.10.2026', '36,50 €', '4,90 €', '41,40 €', '§ 19 UStG', 'Versand nach Deutschland', '2–4 Werktage', 'Kreditkarte', 'Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt', 'AGB-Text Zeile 2', 'Widerrufsbelehrung Zeile 1', 'Hiermit widerrufe ich', 'GESETZLICHE GEWÄHRLEISTUNG', 'Hinweis zur gesetzlichen Gewährleistung', 'Muster-Widerrufsformular', 'Allgemeine Geschäftsbedingungen']) {
|
||||
assert.ok(mail.text.includes(teil), teil)
|
||||
}
|
||||
assert.match(mail.text, /Lieferadresse: Erika Beispiel/)
|
||||
assert.doesNotMatch(mail.text, /Abholort|Werkstatt, Hauptstr/)
|
||||
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/shop\/sk\/widerruf"/)
|
||||
})
|
||||
|
||||
test('G09 Abholung ohne Ausschluss: Abholort statt Lieferadresse, kein Widerrufs-Ausschluss', () => {
|
||||
const mail = g('G09', { ...G09_WERTE, lieferart_text: 'Abholung', versand: 'Abholung – kostenlos' }, { versand: false, abholung: true, ausschluss: false })
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.match(mail.text, /Abholort: Werkstatt, Hauptstr\. 1/)
|
||||
assert.doesNotMatch(mail.text, /Lieferadresse|Musterweg|Hinweis zum Widerrufsrecht|ausgeschlossen/)
|
||||
assert.match(mail.text, /Abholung – kostenlos/)
|
||||
})
|
||||
|
||||
test('G10: Sendungsnummer und Versanddienst nur im Block sendung', () => {
|
||||
const mit = g('G10', { sendungsnummer: '0034<1>', versanddienst: 'DHL' }, { sendung: true })
|
||||
assert.deepEqual(mit.problems, [])
|
||||
assert.equal(mit.subject, 'Ihre Bestellung 26-K7QM2X wurde versendet')
|
||||
assert.match(mit.text, /Versanddienst: DHL/)
|
||||
assert.match(mit.text, /Sendungsnummer: 0034<1>/)
|
||||
assert.match(mit.html, /0034<1>/)
|
||||
const ohne = g('G10', { sendungsnummer: '', versanddienst: '' }, {})
|
||||
assert.deepEqual(ohne.problems, [])
|
||||
assert.doesNotMatch(ohne.text, /Sendungsnummer|Versanddienst/)
|
||||
assert.match(ohne.text, /versendet/)
|
||||
})
|
||||
|
||||
test('G11: Abholort und Hinweis', () => {
|
||||
const mail = g('G11', { abholort: 'Werkstatt, Hauptstr. 1', hinweis: 'Di–Fr 10–16 Uhr' })
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X ist abholbereit')
|
||||
assert.match(mail.text, /Abholort: Werkstatt, Hauptstr\. 1/)
|
||||
assert.match(mail.text, /Hinweis: Di–Fr 10–16 Uhr/)
|
||||
})
|
||||
|
||||
test('G12: Eingang mit Datum und Uhrzeit, Erklärung nur im Block nachricht, Händler meldet sich', () => {
|
||||
const werte = { name: 'Erika <E>', bestellnummer: '26-ZZZZZZ', eingang: '05.10.2026, 14:03 Uhr', nachricht: 'Bitte stornieren.\nDanke' }
|
||||
const mit = g('G12', werte, { nachricht: true })
|
||||
assert.deepEqual(mit.problems, [])
|
||||
assert.equal(mit.subject, 'Eingangsbestätigung Ihres Widerrufs bei Töpferei <Muster>')
|
||||
assert.match(mit.text, /Guten Tag Erika <E>,/)
|
||||
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||||
assert.match(mit.text, /Bestellnummer: 26-ZZZZZZ/)
|
||||
assert.match(mit.html, /Bitte stornieren\.<br>Danke/)
|
||||
assert.match(mit.text, /meldet sich bei Ihnen/)
|
||||
const ohne = g('G12', werte, {})
|
||||
assert.doesNotMatch(ohne.text, /Bitte stornieren|Ihre Erklärung:/)
|
||||
})
|
||||
|
||||
test('G13: Betrag, Grund nur im Block, Gutschrift über die ursprüngliche Zahlungsart', () => {
|
||||
const mit = g('G13', { betrag: '12,50 €', grund: 'Artikel beschädigt' }, { grund: true })
|
||||
assert.deepEqual(mit.problems, [])
|
||||
assert.equal(mit.subject, 'Erstattung zu Ihrer Bestellung 26-K7QM2X')
|
||||
assert.match(mit.text, /Erstatteter Betrag: 12,50 €/)
|
||||
assert.match(mit.text, /Grund: Artikel beschädigt/)
|
||||
assert.match(mit.text, /Die Gutschrift erfolgt über die ursprüngliche Zahlungsart\./)
|
||||
assert.doesNotMatch(g('G13', { betrag: '12,50 €', grund: 'x' }, {}).text, /Grund:/)
|
||||
})
|
||||
|
||||
test('G14: nichts berechnet, kein Betrag', () => {
|
||||
const mail = g('G14', {})
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X wurde nicht abgeschlossen')
|
||||
assert.match(mail.text, /wurde nicht abgeschlossen; es wurde nichts berechnet\./)
|
||||
})
|
||||
|
||||
const K25_WERTE = { kunde_name: 'Erika', shopname: 'Töpferei', bestellnummer: '26-K7QM2X', gesamt: '41,40 €', positionen: '2 × Tasse – 24,00 €\n1 × Teller – 12,50 €', lieferart_text: 'Versand nach Deutschland', kaeufer_name: 'Max <M>', portal_url: 'https://portal.example/dashboard.html#webshop?projekt=p1' }
|
||||
|
||||
test('K25: Betreff, Platzhalter, Blöcke; Bestand-Warnung und Lastschrift-Hinweis nur im Block', () => {
|
||||
const tpl = loadTemplate('K25')
|
||||
assert.equal(tpl.file, 'K25 Neue Bestellung.html')
|
||||
assert.equal(tpl.subject, 'Neue Bestellung {{bestellnummer}} in Deinem Webshop')
|
||||
assert.deepEqual([...tpl.placeholders].sort(), Object.keys(K25_WERTE).sort())
|
||||
assert.deepEqual(tpl.blocks, ['ueberverkauft', 'sepa'])
|
||||
const normal = renderMail('K25', K25_WERTE, {})
|
||||
assert.deepEqual(normal.problems, [])
|
||||
assert.equal(normal.subject, 'Neue Bestellung 26-K7QM2X in Deinem Webshop')
|
||||
assert.match(normal.text, /Hallo Erika/)
|
||||
assert.match(normal.text, /Käufer: Max <M>/)
|
||||
assert.match(normal.html, /2 × Tasse – 24,00 €<br>1 × Teller – 12,50 €/)
|
||||
assert.match(normal.html, /href="https:\/\/portal\.example\/dashboard\.html#webshop\?projekt=p1"/)
|
||||
assert.doesNotMatch(normal.text, /Achtung|Lastschrift/)
|
||||
const voll = renderMail('K25', K25_WERTE, { ueberverkauft: true, sepa: true })
|
||||
assert.match(voll.text, /Achtung: Bestand reichte nicht/)
|
||||
assert.match(voll.text, /Zahlung per Lastschrift/)
|
||||
assert.doesNotMatch(renderMail('K25', K25_WERTE, { sepa: true }).text, /Achtung/)
|
||||
assert.doesNotMatch(renderMail('K25', K25_WERTE, { ueberverkauft: true }).text, /Lastschrift/)
|
||||
})
|
||||
|
||||
const K26_WERTE = { kunde_name: 'Erika', shopname: 'Töpferei', bestellnummer: '26-K7QM2X', kaeufer_name: 'Max <M>', kaeufer_email: 'max@example.com', eingang: '05.10.2026, 14:03 Uhr', nachricht: 'Ich widerrufe.\nBitte melden', portal_url: 'https://portal.example/dashboard.html#webshop?projekt=p1' }
|
||||
|
||||
test('K26: Betreff, Platzhalter, Erklärung nur im Block, Hinweis auf die 14 Tage', () => {
|
||||
const tpl = loadTemplate('K26')
|
||||
assert.equal(tpl.file, 'K26 Widerruf im Webshop.html')
|
||||
assert.equal(tpl.subject, 'Widerruf in Deinem Webshop eingegangen')
|
||||
assert.deepEqual([...tpl.placeholders].sort(), Object.keys(K26_WERTE).sort())
|
||||
assert.deepEqual(tpl.blocks, ['nachricht'])
|
||||
const mit = renderMail('K26', K26_WERTE, { nachricht: true })
|
||||
assert.deepEqual(mit.problems, [])
|
||||
assert.match(mit.text, /Hallo Erika/)
|
||||
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||||
assert.match(mit.text, /E-Mail: max@example\.com/)
|
||||
assert.match(mit.html, /Ich widerrufe\.<br>Bitte melden/)
|
||||
assert.match(mit.text, /innerhalb von 14 Tagen/)
|
||||
assert.doesNotMatch(renderMail('K26', K26_WERTE, {}).text, /Ich widerrufe|Erklärung:/)
|
||||
})
|
||||
@@ -0,0 +1,360 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||
|
||||
// Webshop E4: Mails des Shops (services/shop/mails.js). Käufermails laufen durch die Warteschlange des Terminplaners
|
||||
// (termineOutbox), Händlermails gehen direkt an das Kundenkonto. Mails landen in der Senke, nichts verlässt den Prozess.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: { termineOutbox: schemaKeys('termineOutbox') },
|
||||
datetimes: TERMINE_DATETIMES,
|
||||
defaultLimit: 25,
|
||||
})
|
||||
after(() => aw.close())
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||||
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
|
||||
const { gewaehrleistungsHinweis } = await import('../server/services/shop/rechtstexte.js')
|
||||
const M = await import('../server/services/shop/mails.js')
|
||||
const { anKaeufer, anHaendler, kaeuferWerte, haendlerK25, haendlerK26 } = M
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin
|
||||
const SITE_KEY = 'abcdefghijklmnop'
|
||||
const ABSENDER_VORHER = config.smtp.from
|
||||
const BASIS_VORHER = config.publicBaseUrl
|
||||
|
||||
const einstellungen = (over = {}) => ({
|
||||
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
|
||||
shopname: 'Töpferei Muster',
|
||||
anbieter: { name: 'Erika Muster', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: 'DE', email: 'info@toepferei.example', telefon: '', ustId: '' },
|
||||
lieferzeit: '2–4 Werktage',
|
||||
akzent: '#aa3300',
|
||||
abholung: { aktiv: true, adresse: 'Werkstatt, Hauptstr. 1, 67722 Winnweiler', hinweis: 'Di–Fr 10–16 Uhr' },
|
||||
...over,
|
||||
})
|
||||
const texte = (over = {}) => ({
|
||||
agb: 'AGB Zeile 1\nAGB Zeile 2',
|
||||
widerrufsbelehrung: 'Belehrung Zeile 1\nZeile 2',
|
||||
widerrufsformular: 'An Töpferei Muster\nHiermit widerrufe ich',
|
||||
impressumUrl: 'https://toepferei.example/impressum',
|
||||
datenschutzUrl: 'https://toepferei.example/datenschutz',
|
||||
...over,
|
||||
})
|
||||
const ctxMit = (e = einstellungen(), t = texte()) => ({
|
||||
doc: { $id: 'f_shop01', customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: SITE_KEY },
|
||||
shop: {
|
||||
id: 'f_shop01',
|
||||
customerId: 'c1',
|
||||
projectId: 'p1',
|
||||
siteKey: SITE_KEY,
|
||||
einstellungen: e,
|
||||
rechtstexte: { entwurf: texte(), bestaetigt: { texte: t, hash: 'h1', at: JETZT.toISOString(), von: 'c1' } },
|
||||
},
|
||||
})
|
||||
|
||||
const bestellung = (over = {}, snap = {}) => ({
|
||||
$id: 'o_bestellung01',
|
||||
bestellnummer: '26-K7QM2X',
|
||||
kaeuferEmail: 'max@example.com',
|
||||
bezahltAt: JETZT.toISOString(),
|
||||
erstelltAt: new Date(JETZT.getTime() - 600000).toISOString(),
|
||||
summeCent: 4140,
|
||||
bestellung: JSON.stringify({
|
||||
positionen: [
|
||||
{ name: 'Tasse', varianteText: 'blau', menge: 2, summeCent: 2400, widerruf: 'gesetzlich' },
|
||||
{ name: 'Teller', varianteText: '', menge: 1, summeCent: 1250, summeText: '12,50 €', widerruf: 'individuell' },
|
||||
],
|
||||
summen: { zwischensummeCent: 3650, versandCent: 490, gesamtCent: 4140, texte: { zwischensumme: '36,50 €', versand: '4,90 €', gesamt: '41,40 €', steuerHinweis: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.' } },
|
||||
lieferart: 'versand',
|
||||
land: 'DE',
|
||||
kaeufer: { name: 'Max Mustermann', email: 'max@example.com' },
|
||||
lieferadresse: { name: 'Max Mustermann', line1: 'Musterweg 3', postal_code: '12345', city: 'Musterstadt', country: 'DE' },
|
||||
zahlungsart: 'card',
|
||||
...snap,
|
||||
}),
|
||||
...over,
|
||||
})
|
||||
|
||||
let sink
|
||||
let log
|
||||
beforeEach(async () => {
|
||||
for (const id of aw.docs('termineOutbox').keys()) await deleteDocument(config.collections.termineOutbox, id)
|
||||
for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id)
|
||||
resetOutboxForTests()
|
||||
config.smtp.from = 'noreply@webklar.com'
|
||||
config.publicBaseUrl = 'https://project.webklar.com'
|
||||
sink = mailSink()
|
||||
setMailTransportForTests(sink)
|
||||
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
|
||||
aw.seed('customers', { $id: 'c1', name: 'Erika Muster', email: 'erika@toepferei.example' })
|
||||
})
|
||||
afterEach(async () => {
|
||||
await warteAufVersand()
|
||||
mock.restoreAll()
|
||||
config.smtp.from = ABSENDER_VORHER
|
||||
config.publicBaseUrl = BASIS_VORHER
|
||||
})
|
||||
|
||||
const eintraege = () => [...aw.docs('termineOutbox').values()]
|
||||
const warnLog = () => log.warn.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' '))
|
||||
|
||||
// ---------------------------------------------------------------- Werte
|
||||
|
||||
test('kaeuferWerte: Kopf, Fuß, Positionen, Summen, Lieferadresse und Rechtstexte aus Schnappschuss und Shop', () => {
|
||||
const w = kaeuferWerte(ctxMit(), bestellung(), JETZT)
|
||||
// euro() setzt ein geschütztes Leerzeichen vor das €-Zeichen
|
||||
for (const [schluessel, wert] of Object.entries(w)) w[schluessel] = wert.replaceAll('\u00a0', ' ')
|
||||
assert.equal(w.farbe, '#aa3300')
|
||||
assert.equal(w.betrieb_name, 'Töpferei Muster')
|
||||
assert.equal(w.anbieter_zeile, 'Erika Muster, Hauptstr. 1, 67722 Winnweiler, info@toepferei.example')
|
||||
assert.equal(w.widerruf_url, `https://project.webklar.com/shop/${SITE_KEY}/widerruf`)
|
||||
assert.equal(w.impressum_url, 'https://toepferei.example/impressum')
|
||||
assert.equal(w.datenschutz_url, 'https://toepferei.example/datenschutz')
|
||||
assert.equal(w.bestellnummer, '26-K7QM2X')
|
||||
assert.equal(w.datum, '05.10.2026')
|
||||
assert.equal(w.positionen, '2 × Tasse (blau) – 24,00 €\n1 × Teller – 12,50 €')
|
||||
assert.deepEqual([w.zwischensumme, w.versand, w.gesamt], ['36,50 €', '4,90 €', '41,40 €'])
|
||||
assert.equal(w.steuer_hinweis, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')
|
||||
assert.equal(w.lieferart_text, 'Versand nach Deutschland')
|
||||
assert.equal(w.lieferadresse, 'Max Mustermann\nMusterweg 3\n12345 Musterstadt')
|
||||
assert.equal(w.abholort, 'Werkstatt, Hauptstr. 1, 67722 Winnweiler')
|
||||
assert.equal(w.lieferzeit, '2–4 Werktage')
|
||||
assert.equal(w.zahlungsart, 'Kreditkarte, Apple Pay, Google Pay')
|
||||
assert.equal(w.ausschluss_text, 'Teller: Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt')
|
||||
assert.equal(w.agb, 'AGB Zeile 1\nAGB Zeile 2')
|
||||
assert.equal(w.widerrufsbelehrung, 'Belehrung Zeile 1\nZeile 2')
|
||||
assert.equal(w.widerrufsformular, 'An Töpferei Muster\nHiermit widerrufe ich')
|
||||
assert.equal(w.gewaehrleistung, gewaehrleistungsHinweis())
|
||||
})
|
||||
|
||||
test('kaeuferWerte: Links fallen ohne Impressum-/Datenschutz-Adresse auf den Shop zurück, ungültige Farbe auf Schwarz, ohne Bestellung "–"', () => {
|
||||
const w = kaeuferWerte(ctxMit(einstellungen({ akzent: 'red;x' }), texte({ impressumUrl: '', datenschutzUrl: '' })), null, JETZT)
|
||||
assert.equal(w.farbe, '#111111')
|
||||
assert.equal(w.impressum_url, `https://project.webklar.com/shop/${SITE_KEY}`)
|
||||
assert.equal(w.datenschutz_url, `https://project.webklar.com/shop/${SITE_KEY}`)
|
||||
assert.equal(w.bestellnummer, '–')
|
||||
assert.equal(w.positionen, '–')
|
||||
assert.equal(w.lieferadresse, '–')
|
||||
assert.equal(w.datum, '05.10.2026', 'ohne Bestellung gilt das übergebene Datum')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Käufermails
|
||||
|
||||
test('G09: wird mit Shopname als Absender und Antwort an den Händler eingereiht, kein BCC, Rechtstexte stehen in der Mail', async () => {
|
||||
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
|
||||
assert.equal(erg.ok, true)
|
||||
assert.equal(erg.gekuerzt, false)
|
||||
await warteAufVersand()
|
||||
|
||||
const [doc] = eintraege()
|
||||
assert.equal(doc.art, 'G09')
|
||||
assert.equal(doc.featureId, 'f_shop01')
|
||||
assert.equal(doc.bookingId, 'o_bestellung01')
|
||||
assert.equal(doc.fromName, 'Töpferei Muster')
|
||||
assert.equal(doc.replyTo, 'info@toepferei.example')
|
||||
assert.equal(doc.status, 'gesendet')
|
||||
assert.equal(doc.an, '', 'nach dem Versand ist die Adresse gelöscht')
|
||||
|
||||
assert.equal(sink.sent.length, 1)
|
||||
const mail = sink.sent[0]
|
||||
assert.deepEqual(mail.to.map((t) => t.address), ['max@example.com'])
|
||||
assert.ok(!mail.bcc, 'kein BCC')
|
||||
assert.equal(mail.from.name, 'Töpferei Muster')
|
||||
assert.equal(mail.from.address, 'noreply@webklar.com')
|
||||
assert.equal(mail.replyTo[0].address, 'info@toepferei.example')
|
||||
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X bei Töpferei Muster')
|
||||
for (const teil of ['2 × Tasse (blau) – 24,00 €', '41,40 €', 'Max Mustermann', 'Musterweg 3', 'Vom Widerruf ausgeschlossen', 'AGB Zeile 2', 'Belehrung Zeile 1', 'Hiermit widerrufe ich', 'GESETZLICHE GEWÄHRLEISTUNG', 'Vertrag widerrufen']) {
|
||||
assert.ok(mail.text.includes(teil), teil)
|
||||
}
|
||||
assert.match(mail.html, new RegExp(`href="https://project\\.webklar\\.com/shop/${SITE_KEY}/widerruf"`))
|
||||
assert.doesNotMatch(mail.text, /Abholort/)
|
||||
assert.deepEqual(warnLog(), [], 'keine Platzhalter-Warnung der Vorlage')
|
||||
})
|
||||
|
||||
test('G09 Abholung: Abholort statt Lieferadresse', async () => {
|
||||
const erg = await anKaeufer('G09', ctxMit(), bestellung({}, { lieferart: 'abholung', lieferadresse: null, land: null, positionen: [{ name: 'Tasse', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }] }), {}, JETZT)
|
||||
assert.equal(erg.ok, true)
|
||||
await warteAufVersand()
|
||||
const text = sink.sent[0].text
|
||||
assert.match(text, /Lieferart: Abholung/)
|
||||
assert.match(text, /Abholort: Werkstatt, Hauptstr\. 1, 67722 Winnweiler/)
|
||||
assert.doesNotMatch(text, /Lieferadresse|Hinweis zum Widerrufsrecht/)
|
||||
assert.deepEqual(warnLog(), [])
|
||||
})
|
||||
|
||||
test('G09: zu lang für die Warteschlange ⇒ der AGB-Text wird durch einen Link auf die AGB ersetzt, Belehrung und Formular bleiben', async () => {
|
||||
const lang = 'Dies ist ein langer Satz der Geschäftsbedingungen.\n'.repeat(560) // etwa 28 000 Zeichen
|
||||
const t = texte({ agb: lang, widerrufsbelehrung: 'Belehrung Satz.\n'.repeat(1000), widerrufsformular: 'Formular Zeile\n'.repeat(300) })
|
||||
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
|
||||
assert.equal(erg.ok, true)
|
||||
assert.equal(erg.gekuerzt, true)
|
||||
const doc = eintraege()[0]
|
||||
assert.ok(doc.html.length <= 100000 && doc.text.length <= 50000, `${doc.html.length}/${doc.text.length}`)
|
||||
assert.match(doc.text, new RegExp(`Die vollständigen AGB finden Sie unter https://project\\.webklar\\.com/shop/${SITE_KEY}\\?ansicht=agb`))
|
||||
assert.doesNotMatch(doc.text, /langer Satz der Geschäftsbedingungen/)
|
||||
assert.match(doc.text, /Belehrung Satz\./)
|
||||
assert.match(doc.text, /Formular Zeile/)
|
||||
assert.match(doc.text, /GESETZLICHE GEWÄHRLEISTUNG/)
|
||||
})
|
||||
|
||||
test('G09: auch gekürzt zu lang ⇒ { ok: false } ohne Wurf und ohne Eintrag', async () => {
|
||||
const t = texte({ widerrufsbelehrung: 'x'.repeat(60000) })
|
||||
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
|
||||
assert.deepEqual(erg, { ok: false, fehler: 'Die E-Mail an den Käufer konnte nicht eingereiht werden.' })
|
||||
assert.equal(eintraege().length, 0)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
assert.doesNotMatch(warnLog().join('\n'), /max@example\.com/)
|
||||
})
|
||||
|
||||
test('G10: Versanddienst und Sendungsnummer (Block sendung); ohne beide kein Block', async () => {
|
||||
assert.equal((await anKaeufer('G10', ctxMit(), bestellung(), { versanddienst: 'DHL', sendungsnummer: '00340434' }, JETZT)).ok, true)
|
||||
assert.equal((await anKaeufer('G10', ctxMit(), bestellung({ $id: 'o_zwei' }), {}, JETZT)).ok, true)
|
||||
await warteAufVersand()
|
||||
const [mit, ohne] = sink.sent
|
||||
assert.equal(mit.subject, 'Ihre Bestellung 26-K7QM2X wurde versendet')
|
||||
assert.match(mit.text, /Versanddienst: DHL/)
|
||||
assert.match(mit.text, /Sendungsnummer: 00340434/)
|
||||
assert.doesNotMatch(ohne.text, /Sendungsnummer/)
|
||||
})
|
||||
|
||||
test('G11: Abholort und Hinweis des Händlers, ersatzweise ein Standardhinweis', async () => {
|
||||
await anKaeufer('G11', ctxMit(), bestellung(), {}, JETZT)
|
||||
const ohneHinweis = einstellungen({ abholung: { aktiv: true, adresse: 'Werkstatt', hinweis: '' } })
|
||||
await anKaeufer('G11', ctxMit(ohneHinweis), bestellung({ $id: 'o_zwei' }), {}, JETZT)
|
||||
await warteAufVersand()
|
||||
const texte11 = sink.sent.map((m) => m.text)
|
||||
assert.match(texte11[0], /Abholort: Werkstatt, Hauptstr\. 1/)
|
||||
assert.match(texte11[0], /Hinweis: Di–Fr 10–16 Uhr/)
|
||||
assert.match(texte11[1], /Hinweis: Bitte bringen Sie Ihre Bestellnummer mit\./)
|
||||
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X ist abholbereit')
|
||||
})
|
||||
|
||||
test('G12: ohne passende Bestellung, an die Adresse der Erklärung, mit Datum und Uhrzeit; Vorrang-Klasse', async () => {
|
||||
const erg = await anKaeufer('G12', ctxMit(), null, { an: 'kaeufer@example.com', bookingId: 'w_widerruf01', name: 'Kai Käufer', bestellnummer: '26-ZZZZZZ', eingang: JETZT, nachricht: 'Ich widerrufe.' }, JETZT)
|
||||
assert.equal(erg.ok, true)
|
||||
assert.equal(erg.eintrag.bookingId, 'w_widerruf01')
|
||||
await warteAufVersand()
|
||||
const mail = sink.sent[0]
|
||||
assert.deepEqual(mail.to.map((t) => t.address), ['kaeufer@example.com'])
|
||||
assert.equal(mail.subject, 'Eingangsbestätigung Ihres Widerrufs bei Töpferei Muster')
|
||||
assert.match(mail.text, /Guten Tag Kai Käufer,/)
|
||||
assert.match(mail.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||||
assert.match(mail.text, /Bestellnummer: 26-ZZZZZZ/)
|
||||
assert.match(mail.text, /Ihre Erklärung: Ich widerrufe\./)
|
||||
assert.deepEqual(warnLog(), [])
|
||||
})
|
||||
|
||||
test('G13: erstatteter Betrag in Euro, Grund nur wenn angegeben; ohne Betrag { ok: false }', async () => {
|
||||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung(), { betragCent: 1250, grund: 'Artikel beschädigt' }, JETZT)).ok, true)
|
||||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_zwei' }), { betragCent: 4140 }, JETZT)).ok, true)
|
||||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_drei' }), {}, JETZT)).ok, false)
|
||||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_vier' }), { betragCent: 12.5 }, JETZT)).ok, false)
|
||||
await warteAufVersand()
|
||||
assert.equal(sink.sent.length, 2)
|
||||
assert.match(sink.sent[0].text, /Erstatteter Betrag: 12,50 €/)
|
||||
assert.match(sink.sent[0].text, /Grund: Artikel beschädigt/)
|
||||
assert.match(sink.sent[1].text, /Erstatteter Betrag: 41,40 €/)
|
||||
assert.doesNotMatch(sink.sent[1].text, /Grund:/)
|
||||
assert.match(sink.sent[1].text, /Die Gutschrift erfolgt über die ursprüngliche Zahlungsart\./)
|
||||
})
|
||||
|
||||
test('G14: Zahlung fehlgeschlagen, nichts berechnet', async () => {
|
||||
assert.equal((await anKaeufer('G14', ctxMit(), bestellung(), {}, JETZT)).ok, true)
|
||||
await warteAufVersand()
|
||||
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X wurde nicht abgeschlossen')
|
||||
assert.match(sink.sent[0].text, /es wurde nichts berechnet/)
|
||||
})
|
||||
|
||||
test('anKaeufer wirft nie: unbekannte Art, fehlende Adresse und Speicherfehler ergeben { ok: false }', async () => {
|
||||
assert.equal((await anKaeufer('G01', ctxMit(), bestellung(), {}, JETZT)).ok, false)
|
||||
assert.equal((await anKaeufer('G09', ctxMit(), bestellung({ kaeuferEmail: '' }), {}, JETZT)).ok, false)
|
||||
assert.equal((await anKaeufer('G09', ctxMit(), bestellung(), {}, new Date('kaputt'))).ok, false)
|
||||
aw.failNext({ collection: 'termineOutbox', method: 'POST', status: 500, message: 'Nachbau kaputt' })
|
||||
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
|
||||
assert.equal(erg.ok, false)
|
||||
assert.equal(eintraege().length, 0)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Käufermails zählen als Gästemails: G09 und G12 mit Vorrang, die übrigen normal', async () => {
|
||||
const { mailKlasse } = await import('../server/services/termine/outbox.js')
|
||||
assert.deepEqual(['G09', 'G10', 'G11', 'G12', 'G13', 'G14'].map(mailKlasse), ['vorrang', 'normal', 'normal', 'vorrang', 'normal', 'normal'])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Händlermails
|
||||
|
||||
test('K25: geht an das Kundenkonto, ohne BCC ans Team, mit Käufer, Positionen und Portal-Link', async () => {
|
||||
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
|
||||
assert.deepEqual(blocks, { ueberverkauft: false, sepa: false })
|
||||
const erg = await anHaendler('K25', ctxMit(), values, blocks)
|
||||
assert.deepEqual(erg, { ok: true })
|
||||
assert.equal(sink.sent.length, 1)
|
||||
const mail = sink.sent[0]
|
||||
assert.deepEqual(mail.to.map((t) => t.address), ['erika@toepferei.example'])
|
||||
assert.ok(!mail.bcc, 'kein BCC ans Team')
|
||||
assert.equal(mail.subject, 'Neue Bestellung 26-K7QM2X in Deinem Webshop')
|
||||
assert.match(mail.text, /Hallo Erika Muster/)
|
||||
assert.match(mail.text, /Töpferei Muster/)
|
||||
assert.match(mail.text, /Käufer: Max Mustermann/)
|
||||
assert.match(mail.text, /Gesamt: 41,40 €/)
|
||||
assert.match(mail.html.replaceAll('\u00a0', ' '), /2 × Tasse \(blau\) – 24,00 €<br>1 × Teller – 12,50 €/)
|
||||
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/dashboard\.html#webshop\?projekt=p1"/)
|
||||
assert.doesNotMatch(mail.text, /Achtung|Lastschrift/)
|
||||
assert.deepEqual(warnLog(), [])
|
||||
})
|
||||
|
||||
test('K25: Blöcke ueberverkauft und sepa', async () => {
|
||||
const { values, blocks } = haendlerK25(ctxMit(), bestellung({}, { zahlungsart: 'sepa_debit' }), { ueberverkauft: true })
|
||||
assert.deepEqual(blocks, { ueberverkauft: true, sepa: true })
|
||||
assert.equal((await anHaendler('K25', ctxMit(), values, blocks)).ok, true)
|
||||
assert.match(sink.sent[0].text, /Achtung: Bestand reichte nicht/)
|
||||
assert.match(sink.sent[0].text, /Zahlung per Lastschrift/)
|
||||
})
|
||||
|
||||
test('K26: Widerruf mit Eingang in Berliner Zeit und Erklärung, ohne BCC; ohne Erklärung kein Block', async () => {
|
||||
const widerruf = { name: 'Kai <K>', email: 'kai@example.com', bestellnummer: '26-ZZZZZZ', nachricht: 'Ich widerrufe.', eingangAt: JETZT.toISOString() }
|
||||
const { values, blocks } = haendlerK26(ctxMit(), widerruf)
|
||||
assert.equal(values.eingang, '05.10.2026, 14:03 Uhr')
|
||||
assert.deepEqual(blocks, { nachricht: true })
|
||||
assert.equal((await anHaendler('K26', ctxMit(), values, blocks)).ok, true)
|
||||
const ohne = haendlerK26(ctxMit(), { ...widerruf, nachricht: ' ' })
|
||||
assert.deepEqual(ohne.blocks, { nachricht: false })
|
||||
assert.equal((await anHaendler('K26', ctxMit(), ohne.values, ohne.blocks)).ok, true)
|
||||
const [mit, kurz] = sink.sent
|
||||
assert.ok(!mit.bcc)
|
||||
assert.equal(mit.subject, 'Widerruf in Deinem Webshop eingegangen')
|
||||
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||||
assert.match(mit.text, /Käufer: Kai <K>/)
|
||||
assert.match(mit.text, /E-Mail: kai@example\.com/)
|
||||
assert.match(mit.text, /Erklärung: Ich widerrufe\./)
|
||||
assert.match(mit.html, /Kai <K>/)
|
||||
assert.doesNotMatch(kurz.text, /Erklärung:/)
|
||||
})
|
||||
|
||||
test('anHaendler wirft nie: Kunde fehlt oder ohne E-Mail, Versandfehler ⇒ { ok: false } und Warnung ohne Adresse', async () => {
|
||||
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
|
||||
const fremd = ctxMit()
|
||||
fremd.shop.customerId = 'c_gibtsnicht'
|
||||
assert.equal((await anHaendler('K25', fremd, values, blocks)).ok, false)
|
||||
aw.seed('customers', { $id: 'c_ohne', name: 'Ohne', email: '' })
|
||||
const ohneMail = ctxMit()
|
||||
ohneMail.shop.customerId = 'c_ohne'
|
||||
assert.equal((await anHaendler('K25', ohneMail, values, blocks)).ok, false)
|
||||
setMailTransportForTests({
|
||||
async sendMail() {
|
||||
throw new Error('SMTP erika@toepferei.example kaputt')
|
||||
},
|
||||
})
|
||||
assert.deepEqual(await anHaendler('K25', ctxMit(), values, blocks), { ok: false, fehler: 'Die E-Mail an den Händler konnte nicht gesendet werden.' })
|
||||
assert.equal(sink.sent.length, 0)
|
||||
assert.doesNotMatch(warnLog().join('\n'), /erika@toepferei\.example/, 'keine Adresse im Log')
|
||||
assert.ok(warnLog().some((zeile) => /K25 nicht gesendet/.test(zeile)))
|
||||
})
|
||||
@@ -3016,3 +3016,90 @@ test('server/index.js: die drei Jobs laufen alle 60 s, alle 5 min und täglich (
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Webshop: die Mail-Warteschlange gehört auch dem Shop (E4)
|
||||
|
||||
const SHOP_MODUS_VORHER = config.shop.mode
|
||||
afterEach(() => {
|
||||
config.shop.mode = SHOP_MODUS_VORHER
|
||||
})
|
||||
|
||||
test('Webshop aktiv, Terminplaner off: der Minutenjob sendet die Warteschlange, der Verfall und alles andere bleibt aus', async () => {
|
||||
await richteFunktionEin()
|
||||
await legeBuchung('b_abgelaufen', { status: 'gehalten', heldUntil: iso(plus(JETZT, -5 * MIN)) })
|
||||
legeEintrag('o_faellig', { art: 'G09' })
|
||||
legeEintrag('o_spaeter', { art: 'G09', naechsterVersuch: iso(plus(JETZT, 10 * MIN)) })
|
||||
config.termine.mode = 'off'
|
||||
config.shop.mode = 'pilot'
|
||||
|
||||
assert.deepEqual(await minutenJob(JETZT), { eingeschraenkt: true, verfallen: 0, faellig: 1, gesendet: 1, fehler: 0 })
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.equal(aw.docs('termineOutbox').get('o_faellig').status, 'gesendet')
|
||||
assert.equal(aw.docs('termineOutbox').get('o_spaeter').status, 'wartet')
|
||||
assert.equal(status('b_abgelaufen'), 'gehalten', 'der Verfall der Haltungen bleibt aus')
|
||||
|
||||
// Shop aus, Terminplaner off: wie bisher nichts
|
||||
config.shop.mode = 'off'
|
||||
legeEintrag('o_nochmal', { art: 'G09' })
|
||||
assert.deepEqual(await minutenJob(JETZT), { aus: true })
|
||||
assert.equal(aw.docs('termineOutbox').get('o_nochmal').status, 'wartet')
|
||||
})
|
||||
|
||||
test('Webshop aktiv, Terminplaner off und ohne bezahlte Terminplaner: der Tagesjob beendet festgefahrene Mails und räumt die Warteschlange auf, sonst nichts', async () => {
|
||||
await richteFunktionEin() // aktiv, nicht bezahlt
|
||||
await legeLoeschFall('b_verfallen', 'verfallen', 1)
|
||||
legeEintrag('o_festgefahren', { art: 'G09', createdAt: iso(plus(JETZT, -3 * TAG)) })
|
||||
legeEintrag('o_gesendet', { art: 'G09', status: 'gesendet', an: '', html: '', text: '', naechsterVersuch: iso(plus(JETZT, -MIN)) })
|
||||
legeEintrag('o_neu', { art: 'G09' })
|
||||
config.termine.mode = 'off'
|
||||
config.shop.mode = 'on'
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg, {
|
||||
eingeschraenkt: true,
|
||||
aboAbgleich: { geprueft: 0, abgeschaltet: 0, wiederEin: 0, mails: 0, ausgenommen: 0, fehler: 0 },
|
||||
abgeschaltet: { funktionen: 0, buchungen: 0 },
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 },
|
||||
geloescht: 0,
|
||||
aktiveBetriebe: null,
|
||||
festgefahren: 1,
|
||||
aufgeraeumt: 1,
|
||||
fehler: 0,
|
||||
})
|
||||
assert.equal(aw.docs('termineOutbox').get('o_festgefahren').status, 'fehler')
|
||||
assert.equal(aw.docs('termineOutbox').get('o_gesendet'), undefined, 'gesendeter Eintrag gelöscht')
|
||||
assert.equal(aw.docs('termineOutbox').get('o_neu').status, 'wartet')
|
||||
assert.ok(buchung('b_verfallen'), 'keine Löschfristen der Buchungen')
|
||||
assert.equal(
|
||||
tagesbericht(erg),
|
||||
'[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung sowie Mail-Warteschlange): 0 Buchungen gelöscht (abgeschaltete Funktionen 0), 0 Funktionen bereinigt, 1 Outbox, 1 Mails nicht zugestellt, Abo-Abgleich 0 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 0 ausgenommen, 0 Fehler',
|
||||
)
|
||||
})
|
||||
|
||||
test('Webshop aktiv, Terminplaner off, bezahlter Terminplaner: Abo-Abgleich und Löschung laufen wie bisher, dazu die Warteschlange', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
wartung('active')
|
||||
legeEintrag('o_festgefahren', { art: 'G09', createdAt: iso(plus(JETZT, -3 * TAG)) })
|
||||
config.termine.mode = 'off'
|
||||
config.shop.mode = 'pilot'
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.equal(erg.aboAbgleich.geprueft, 1)
|
||||
assert.equal(erg.festgefahren, 1)
|
||||
assert.equal(erg.aufgeraeumt, 0)
|
||||
assert.equal(aw.docs('termineOutbox').get('o_festgefahren').status, 'fehler')
|
||||
})
|
||||
|
||||
test('Webshop aktiv, Terminplaner off: schlägt die Guard-Abfrage fehl, läuft nur die Warteschlange (Warnung ohne Kennungen)', async () => {
|
||||
legeEintrag('o_festgefahren', { art: 'G09', createdAt: iso(plus(JETZT, -3 * TAG)) })
|
||||
config.termine.mode = 'off'
|
||||
config.shop.mode = 'on'
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt' })
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.equal(erg.festgefahren, 1)
|
||||
assert.equal(warnLog().filter((zeile) => /taeglicherJob/.test(zeile) && /Nachbau kaputt/.test(zeile)).length, 1)
|
||||
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\bp_|\bc_|@/)
|
||||
})
|
||||
@@ -308,10 +308,10 @@ test('Fehlversuche verbrauchen kein Stundenbudget (auch nicht Anmelde- oder Verb
|
||||
/** Eintrag einer bestimmten Funktion und Art (Vorlage bleibt G01: die Warteschlange prueft Art und Vorlage nicht gegeneinander). */
|
||||
const von = (featureId, art, nummer, extra = {}) => eintrag({ featureId, art, bookingId: `b_${featureId}_${art}_${nummer}`, ...extra })
|
||||
|
||||
test('mailKlasse: G01/G02/G08 vorrang, G05/K20/K21 nachrangig, alle anderen normal', () => {
|
||||
assert.deepEqual(MAIL_VORRANG, ['G01', 'G02', 'G08'])
|
||||
test('mailKlasse: G01/G02/G08 (Webshop: G09/G12) vorrang, G05/K20/K21 nachrangig, alle anderen normal', () => {
|
||||
assert.deepEqual(MAIL_VORRANG, ['G01', 'G02', 'G08', 'G09', 'G12'])
|
||||
assert.deepEqual(MAIL_NACHRANGIG, ['G05', 'K20', 'K21'])
|
||||
const erwartet = { G01: 'vorrang', G02: 'vorrang', G03: 'normal', G04: 'normal', G05: 'nachrangig', G06: 'normal', G07: 'normal', G08: 'vorrang', K20: 'nachrangig', K21: 'nachrangig' }
|
||||
const erwartet = { G01: 'vorrang', G02: 'vorrang', G03: 'normal', G04: 'normal', G05: 'nachrangig', G06: 'normal', G07: 'normal', G08: 'vorrang', G09: 'vorrang', G10: 'normal', G11: 'normal', G12: 'vorrang', G13: 'normal', G14: 'normal', K20: 'nachrangig', K21: 'nachrangig' }
|
||||
assert.deepEqual(Object.fromEntries(Object.keys(erwartet).map((art) => [art, mailKlasse(art)])), erwartet)
|
||||
assert.equal(mailKlasse('unbekannt'), 'normal')
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user