Mails K22, K23 und Hinweis auf das Ende der Funktionen in K02/K03

Terminplaner TP2 E3. K22 "Terminplaner beendet" (Blöcke abo_ende, hosting,
vor_einrichtung, daten) und K23 "Terminplaner läuft wieder" im Layout von K19,
Du-Form, Texte als Entwurf für die Mail-Überarbeitung. K02 und K03 bekommen den
Block funktionen (in K03 innerhalb von abo); routes/abo.js setzt ihn nur beim
Wartungs-Abo und nie bei der Speicher-Kündigung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 14:09:05 +00:00
1 parent eae6eee03c
commit 973135d2de
7 files changed
+282 -8

No files matched your search

@@ -8,8 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail am Abo) · BCC intern
Platzhalter: {{name}}, {{produkt}}, {{eingang_zeitpunkt}}, {{vertragsende}}, {{vertragsende_paragraph}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung
produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent.
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung, funktionen
produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent; funktionen = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (nur Wartungs-Abo; Sie-Form wie die Vorlage).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -39,6 +39,9 @@ a{color:#111111;}
<tr><td class="px" style="padding:32px 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:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="mut" style="font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#6b6964;">Ihr Abo endet zum</div><div class="ink" style="font-size:30px;line-height:38px;font-weight:bold;letter-spacing:-0.5px;color:#111111;padding-top:4px;">{{vertragsende}}</div></td></tr></table></td></tr>
<tr><td class="px mut" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Bis dahin bleiben alle Leistungen verfügbar, danach erfolgen keine weiteren Abbuchungen. Gebuchter E-Mail-Zusatzspeicher endet zusammen mit dem Abo.<!-- WENN wartung --> Nicht genutzte Zeit aus dem Stundenkontingent entfällt mit Vertragsende.<!-- ENDE wartung --></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;">Ihre Daten, den Domain-AuthCode und die Inhalte Ihrer Postfächer stellen wir Ihnen auf Wunsch gemäß {{vertragsende_paragraph}} der Vertragsbedingungen bereit.</td></tr>
<!-- WENN funktionen -->
<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;">Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.</td></tr>
<!-- ENDE funktionen -->
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf6e7;border:1px solid #f0dcb0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Sie haben das nicht veranlasst?</div><div class="mut" style="color:#4a4843;">Dann melden Sie sich bitte umgehend bei uns: <a class="mut" href="mailto:support@webklar.com" style="color:#4a4843;">support@webklar.com</a> oder 0176 23726355.</div></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>Ihr WEBklar-Team</strong></td></tr>
</table>
@@ -8,8 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: E-Mail aus dem Formular · BCC intern
Platzhalter: {{name}}, {{produkt}}, {{kuendigungsart}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}, {{vertrag}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{vertragsende_paragraph}}, {{pakete}}, {{pakete_neu}}, {{pakete_angabe}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1).
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung, funktionen
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1); funktionen (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (Sie-Form wie die Vorlage).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -79,6 +79,9 @@ a{color:#111111;}
<!-- WENN abo -->
<tr><td class="px mut" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;"><!-- WENN wartung -->Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit Vertragsende. <!-- ENDE wartung -->Ihre Daten, den Domain-AuthCode und die Inhalte Ihrer Postfächer stellen wir Ihnen auf Wunsch gemäß {{vertragsende_paragraph}} der Vertragsbedingungen bereit. Gebuchter E-Mail-Zusatzspeicher endet zusammen mit dem Abo.</td></tr>
<!-- WENN funktionen -->
<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;">Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.</td></tr>
<!-- ENDE funktionen -->
<!-- ENDE abo -->
<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>Ihr WEBklar-Team</strong></td></tr>
</table>
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K22 · Terminplaner beendet
Betreff: Dein Terminplaner wurde beendet – {{projekt}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{enddatum}}, {{loeschdatum}}, {{preis}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): abo_ende, hosting, vor_einrichtung, daten
Genau einer der Gründe abo_ende (Wartungs-Abo beendet), hosting (Hosting-Abo reicht nicht) oder vor_einrichtung (Abo endete vor der Einrichtung, die Einrichtung wird voll erstattet) ist an; daten (Kasten „Was jetzt gilt“) gehört nur zu den ersten beiden.
enddatum = Tag des Abo-Endes; loeschdatum = Tag, ab dem wir die Termindaten löschen (30 Tage nach dem Abschalten); preis = Preis der Einrichtung, z. B. „149 €“.
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>Dein Terminplaner wurde beendet – {{projekt}}</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;">Dein Terminplaner wurde beendet.&#8199;&#847;&#8199;&#847;&#8199;&#847;</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;">Beendet</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;">Terminplaner</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;">Dein Terminplaner wurde beendet</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><!-- WENN abo_ende -->Dein Terminplaner wurde beendet, weil Dein Wartungs-Abo am {{enddatum}} beendet wurde. Der Terminplaner gehört zum Wartungs-Abo und endet mit ihm.<!-- ENDE abo_ende --><!-- WENN hosting -->Dein Terminplaner wurde beendet, weil Dein Abo ein Hosting-Abo ist. Der Terminplaner gehört nur zum Wartungs-Abo; ein Hosting-Abo reicht dafür nicht.<!-- ENDE hosting --><!-- WENN vor_einrichtung -->Dein Wartungs-Abo wurde am {{enddatum}} beendet, noch vor der Einrichtung Deines Terminplaners. Deshalb richten wir ihn nicht mehr ein und erstatten Dir die Einrichtung ({{preis}}) vollständig.<!-- ENDE vor_einrichtung --></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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Terminplaner</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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Beendet am</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{enddatum}}</td></tr></table></td></tr>
<!-- WENN daten -->
<tr><td class="px" style="padding:24px 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:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Bis {{loeschdatum}} kannst Du Deine Termine im Kundenportal ansehen und als CSV exportieren.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Gäste können ihre Termine weiter absagen. Bestehende Termine bleiben eingetragen, und die Erinnerungen an Deine Gäste laufen bis zur Löschung weiter. <strong>Bitte informiere Deine Gäste selbst.</strong></div><div class="mut" style="color:#4a4843;padding-top:8px;">Ab {{loeschdatum}} löschen wir die Termindaten, 30 Tage nach dem Abschalten.</div><div class="mut" style="color:#4a4843;padding-top:8px;"><strong>Rückkehr:</strong> Läuft bis dahin wieder ein Wartungs-Abo, schalten wir den Terminplaner automatisch und ohne Gebühr wieder ein. Nach der Löschung braucht es einen neuen Fragebogen und die Einrichtung für {{preis}} noch einmal.</div></td></tr></table></td></tr>
<!-- ENDE daten -->
<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;">Zum Kundenportal &rarr;</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,50 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K23 · Terminplaner läuft wieder
Betreff: Dein Terminplaner läuft wieder – {{projekt}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{portal_url}}
Geht nur an Kunden, die K22 bekommen haben, wenn ihr Wartungs-Abo innerhalb der Löschfrist zurückkehrt und wir den Terminplaner automatisch und ohne Gebühr wieder einschalten.
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>Dein Terminplaner läuft wieder – {{projekt}}</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;">Dein Terminplaner läuft wieder.&#8199;&#847;&#8199;&#847;&#8199;&#847;</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;">Wieder aktiv</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;">&#10003;&nbsp; Terminplaner</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;">Dein Terminplaner läuft wieder</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>Dein Wartungs-Abo läuft wieder. Deshalb haben wir Deinen Terminplaner automatisch und ohne Gebühr wieder eingeschaltet. Deine Termine und Einstellungen sind unverändert da, und Deine Gäste können wieder buchen.</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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Terminplaner</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{projekt}}</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;">Zum Kundenportal &rarr;</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>
+4 -1
View File
@@ -474,7 +474,8 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) {
vertragsende: fmtDate(endsAt),
vertragsende_paragraph: endParagraphOf(sub),
},
blocks: { wartung: isWartung(sub) },
// Hinweis auf nicht genutzte Arbeitszeit und auf das Ende von Website-Analyse und Terminplaner nur beim Wartungs-Abo
blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) },
})
await updateDocument(config.collections.subscriptions, sub.$id, {
cancelRequestedAt: receivedAt.toISOString(),
@@ -942,6 +943,8 @@ router.post('/public/cancel', async (req, res) => {
abo: !storage,
// Hinweis auf nicht genutzte Arbeitszeit nur im Wartungs-Abo (Block innerhalb von abo)
wartung: !storage && isWartung(sub),
// Website-Analyse und Terminplaner enden mit dem Wartungs-Abo (Block innerhalb von abo, nie bei der Speicher-Kündigung)
funktionen: !storage && isWartung(sub),
automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe',
sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe',
ausserordentlich: !storage && !effectiveAt && d.kind === 'ausserordentlich',
+116 -2
View File
@@ -6,9 +6,9 @@ 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; T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b)
// 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)
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 19), 'T20']
const EXPECTED = [...ids('G', 8), ...ids('K', 23), ...ids('T', 19), 'T20']
const GUEST = ids('G', 8)
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
@@ -114,6 +114,108 @@ test('T16, T19: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
}
})
// Terminplaner TP2 (E3): K22 „Terminplaner beendet“ je Grund eine Variante, K23 „läuft wieder“; Texte sind Entwurf für die Mail-Überarbeitung
const K22_WERTE = { kunde_name: 'Erika', projekt: 'Praxis Muster', enddatum: '03.10.2026', loeschdatum: '02.11.2026', preis: '149 €', portal_url: 'https://portal.example/#termine' }
const k22 = (blocks) => renderMail('K22', K22_WERTE, blocks)
test('K22, K23: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
const tpl22 = loadTemplate('K22')
assert.equal(tpl22.file, 'K22 Terminplaner beendet.html')
assert.equal(tpl22.subject, 'Dein Terminplaner wurde beendet – {{projekt}}')
assert.deepEqual(tpl22.placeholders, ['kunde_name', 'projekt', 'enddatum', 'loeschdatum', 'preis', 'portal_url'])
assert.deepEqual(tpl22.blocks, ['abo_ende', 'hosting', 'vor_einrichtung', 'daten'])
const tpl23 = loadTemplate('K23')
assert.equal(tpl23.file, 'K23 Terminplaner laeuft wieder.html')
assert.equal(tpl23.subject, 'Dein Terminplaner läuft wieder – {{projekt}}')
assert.deepEqual(tpl23.placeholders, ['kunde_name', 'projekt', 'portal_url'])
assert.deepEqual(tpl23.blocks, [])
assert.equal(k22({}).subject, 'Dein Terminplaner wurde beendet – Praxis Muster')
})
test('K22 abo_ende: nennt das beendete Wartungs-Abo, keine Erstattung', () => {
const mail = k22({ abo_ende: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Wartungs-Abo/)
assert.match(mail.text, /03\.10\.2026/, 'Enddatum')
assert.doesNotMatch(mail.text, /erstatten/)
assert.doesNotMatch(mail.text, /Hosting-Abo/, 'nicht die Hosting-Variante')
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/, 'der Kasten kommt nur mit dem Block daten')
})
test('K22 hosting: das Hosting-Abo reicht für den Terminplaner nicht', () => {
const mail = k22({ hosting: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Hosting-Abo/)
assert.match(mail.text, /Wartungs-Abo/, 'sagt, was der Terminplaner braucht')
assert.doesNotMatch(mail.text, /erstatten/)
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/)
})
test('K22 vor_einrichtung: Einrichtung wird voll erstattet, nennt den Preis, kein Löschkasten', () => {
const mail = k22({ vor_einrichtung: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /149 €/)
assert.match(mail.text, /erstatten/)
assert.match(mail.text, /vor der Einrichtung/)
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt|Rückkehr|löschen/, 'es gibt noch keine Termindaten, also keinen Löschkasten')
})
test('K22 daten: Kasten „Was jetzt gilt“ mit Löschdatum, Gästen und Rückkehr', () => {
const mail = k22({ daten: true })
assert.deepEqual(mail.problems, [])
const t = mail.text
assert.match(t, /Was jetzt gilt/)
assert.match(t, /Bis 02\.11\.2026 kannst Du Deine Termine im Kundenportal ansehen und als CSV exportieren/)
assert.match(t, /Gäste können ihre Termine weiter absagen/)
assert.match(t, /Bestehende Termine bleiben eingetragen, und die Erinnerungen an Deine Gäste laufen bis zur Löschung weiter/)
assert.match(t, /Bitte informiere Deine Gäste selbst/)
assert.match(t, /Ab 02\.11\.2026 löschen wir die Termindaten/)
assert.match(t, /30 Tage/, 'Löschfrist 30 Tage nach dem Abschalten')
assert.match(t, /Rückkehr: Läuft bis dahin wieder ein Wartungs-Abo, schalten wir den Terminplaner automatisch und ohne Gebühr wieder ein/)
assert.match(t, /Nach der Löschung braucht es einen neuen Fragebogen und die Einrichtung für 149 € noch einmal/)
assert.doesNotMatch(t, /erstatten/)
})
test('K22: Anrede, Projekt und Knopf ins Kundenportal stehen in jeder Variante', () => {
const mail = k22({ abo_ende: true, daten: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Hallo Erika,/)
assert.match(mail.text, /Praxis Muster/)
assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/)
assert.match(mail.html, /href="https:\/\/portal\.example\/#termine"/)
})
test('K23: nennt das Projekt und führt ins Kundenportal', () => {
const mail = renderMail('K23', { kunde_name: 'Erika', projekt: 'Praxis Muster', portal_url: 'https://portal.example/#termine' })
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Dein Terminplaner läuft wieder – Praxis Muster')
assert.match(mail.text, /Hallo Erika,/)
assert.match(mail.text, /Praxis Muster/)
assert.match(mail.text, /Wartungs-Abo/)
assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/)
assert.match(mail.html, /<a href="https:\/\/portal\.example\/#termine"/)
})
// Terminplaner TP2 (E3): K02/K03 sagen, dass die Funktionen mit dem Wartungs-Abo enden (Sie-Form wie der Rest der Vorlagen)
test('K02, K03: Block funktionen nennt Terminplaner und Löschfrist, nur mit Block (in K03 nur innerhalb von abo)', () => {
const k02 = { name: 'K', produkt: 'Wartungs-Abo', eingang_zeitpunkt: '01.10.2026, 10:00', vertragsende: '01.10.2027', vertragsende_paragraph: '§ 11' }
assert.ok(loadTemplate('K02').blocks.includes('funktionen'))
const an = renderMail('K02', k02, { wartung: true, funktionen: true })
assert.deepEqual(an.problems, [])
assert.match(an.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./)
assert.doesNotMatch(renderMail('K02', k02, { wartung: true }).text, /Terminplaner/)
assert.doesNotMatch(renderMail('K02', { ...k02, produkt: 'Hosting-Abo' }, {}).text, /Terminplaner|Wartungs-Abo/, 'Hosting-Abo: kein Hinweis')
const k03 = { ...sampleValues(loadTemplate('K03')), vertragsende_paragraph: '§ 11' }
assert.ok(loadTemplate('K03').blocks.includes('funktionen'))
const abo = renderMail('K03', k03, { abo: true, wartung: true, funktionen: true })
assert.deepEqual(abo.problems, [])
assert.match(abo.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./)
assert.doesNotMatch(renderMail('K03', k03, { abo: true, wartung: true }).text, /Terminplaner/)
// Speicher-Kündigung (abo aus): der Block steht innerhalb von abo und fällt mit ihm weg
assert.doesNotMatch(renderMail('K03', k03, { abo: false, funktionen: true }).text, /Terminplaner/)
})
test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden <br>', () => {
const mail = renderMail('T01', { kunde_name: 'Max <b>&"\'', nachricht: 'Zeile 1\nZeile <2>' })
assert.match(mail.html, /Max &lt;b&gt;&amp;&quot;&#39; hat im Kundenportal geschrieben/)
@@ -816,6 +918,18 @@ test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und na
assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
})
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest)
test('K22, K23: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
for (const id of ['K22', 'K23']) {
const tpl = loadTemplate(id)
assert.ok(!tpl.blocks.includes('sie') && !tpl.blocks.includes('du'), `${id}: Du-Form fest, keine Anredeblöcke`)
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
assert.doesNotMatch(mail.text, SIE_FORM, `${id}: Du-Form\n${mail.text}`)
assert.match(mail.text, /Hallo Wert-kunde_name,/, id)
assert.match(mail.text, DU_FORM, `${id}: spricht den Kunden mit Du an`)
}
})
test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fall', () => {
for (const id of ['K20', 'K21']) {
const tpl = loadTemplate(id)
+47 -1
View File
@@ -22,7 +22,7 @@ const aw = await startAppwriteMock({
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post } = await import('./support/testApp.js')
const { startTestApp, post, postWebhook } = await import('./support/testApp.js')
const { hostingDoc, consent, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
const { default: aboRoutes, resetPublicRateLimit, publicRateLimitState, purgeRateLimit } = await import('../server/routes/abo.js')
const { listContractEvents, recordContractEvent } = await import('../server/services/contractEvents.js')
@@ -147,6 +147,52 @@ test('K03/K04/T08/T09: Produkt des Kunden, Vertragsende nach § 11 der Fassung 4
assert.ok(k03u.text.includes('§ 11 der Vertragsbedingungen'), k03u.text)
})
// Terminplaner TP2 (E3): Website-Analyse und Terminplaner gehören zum Wartungs-Abo; K03 und K02 sagen, dass sie mit ihm enden
const FUNKTIONEN_SATZ = 'Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm.'
const WARTUNG_V4 = { productKey: 'wartung', termsVersion: config.abo.termsVersion }
const k02 = () => bySubject('Bestätigung Ihrer Kündigung')
test('K03: Hinweis auf das Ende der Funktionen nur bei der Kündigung eines Wartungs-Abos', async () => {
await resetSubH({ patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } })
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200)
assert.ok(k03().text.includes(FUNKTIONEN_SATZ), k03().text)
assert.ok(k03().text.includes('Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.'), k03().text)
// Hosting-Abo: weder Analyse noch Terminplaner
await resetSubH()
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200)
assert.doesNotMatch(k03().text, /Terminplaner/)
// nur der Zusatzspeicher eines Wartungs-Abos gekündigt: das Abo läuft weiter, kein Hinweis
await resetSubH({ units: 3, patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } })
assert.equal((await cancel({ contract: 'zusatzspeicher' })).status, 200)
assert.doesNotMatch(k03().text, /Terminplaner/)
// unbekannte E-Mail (kein Abo gefunden): kein Hinweis
await resetSubH({ patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } })
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich', email: 'niemand@example.invalid' })).status, 200)
assert.doesNotMatch(bySubject('Eingangsbestätigung Ihrer Kündigung').text, /Terminplaner/)
})
test('K02: Kündigung im Stripe-Kundenportal, Hinweis auf das Ende der Funktionen nur beim Wartungs-Abo', async () => {
const gekuendigt = (id) => ({
id,
object: 'event',
type: 'customer.subscription.updated',
created: Math.floor(Date.now() / 1000),
data: { object: { id: 'sub_h', object: 'subscription', cancel_at_period_end: true, metadata: {} }, previous_attributes: { cancel_at_period_end: false } },
})
for (const [productKey, mitHinweis] of [['wartung', true], ['hosting', false]]) {
const patch = productKey === 'wartung' ? WARTUNG_V4 : {}
await resetSubH({ patch: { ...patch, cancelAtPeriodEnd: true }, stripe: { productKey, cancel_at_period_end: true, metadata: productKey === 'wartung' ? { termsVersion: config.abo.termsVersion } : {} } })
const res = await postWebhook(gekuendigt(`evt_k02_${productKey}`))
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.ok(k02(), `${productKey}: K02`)
assert.equal(k02().text.includes(FUNKTIONEN_SATZ), mitHinweis, `${productKey}\n${k02().text}`)
assert.equal(/Terminplaner/.test(k02().text), mitHinweis, productKey)
}
})
test('2 von 3 Paketen ergibt Menge 1', async () => {
await resetSubH({ units: 3 })
const res = await cancel({ contract: 'zusatzspeicher', packages: '2' })