Kündigung und Widerruf: Paketzahl in jeder Bestätigung, klare Hinweise fürs Team, Tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-29 10:15:51 +00:00
1 parent e5cc2f3c82
commit 0d2254ca20
6 files changed
+275 -39

No files matched your search

@@ -7,8 +7,8 @@
Betreff: Eingangsbestätigung Ihrer Kündigung – WEBklar Wartungs-Abo
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: E-Mail aus dem Formular · BCC intern
Platzhalter: {{name}}, {{kuendigungsart}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}, {{vertrag}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{pakete}}, {{pakete_neu}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo
Platzhalter: {{name}}, {{kuendigungsart}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}, {{vertrag}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{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, speicher_angaben, speicher_kind_pruefung
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -39,6 +39,9 @@ a{color:#111111;}
<!-- WENN kundennummer -->
<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;">Kundennummer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kundennummer}}</td></tr>
<!-- ENDE kundennummer -->
<!-- WENN speicher_angaben -->
<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;">Pakete</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{pakete_angabe}}</td></tr>
<!-- ENDE speicher_angaben -->
<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;">Vertrag</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{vertrag}}</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;">Gewünschter Zeitpunkt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{wunschtermin}}</td></tr>
<!-- WENN grund -->
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Grund</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{grund}}</td></tr>
@@ -69,6 +72,9 @@ a{color:#111111;}
<!-- WENN pruefen -->
<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:#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;">Wir prüfen Ihre Kündigung</div><div class="mut" style="color:#4a4843;">Wir prüfen Ihre Kündigung und bestätigen Ihnen den Zeitpunkt, zu dem sie wirkt, gesondert per E-Mail.</div></td></tr></table></td></tr>
<!-- ENDE pruefen -->
<!-- WENN speicher_kind_pruefung -->
<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:#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;">Wir prüfen die Art Ihrer Kündigung</div><div class="mut" style="color:#4a4843;">Ihre {{kuendigungsart}} wird geprüft. Den Zusatzspeicher haben wir unabhängig davon wie oben zum Ende des Abrechnungsmonats verringert; ergibt die Prüfung ein früheres Ende, bestätigen wir es Ihnen gesondert.</div></td></tr></table></td></tr>
<!-- ENDE speicher_kind_pruefung -->
<!-- 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;">Ein bestehendes Stundenguthaben verfällt mit Vertragsende. Ihre Daten, den Domain-AuthCode und Ihr Postfach stellen wir Ihnen auf Wunsch gemäß § 9 der Vertragsbedingungen bereit.</td></tr>
@@ -7,8 +7,8 @@
Betreff: [Abo] Kündigung eingegangen: {{email}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{email}}, {{name}}, {{kundennummer}}, {{vertrag}}, {{kuendigungsart}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{stripe_abo_id}}, {{pakete}}, {{fehler}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, kein_speicher, wechsel_ausstehend, speicher_fehlgeschlagen, nachweis_fehlt
Platzhalter: {{email}}, {{name}}, {{kundennummer}}, {{vertrag}}, {{kuendigungsart}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{stripe_abo_id}}, {{pakete}}, {{pakete_angabe}}, {{fehler}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, kein_speicher, wechsel_ausstehend, speicher_fehlgeschlagen, nachweis_fehlt, speicher_angaben, speicher_kind_pruefung
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -38,6 +38,9 @@ a{color:#111111;}
<!-- WENN kundennummer -->
<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;">Kundennummer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kundennummer}}</td></tr>
<!-- ENDE kundennummer -->
<!-- WENN speicher_angaben -->
<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;">Pakete</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{pakete_angabe}}</td></tr>
<!-- ENDE speicher_angaben -->
<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;">Vertrag</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{vertrag}}</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;">Art</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kuendigungsart}}</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;">Gewünschter Zeitpunkt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{wunschtermin}}</td></tr>
<!-- WENN grund -->
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Grund</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{grund}}</td></tr>
@@ -69,14 +72,17 @@ a{color:#111111;}
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Abo {{stripe_abo_id}} gefunden, aber kein Zusatzspeicher gebucht. Bei Stripe ist nichts zu tun; der Kunde hat „wir prüfen“ bekommen – bitte kurz antworten.</div></td></tr></table></td></tr>
<!-- ENDE kein_speicher -->
<!-- WENN wechsel_ausstehend -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Zu tun</div><div class="mut" style="color:#4a4843;">Wechsel ausstehend – Speicher nach Abschluss des Wechsels von Hand reduzieren ({{stripe_abo_id}}, gekündigt: {{pakete}} Paket(e)). Solange die Wechselzahlung aussteht, würde eine Änderung des Abos die Wechselrechnung stornieren. Der Kunde hat „wir prüfen“ bekommen; den Zeitpunkt bestätigen.</div></td></tr></table></td></tr>
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Zu tun</div><div class="mut" style="color:#4a4843;">Wechsel ausstehend – Speicher nach Abschluss des Wechsels von Hand reduzieren ({{stripe_abo_id}}, gekündigt: {{pakete_angabe}}). Solange die Wechselzahlung aussteht, würde eine Änderung des Abos die Wechselrechnung stornieren. Der Kunde hat „wir prüfen“ bekommen; den Zeitpunkt bestätigen.</div></td></tr></table></td></tr>
<!-- ENDE wechsel_ausstehend -->
<!-- WENN speicher_fehlgeschlagen -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Zusatzspeicher NICHT automatisch verringert ({{stripe_abo_id}}, gekündigt: {{pakete}} Paket(e)): {{fehler}} Bitte in Stripe von Hand verringern und dem Kunden den Zeitpunkt bestätigen.</div></td></tr></table></td></tr>
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Zusatzspeicher NICHT automatisch verringert ({{stripe_abo_id}}, gekündigt: {{pakete_angabe}}): {{fehler}} Bitte in Stripe von Hand verringern und dem Kunden den Zeitpunkt bestätigen.</div></td></tr></table></td></tr>
<!-- ENDE speicher_fehlgeschlagen -->
<!-- WENN nachweis_fehlt -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Nachweis nicht gespeichert</div><div class="mut" style="color:#4a4843;">Das Ereignis kuendigung konnte nicht in contractEvents angelegt werden. Diese Mail ist der Nachweis der Erklärung.</div></td></tr></table></td></tr>
<!-- ENDE nachweis_fehlt -->
<!-- WENN speicher_kind_pruefung -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Zu tun</div><div class="mut" style="color:#4a4843;">{{kuendigungsart}} des Zusatzspeichers: Grund prüfen, ggf. früher beenden und den Rest erstatten. Die Verringerung zum Periodenende läuft unabhängig davon (siehe oben).</div></td></tr></table></td></tr>
<!-- ENDE speicher_kind_pruefung -->
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
@@ -7,8 +7,8 @@
Betreff: [Abo] WIDERRUF eingegangen: {{email}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{email}}, {{name}}, {{kundennummer}}, {{bestellung}}, {{stripe_abo_id}}, {{abo_beginn}}, {{fristende}}, {{rechtzeitig}}, {{ereignisse}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, abo_gefunden, auf_kuendigung_gesetzt, kein_abo_gefunden, kuendigung_fehlgeschlagen, abschluss, wechsel, zusatzspeicher, keine_frist, nachweis_fehlt
Platzhalter: {{email}}, {{name}}, {{kundennummer}}, {{bestellung}}, {{stripe_abo_id}}, {{abo_beginn}}, {{fristende}}, {{rechtzeitig}}, {{ereignisse}}, {{pruefhinweis}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, abo_gefunden, auf_kuendigung_gesetzt, kein_abo_gefunden, kuendigung_fehlgeschlagen, abschluss, wechsel, zusatzspeicher, keine_frist, nachweis_fehlt, wechsel_offen, abschluss_verspaetet, nicht_pruefbar, bereits_widerrufen
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -39,7 +39,7 @@ a{color:#111111;}
<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;">Kundennummer</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kundennummer}}</td></tr>
<!-- ENDE kundennummer -->
<!-- WENN abo_gefunden -->
<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;">Stripe-Abo</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{stripe_abo_id}}<!-- WENN auf_kuendigung_gesetzt --> <span class="mut" style="color:#6b6964;">(auf Kündigung gesetzt)</span><!-- ENDE auf_kuendigung_gesetzt --></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;">Abo-Beginn</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{abo_beginn}}</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;">Widerrufsfrist bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{fristende}}, 24 Uhr</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Eingang</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;"><strong>{{rechtzeitig}}</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;">Stripe-Abo</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{stripe_abo_id}}<!-- WENN auf_kuendigung_gesetzt --> <span class="mut" style="color:#6b6964;">(auf Kündigung gesetzt)</span><!-- ENDE auf_kuendigung_gesetzt --></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;">Abo-Beginn</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{abo_beginn}}</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;">Widerrufsfrist bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{fristende}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Eingang</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;"><strong>{{rechtzeitig}}</strong></td></tr>
<!-- ENDE abo_gefunden -->
</table></td></tr>
@@ -63,6 +63,18 @@ a{color:#111111;}
<!-- WENN keine_frist -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Nichts automatisch geändert – keine offene Frist gefunden: zu dieser Bestellung läuft keine Widerrufsfrist (mehr) oder es gibt keine Bestellung dieser Art. Bitte prüfen, bevor der Widerruf abgelehnt wird; der Kunde hat „wir prüfen“ bekommen.</div></td></tr></table></td></tr>
<!-- ENDE keine_frist -->
<!-- WENN wechsel_offen -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Wechselrechnung noch offen – stornieren, damit der Wechsel nicht nachträglich wirksam wird (Stripe → Rechnung stornieren; die zurückgehaltene Änderung verfällt damit).</div></td></tr></table></td></tr>
<!-- ENDE wechsel_offen -->
<!-- WENN abschluss_verspaetet -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Hinweis</div><div class="mut" style="color:#4a4843;">Widerruf des Abschlusses nach der Frist: ggf. als ordentliche Kündigung behandeln (Mindestlaufzeit beachten) und dem Kunden das Ergebnis mitteilen.</div></td></tr></table></td></tr>
<!-- ENDE abschluss_verspaetet -->
<!-- WENN nicht_pruefbar -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Achtung</div><div class="mut" style="color:#4a4843;">Widerrufsfrist nur eingeschränkt prüfbar: {{pruefhinweis}}. Beim Abschluss zählt ersatzweise der Abo-Beginn; ohne offene Frist wurde nichts automatisch geändert und der Kunde hat „wir prüfen“ bekommen.</div></td></tr></table></td></tr>
<!-- ENDE nicht_pruefbar -->
<!-- WENN bereits_widerrufen -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Hinweis</div><div class="mut" style="color:#4a4843;">Diese Bestellung ist schon widerrufen (Ereignis widerrufen). Nichts weiter zu tun, sofern die Rückabwicklung läuft; dem Kunden kurz antworten.</div></td></tr></table></td></tr>
<!-- ENDE bereits_widerrufen -->
<!-- WENN nachweis_fehlt -->
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Nachweis nicht gespeichert</div><div class="mut" style="color:#4a4843;">Das Ereignis widerruf konnte nicht in contractEvents angelegt werden. Diese Mail ist der Nachweis der Erklärung.</div></td></tr></table></td></tr>
<!-- ENDE nachweis_fehlt -->
+71 -19
View File
@@ -645,7 +645,8 @@ const RATE_WINDOW_MS = 60 * 60 * 1000
const RATE_MAX = 10
const recentRequests = new Map() // ip -> [timestamps]
function purgeRateLimit(now) {
/** Eintraege aelter als eine Stunde (zum Zeitpunkt now) entfernen; laeuft bei jeder Anfrage, exportiert fuer Tests. */
export function purgeRateLimit(now = Date.now()) {
for (const [ip, list] of recentRequests) {
const fresh = list.filter((t) => now - t < RATE_WINDOW_MS)
if (fresh.length) recentRequests.set(ip, fresh)
@@ -666,10 +667,9 @@ export function resetPublicRateLimit() {
recentRequests.clear()
}
/** Nur fuer Tests: Zahl der IP-Adressen, die zum Zeitpunkt now noch gezaehlt wuerden (aendert nichts). */
export function publicRateLimitState({ now = Date.now() } = {}) {
const ips = [...recentRequests.values()].filter((list) => list.some((t) => now - t < RATE_WINDOW_MS)).length
return { ips }
/** Nur fuer Tests: Zahl der IP-Adressen, die gerade in der Map stehen. */
export function publicRateLimitState() {
return { ips: recentRequests.size }
}
// Auswahl im Formular: Vertrag (Kuendigung) und Bestellung (Widerruf), Standard jeweils der erste Wert
@@ -892,9 +892,14 @@ router.post('/public/cancel', async (req, res) => {
wunschtermin: storage ? 'Ende des Abrechnungsmonats' : d.effective ? fmtDate(`${d.effective}T12:00:00Z`) : 'nächstmöglicher Zeitpunkt',
grund: d.reason,
vertragsende: fmtDate(effectiveAt),
// {{pakete}} = gekuendigte Pakete (F31); solange nichts verringert wurde, der Wunsch des Kunden
pakete: reduced ? result.details.packages : d.packages ?? 'alle',
// {{pakete}} = tatsaechlich gekuendigte Pakete (F31, nur nach einer Verringerung); {{pakete_angabe}} = die
// Erklaerung des Kunden (Inhalt der Erklaerung, § 312k Abs. 4 BGB) in jedem Speicherpfad
pakete: reduced ? result.details.packages : '',
pakete_angabe: d.packages ? `${d.packages} Paket(e)` : 'alle gebuchten Pakete',
}
// Eine ausserordentliche Kuendigung oder Geschaeftsaufgabe des Speichers wird trotzdem zum Periodenende
// verringert; die Pruefung der Art kommt dazu (Fix 1, 6)
const storageKindCheck = storage && d.kind !== 'ordentlich'
await sendCustomerMail(d.email, {
template: 'K03',
@@ -910,6 +915,8 @@ router.post('/public/cancel', async (req, res) => {
kein_abo_gefunden: storage ? !sub : !effectiveAt && d.kind !== 'ausserordentlich',
zusatzspeicher: reduced,
pruefen: storage && Boolean(sub) && !reduced,
speicher_angaben: storage,
speicher_kind_pruefung: storageKindCheck && reduced,
},
}).catch((e) => console.error('[abo] Kuendigungsbestaetigung fehlgeschlagen:', e.message))
@@ -934,6 +941,8 @@ router.post('/public/cancel', async (req, res) => {
wechsel_ausstehend: result.outcome === 'wechsel_ausstehend',
speicher_fehlgeschlagen: result.outcome === 'fehlgeschlagen',
nachweis_fehlt: Boolean(sub) && !result.event,
speicher_angaben: storage,
speicher_kind_pruefung: storageKindCheck,
},
})
@@ -952,13 +961,27 @@ const latestUntil = (list) => list.reduce((latest, w) => (!latest || w.until > l
*/
async function withdrawOrder(sub, d, receivedAt) {
const type = ORDER_EVENT_TYPE[d.order]
const events = (await listContractEvents(sub.customerId)).filter((e) => e.subscriptionId === sub.$id && e.type === type)
// Ereignisse nicht lesbar: nichts automatisch aendern (ausser dem Rueckfall auf startedAt beim Abschluss),
// T09 sagt "von Hand pruefen" (Fix 1, 4)
let events = []
let unreadable = false
try {
events = (await listContractEvents(sub.customerId)).filter((e) => e.subscriptionId === sub.$id && e.type === type)
} catch (err) {
unreadable = true
console.error(`[abo] Vertragsereignisse für Widerruf (Abo ${sub.$id}) nicht lesbar:`, err.message)
}
let open = openWithdrawals(events, receivedAt)
let deadline = latestUntil(open)
if (type === 'abschluss' && !events.length && sub.startedAt) {
const { endsAt } = withdrawalDeadline(sub.startedAt)
deadline = endsAt
if (receivedAt <= endsAt) open = [{ type, eventId: null, until: endsAt }]
let startMissing = false
if (type === 'abschluss' && !events.length) {
if (sub.startedAt) {
const { endsAt } = withdrawalDeadline(sub.startedAt)
deadline = endsAt
if (receivedAt <= endsAt) open = [{ type, eventId: null, until: endsAt }]
} else {
startMissing = true
}
}
if (!deadline) {
// Frist schon vorbei: das spaeteste Fristende der Bestellungen dieses Typs fuer den Hinweis ans Team
@@ -992,7 +1015,23 @@ async function withdrawOrder(sub, d, receivedAt) {
}
}
const event = await recordDeclarationEvent(sub, 'widerruf', receivedAt, { order: d.order, eventId: ids[0] ?? null, eventIds: ids, inTime }, 'offen')
return { sub, inTime, deadline, withdrawn, canceled, cancelFailed, event }
return {
sub,
inTime,
deadline,
withdrawn,
canceled,
cancelFailed,
event,
unreadable,
startMissing,
// Dieselbe Bestellung wurde schon widerrufen (Fix 1, 5)
alreadyWithdrawn: !inTime && events.some((e) => e.status === 'widerrufen'),
// "NACH Fristende" nur, wenn die Frist wirklich vorbei ist
late: !inTime && Boolean(deadline) && receivedAt > deadline,
// Kartenwechsel, der noch auf die Zahlung wartet: die offene Wechselrechnung muss storniert werden (Fix 1, 7)
openSwitchWithdrawn: withdrawn.some((e) => e.type === 'wechsel' && !e.details?.effectiveAt),
}
}
/** Zeile je widerrufenem Ereignis fuer T09: ID, Inhalt (Speicher: Menge vorher -> nachher), Bestelldatum. */
@@ -1012,7 +1051,7 @@ router.post('/public/withdraw', async (req, res) => {
const receivedAt = new Date()
let sub = null
let result = { inTime: false, deadline: null, withdrawn: [], canceled: false, cancelFailed: false, event: null }
let result = { inTime: false, deadline: null, withdrawn: [], canceled: false, cancelFailed: false, event: null, unreadable: false, startMissing: false, alreadyWithdrawn: false, late: false, openSwitchWithdrawn: false }
try {
sub = await findActiveSubscriptionByEmail(d.email)
if (sub) {
@@ -1023,6 +1062,8 @@ router.post('/public/withdraw', async (req, res) => {
console.error('[abo] Widerruf nicht verarbeitbar:', err.message)
}
const { inTime } = result
// Fuer T09: warum sich die Frist nicht pruefen liess
const pruefhinweis = result.unreadable ? 'Ereignisse nicht lesbar – bitte von Hand prüfen' : result.startMissing ? 'Abo-Beginn fehlt' : ''
await sendCustomerMail(d.email, {
template: 'K04',
@@ -1052,15 +1093,22 @@ router.post('/public/withdraw', async (req, res) => {
bestellung: orderLabel(d.order, sub),
stripe_abo_id: sub?.stripeSubscriptionId || '',
abo_beginn: fmtDate(sub?.startedAt),
fristende: fmtDate(result.deadline),
fristende: result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden',
rechtzeitig: !sub
? 'ungeprüft (kein Abo gefunden)'
: inTime
? 'RECHTZEITIG'
: result.deadline
? 'NACH Fristende, vor einer Ablehnung bitte prüfen'
: 'ungeprüft (keine Bestellung dieser Art gefunden)',
: result.alreadyWithdrawn
? 'bereits widerrufen'
: pruefhinweis
? `ungeprüft (${pruefhinweis})`
: result.late
? 'NACH Fristende, vor einer Ablehnung bitte prüfen'
: result.deadline
? 'ungeprüft (keine offene Frist: Bestellung gescheitert)'
: 'ungeprüft (keine Bestellung dieser Art gefunden)',
ereignisse: describeWithdrawn(result.withdrawn),
pruefhinweis,
},
blocks: {
kundennummer: Boolean(d.customerNumber),
@@ -1071,8 +1119,12 @@ router.post('/public/withdraw', async (req, res) => {
abschluss: inTime && d.order === 'abschluss',
wechsel: inTime && d.order === 'wechsel',
zusatzspeicher: inTime && d.order === 'zusatzspeicher',
keine_frist: Boolean(sub) && !inTime,
keine_frist: Boolean(sub) && !inTime && !pruefhinweis && !result.alreadyWithdrawn,
nachweis_fehlt: Boolean(sub) && !result.event,
wechsel_offen: result.openSwitchWithdrawn,
abschluss_verspaetet: Boolean(sub) && d.order === 'abschluss' && result.late && !result.alreadyWithdrawn,
nicht_pruefbar: Boolean(sub) && Boolean(pruefhinweis),
bereits_widerrufen: Boolean(sub) && result.alreadyWithdrawn,
},
})
+13
View File
@@ -290,4 +290,17 @@ test('K03, K04, T08, T09: Vertrag bzw. Bestellung, Pakete und die Blöcke der Au
assert.match(renderMail('T09', t09, { abo_gefunden: true, abschluss: true }).text, /Abo in Stripe sofort beenden/)
assert.match(renderMail('T09', t09, { abo_gefunden: true, keine_frist: true }).text, /keine offene Frist gefunden/)
assert.match(renderMail('T09', t09, { abo_gefunden: true, wechsel: true }).text, /Preis zurück auf das Hosting-Abo/)
// Fix 1: Paketzahl in jedem Speicherpfad (K03/T08), Pruefung der Kuendigungsart, T09-Hinweise
assert.match(renderMail('K03', { ...k03, pakete_angabe: 'alle gebuchten Pakete' }, { speicher_angaben: true, pruefen: true }).text, /Pakete: alle gebuchten Pakete/)
assert.match(renderMail('K03', { ...k03, kuendigungsart: 'außerordentliche Kündigung' }, { speicher_kind_pruefung: true }).text, /Ihre außerordentliche Kündigung wird geprüft/)
const t08a = { ...t08, pakete_angabe: '2 Paket(e)', kuendigungsart: 'Außerordentliche Kündigung' }
assert.match(renderMail('T08', t08a, { speicher_angaben: true, wechsel_ausstehend: true }).text, /Pakete: 2 Paket\(e\)/)
assert.match(renderMail('T08', t08a, { wechsel_ausstehend: true }).text, /gekündigt: 2 Paket\(e\)/)
assert.match(renderMail('T08', t08a, { speicher_kind_pruefung: true }).text, /Außerordentliche Kündigung des Zusatzspeichers/)
const t09b = { ...t09, fristende: 'keine Frist gefunden', pruefhinweis: 'Ereignisse nicht lesbar – bitte von Hand prüfen' }
assert.match(renderMail('T09', t09b, { abo_gefunden: true }).text, /Widerrufsfrist bis: keine Frist gefunden/)
assert.match(renderMail('T09', t09b, { abo_gefunden: true, nicht_pruefbar: true }).text, /Ereignisse nicht lesbar – bitte von Hand prüfen/)
assert.match(renderMail('T09', t09b, { abo_gefunden: true, wechsel: true, wechsel_offen: true }).text, /Wechselrechnung noch offen – stornieren/)
assert.match(renderMail('T09', t09b, { abo_gefunden: true, abschluss_verspaetet: true }).text, /als ordentliche Kündigung behandeln/)
assert.match(renderMail('T09', t09b, { abo_gefunden: true, bereits_widerrufen: true }).text, /schon widerrufen/)
})
+158 -11
View File
@@ -22,7 +22,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post } = await import('./support/testApp.js')
const { hostingDoc, consent, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
const { default: aboRoutes, resetPublicRateLimit, publicRateLimitState } = await import('../server/routes/abo.js')
const { default: aboRoutes, resetPublicRateLimit, publicRateLimitState, purgeRateLimit } = await import('../server/routes/abo.js')
const { listContractEvents, recordContractEvent } = await import('../server/services/contractEvents.js')
const { withdrawalDeadline } = await import('../server/services/deadlines.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
@@ -180,10 +180,17 @@ test('Kündigung Zusatzspeicher ohne gebuchten Speicher: 200, keine Stripe-Ände
assert.match(k03().text, /prüfen/)
assert.doesNotMatch(k03().text, /endet zum|verringert/)
assert.match(t08().text, /kein Zusatzspeicher gebucht/i)
// § 312k Abs. 4 BGB: die Bestaetigung nennt den Inhalt der Erklaerung, also auch die Paketzahl
assert.match(k03().text, /Pakete: alle gebuchten Pakete/)
assert.match(t08().text, /Pakete: alle gebuchten Pakete/)
const [ev] = await events('kuendigung')
assert.equal(ev.status, 'offen')
assert.deepEqual(ev.details, { contract: 'zusatzspeicher', effectiveAt: null, packages: null, fromUnits: 0, toUnits: null })
assert.equal(t13(), null)
mails.sent.length = 0
assert.equal((await cancel({ contract: 'zusatzspeicher', packages: '2' })).status, 200)
assert.match(k03().text, /Pakete: 2 Paket\(e\)/)
assert.match(t08().text, /Pakete: 2 Paket\(e\)/)
})
test('Unbekannte E-Mail: 200, keine Änderung', async () => {
@@ -201,6 +208,8 @@ test('Unbekannte E-Mail: 200, keine Änderung', async () => {
assert.equal(st.calls.length, before)
assert.match(k03().text, /ordnen Ihre Kündigung/)
assert.match(k03().text, /E-Mail-Zusatzspeicher/)
assert.match(k03().text, /Pakete: alle gebuchten Pakete/)
assert.match(t08().text, /Pakete: alle gebuchten Pakete/)
})
test('Kündigung des Abos: wie bisher bei Stripe, dazu das Ereignis kuendigung mit dem Abo-Namen', async () => {
@@ -247,15 +256,18 @@ test('Außerordentliche Kündigung des Abos: keine Stripe-Änderung, Ereignis of
// Controller: waehrend ein Kartenwechsel aussteht (offenes Ereignis wechsel, Stripe pending_update) darf die
// Verringerung das Stripe-Abo nicht anfassen - Stripe wuerde die Wechselrechnung stornieren
test('Wechsel ausstehend (Karte): Speicher nicht bei Stripe geändert, K03 „wir prüfen“, T08 mit Hinweis', async () => {
await resetSubH({ units: 2, stripe: { pending_update: { expires_at: Math.floor(Date.now() / 1000) + 3600 } } })
const switchedAt = new Date().toISOString()
await recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt, effectiveAt: null, paymentMethod: 'card', paymentUrl: 'https://pay.example/inv' },
consent, status: 'offen',
})
const before = st.calls.length
const res = await cancel({ contract: 'zusatzspeicher', packages: '1' })
const openSwitch = () =>
recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt, effectiveAt: null, paymentMethod: 'card', paymentUrl: 'https://pay.example/inv' },
consent, status: 'offen',
})
// Ausloeser 1: nur das offene Ereignis wechsel (Stripe traegt noch kein pending_update, z. B. Webhook-Rueckstand)
await resetSubH({ units: 2 })
await openSwitch()
let before = st.calls.length
let res = await cancel({ contract: 'zusatzspeicher', packages: '1' })
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(subscriptionUpdates(before), [], 'kein Stripe-Update')
assert.equal(storageQuantity(), 2)
@@ -264,8 +276,26 @@ test('Wechsel ausstehend (Karte): Speicher nicht bei Stripe geändert, K03 „wi
assert.equal(ev.status, 'offen')
assert.deepEqual(ev.details, { contract: 'zusatzspeicher', effectiveAt: null, packages: 1, fromUnits: 2, toUnits: null })
assert.match(k03().text, /prüfen/)
assert.match(k03().text, /Pakete: 1 Paket\(e\)/)
assert.match(t08().text, /Wechsel ausstehend – Speicher nach Abschluss des Wechsels von Hand reduzieren/)
assert.match(t08().text, /Pakete: 1 Paket\(e\)/)
assert.equal((await events('wechsel'))[0].status, 'offen')
// Ausloeser 2: nur pending_update am Stripe-Abo (Ereignis nicht gespeichert, nachweis_fehlt beim Wechsel)
await resetSubH({ units: 2, stripe: { pending_update: { expires_at: Math.floor(Date.now() / 1000) + 3600 } } })
before = st.calls.length
res = await cancel({ contract: 'zusatzspeicher' })
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(subscriptionUpdates(before), [], 'kein Stripe-Update')
assert.equal(storageQuantity(), 2)
assert.match(t08().text, /Wechsel ausstehend/)
assert.match(t08().text, /gekündigt: alle gebuchten Pakete/)
assert.match(k03().text, /Pakete: alle gebuchten Pakete/)
// Beide Ausloeser weg: Verringerung laeuft
await resetSubH({ units: 2 })
assert.equal((await cancel({ contract: 'zusatzspeicher', packages: '1' })).status, 200)
assert.equal(storageQuantity(), 1)
})
// F20: wirft changeStorage trotzdem (z. B. Stripe lehnt ab), bleibt die Seite bei 200 - K03 "wir pruefen", T08 Hinweis
@@ -280,7 +310,9 @@ test('changeStorage scheitert: 200, K03 „wir prüfen“, T08 nennt den Fehler'
assert.deepEqual(ev.details, { contract: 'zusatzspeicher', effectiveAt: null, packages: null, fromUnits: 2, toUnits: null })
assert.match(k03().text, /prüfen/)
assert.match(t08().text, /nicht automatisch verringert/i)
assert.match(t08().text, /gekündigt: alle gebuchten Pakete/)
assert.match(t08().text, /Stripe/)
assert.match(k03().text, /Pakete: alle gebuchten Pakete/)
assert.equal((await events('speicher'))[0].status, 'gescheitert')
})
@@ -457,9 +489,124 @@ test('Ratenbegrenzung: 11. Anfrage 429; Reset; Einträge älter als eine Stunde
resetPublicRateLimit()
assert.equal(publicRateLimitState().ips, 0)
assert.equal((await cancel({})).status, 200)
// Nach einer Stunde ist der Eintrag weg (Zeit im Test vorgegeben)
assert.equal(publicRateLimitState({ now: Date.now() + 3600 * 1000 + 1 }).ips, 0)
assert.equal(publicRateLimitState().ips, 1)
// Eintraege aelter als eine Stunde werden aus der Map entfernt (Zeit im Test vorgegeben), juengere bleiben
purgeRateLimit(Date.now() + 3600 * 1000 - 5000)
assert.equal(publicRateLimitState().ips, 1)
purgeRateLimit(Date.now() + 3600 * 1000 + 1)
assert.equal(publicRateLimitState().ips, 0)
})
// Fix 1 (6): eine ausserordentliche Kuendigung des Speichers wird trotzdem zum Periodenende verringert; die Pruefung
// der Art steht in K03 und T08
test('Zusatzspeicher außerordentlich gekündigt: trotzdem zum Periodenende verringert, Prüfung angekündigt', async () => {
await resetSubH({ units: 3 })
const res = await cancel({ contract: 'zusatzspeicher', packages: '2', kind: 'ausserordentlich', reason: 'Postfach dauernd voll' })
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.equal(storageQuantity(), 1)
const [ev] = await events('kuendigung')
assert.equal(ev.details.toUnits, 1)
assert.match(k03().text, /um 2 Paket\(e\) gekündigt/)
assert.match(k03().text, /außerordentliche Kündigung wird geprüft/)
assert.match(t08().text, /Außerordentliche Kündigung des Zusatzspeichers/)
assert.match(t08().text, /Grund: Postfach dauernd voll/)
// ordentlich: kein Pruefungshinweis
await resetSubH({ units: 3 })
assert.equal((await cancel({ contract: 'zusatzspeicher', packages: '2' })).status, 200)
assert.doesNotMatch(k03().text, /wird geprüft/)
assert.doesNotMatch(t08().text, /des Zusatzspeichers: Grund prüfen/)
})
// Fix 1 (4): Ereignisse nicht lesbar - Abschluss faellt auf startedAt zurueck (F14), sonst Hinweis "von Hand pruefen"
test('Widerruf bei nicht lesbaren Ereignissen: Hinweis fürs Team, Abschluss zählt weiter ab startedAt', async () => {
await resetSubH({ units: 0 })
aw.failNext({ collection: config.collections.contractEvents, method: 'GET', status: 500 })
let before = st.calls.length
let res = await withdraw({ order: 'wechsel' })
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(subscriptionUpdates(before), [])
assert.match(k04().text, /prüfen/)
assert.match(t09().text, /Ereignisse nicht lesbar – bitte von Hand prüfen/)
assert.doesNotMatch(t09().text, /keine Bestellung dieser Art/)
assert.equal((await events('widerruf')).length, 1, 'Ereignis widerruf trotzdem gespeichert')
// Abschluss in der Frist: der Rueckfall auf startedAt gilt auch, wenn die Ereignisse nicht lesbar sind
const startedAt = daysAgo(2)
await resetSubH({ units: 0, patch: { startedAt, termStartAt: startedAt } })
aw.failNext({ collection: config.collections.contractEvents, method: 'GET', status: 500 })
before = st.calls.length
res = await withdraw({ order: 'abschluss' })
assert.equal(res.status, 200)
assert.equal(subscriptionUpdates(before).length, 1)
assert.equal(st.subscriptions.get('sub_h').cancel_at_period_end, true)
assert.match(t09().text, /RECHTZEITIG/)
assert.match(t09().text, /Ereignisse nicht lesbar/)
// Abo-Beginn fehlt (kein Ereignis abschluss, kein startedAt): eigener Hinweis, nichts geaendert
await resetSubH({ units: 0, patch: { startedAt: null, termStartAt: null } })
before = st.calls.length
res = await withdraw({ order: 'abschluss' })
assert.equal(res.status, 200)
assert.deepEqual(subscriptionUpdates(before), [])
assert.match(t09().text, /Abo-Beginn fehlt/)
assert.doesNotMatch(t09().text, /keine Bestellung dieser Art|NACH Fristende/)
})
// Fix 1 (5): "NACH Fristende" nur nach dem Fristende; ein zweiter Widerruf derselben Bestellung heisst "bereits widerrufen"
test('Wiederholter Widerruf: „bereits widerrufen“ statt „NACH Fristende“', async () => {
await resetSubH({ units: 0 })
const consentAt = daysAgo(3)
await recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt: consentAt, effectiveAt: consentAt, paidAt: consentAt },
consent: { ...consent, consentAt }, status: 'erledigt',
})
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
assert.match(t09().text, /RECHTZEITIG/)
mails.sent.length = 0
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
assert.match(t09().text, /bereits widerrufen/)
assert.doesNotMatch(t09().text, /NACH Fristende|RECHTZEITIG/)
assert.match(k04().text, /prüfen/)
const widerrufe = await events('widerruf')
assert.equal(widerrufe.length, 2)
assert.equal(widerrufe[0].details.eventId, null)
})
// Fix 1 (7): Widerruf eines noch nicht wirksamen Kartenwechsels bzw. eines verspaeteten Abschluss-Widerrufs
test('T09: offene Wechselrechnung stornieren; verspäteter Abschluss-Widerruf ggf. als Kündigung', async () => {
await resetSubH({ units: 0 })
const consentAt = daysAgo(1)
await recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt: consentAt, effectiveAt: null, paymentMethod: 'card', paymentUrl: 'https://pay.example/inv' },
consent: { ...consent, consentAt }, status: 'offen',
})
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
assert.equal((await events('wechsel'))[0].status, 'widerrufen')
assert.match(t09().text, /Wechselrechnung noch offen – stornieren, damit der Wechsel nicht nachträglich wirksam wird/)
// Wirksamer (bezahlter) Wechsel: kein Storno-Hinweis
await resetSubH({ units: 0 })
await recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt: consentAt, effectiveAt: consentAt, paidAt: consentAt },
consent: { ...consent, consentAt }, status: 'erledigt',
})
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
assert.doesNotMatch(t09().text, /Wechselrechnung noch offen/)
// Abschluss nach der Frist (Fixture: Abo seit 40 Tagen): Hinweis auf die ordentliche Kuendigung
await resetSubH({ units: 0 })
assert.equal((await withdraw({ order: 'abschluss' })).status, 200)
assert.match(t09().text, /NACH Fristende/)
assert.match(t09().text, /ggf\. als ordentliche Kündigung behandeln/)
// Zusatzspeicher ohne jede Bestellung: keine Frist gefunden statt "–, 24 Uhr"
mails.sent.length = 0
assert.equal((await withdraw({ order: 'zusatzspeicher' })).status, 200)
assert.match(t09().text, /Widerrufsfrist bis: keine Frist gefunden/)
assert.doesNotMatch(t09().text, /–, 24 Uhr|als ordentliche Kündigung/)
assert.match(t09().text, /keine Bestellung dieser Art/)
})
// ---------------------------------------------------------------------------