Kundenansicht neu E-Fix F5: Fassung 6 ohne PDF behauptet keinen Anhang, das Team bekommt den Hinweis

Die PDFs der Fassung 6 entstehen erst nach der Freigabe. Bis dahin gingen K01, K12 und K13 ohne Anhang raus, sagten aber "im Anhang" bzw.
"beigefügt", und das Team erfuhr es nicht (nur K38/T24 hatten dafür den Block anhang_fehlt).

- Neuer Helfer vertragsAnhang.js: lädt das PDF der zugestimmten Fassung und meldet fehlt nur bei Fassung 6 ohne lesbare Datei.
- K01, K12, K13: Block anhang (Anhangsaussagen) und ohne_anhang (Satz, dass die Unterlagen gesondert kommen).
- T02, T12, T13: Block anhang_fehlt "Unterlagen von Hand nachsenden" (T12 auch beim späteren Wirksamwerden, T13 auch ohne Aufgabe).
- Alte Fassungen unverändert: Hash-Pins auf die Wiedergabe der Vorlagen vor der Änderung, Anhang und Texte wie bisher.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 21:38:54 +00:00
1 parent 9c04f806eb
commit 72b5ac5265
12 files changed
+362 -66

No files matched your search

@@ -8,7 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail aus der Bestellung) · BCC intern · Anhang: Vertragsunterlagen der zugestimmten Fassung (PDF, contractAttachmentFor)
Platzhalter: {{name}}, {{produkt}}, {{preis}}, {{angebot_text}}, {{vertragsschluss}}, {{leistungsbeginn}}, {{mindestlaufzeit_bis}}, {{bedingungen_version}}, {{zustimmung_zeitpunkt}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, hosting, wartung, wartung_v3, sofortbeginn, beginn_nach_widerrufsfrist
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, hosting, wartung, wartung_v3, sofortbeginn, beginn_nach_widerrufsfrist, anhang, ohne_anhang
anhang = die Vertragsunterlagen hängen als PDF an (immer, außer bei Fassung 6 ohne PDF); ohne_anhang = Fassung 6 ohne PDF: statt der Anhangsaussage der Satz, dass die Unterlagen gesondert kommen (T02 meldet es dem Team, Block anhang_fehlt).
produkt = Katalogname ohne „WEBklar “ („Hosting-Abo“, „Wartungs-Abo“), preis z. B. „20,00 € / Monat“; genau einer der Blöcke hosting/wartung/wartung_v3 bringt die Liste „Enthalten“:
hosting und wartung nach Fassung 4 oder 5 (Spec 3.1), wartung_v3 für ein Wartungs-Abo nach Fassung 3 (termsDocFor nicht v4/v5: „1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat“ wie in den Bedingungen v3 § 3).
bedingungen_version und das PDF im Anhang kommen aus derselben Quelle (termsDocFor(subDoc.termsVersion).version): ohne gespeicherte Fassung beides Fassung 3.
@@ -25,7 +26,7 @@ a{color:#111111;}
</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;">Ihr {{produkt}} ist abgeschlossen. Alle Vertragsunterlagen finden Sie im Anhang.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Ihr {{produkt}} ist abgeschlossen.<!-- WENN anhang --> Alle Vertragsunterlagen finden Sie im Anhang.<!-- ENDE anhang -->&#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;">
@@ -64,7 +65,12 @@ a{color:#111111;}
<!-- WENN hosting -->
<tr><td class="px mut" style="padding:10px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Dort sehen Sie jederzeit Ihre Verträge und Rechnungen.</td></tr>
<!-- ENDE hosting -->
<!-- WENN anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen im Anhang (PDF)</div><div class="mut" style="color:#4a4843;">&#128206;&nbsp; Vertragsbedingungen mit Leistungsbeschreibung<br>&#128206;&nbsp; Widerrufsbelehrung mit Muster-Widerrufsformular<br>&#128206;&nbsp; Auftragsverarbeitungsvertrag</div></td></tr></table></td></tr>
<!-- ENDE anhang -->
<!-- WENN ohne_anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen</div><div class="mut" style="color:#4a4843;">Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung mit Muster-Widerrufsformular und Auftragsverarbeitungsvertrag senden wir Ihnen gesondert zu.</div></td></tr></table></td></tr>
<!-- ENDE ohne_anhang -->
<tr><td class="px mut" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Ihre Zustimmung zu den Vertragsbedingungen (Version {{bedingungen_version}}) einschließlich des Auftragsverarbeitungsvertrags haben wir am {{zustimmung_zeitpunkt}} Uhr erhalten.<br><br>
<!-- WENN sofortbeginn -->
Sie haben ausdrücklich verlangt, dass wir vor Ablauf der Widerrufsfrist mit der Leistung beginnen, und wurden darüber informiert, dass Sie bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahlen.
@@ -8,7 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern · Anhang: Vertragsunterlagen der Fassung 4 (PDF)
Platzhalter: {{name}}, {{wechseltag}}, {{betrag_heute}}, {{monatspreis}}, {{mindestlaufzeit_bis}}, {{bedingungen_version}}, {{zustimmung_zeitpunkt}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_offen, lastschrift, zusatzspeicher, sofortverlangen
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_offen, lastschrift, zusatzspeicher, sofortverlangen, anhang, ohne_anhang
anhang = die Vertragsunterlagen hängen als PDF an (immer, außer bei Fassung 6 ohne PDF); ohne_anhang = Fassung 6 ohne PDF: statt der Anhangsaussage der Satz, dass die Unterlagen gesondert kommen (T12 meldet es dem Team, Block anhang_fehlt).
zahlung_offen bei Karte mit zurückgehaltener Zahlung, lastschrift bei SEPA (Bedingungen § 9 Abs. 3); zusatzspeicher, wenn der Kunde Zusatzspeicher gebucht hat – monatspreis ist dann die Monatsgebühr mit Zusatzspeicher (Spec 3.5).
sofortverlangen (Absatz „Sie haben ausdrücklich verlangt …“ samt Wertersatz): nur, wenn der Kunde den Sofortbeginn ausdrücklich verlangt hat (Zeitstempel earlyStartConsentAt); ein Betrieb in Fassung 6 hat nichts verlangt, der Absatz entfällt.
-->
@@ -24,7 +25,7 @@ a{color:#111111;}
</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;">Ihr Wechsel ins Wartungs-Abo ist bestellt. Alle Vertragsunterlagen finden Sie im Anhang.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Ihr Wechsel ins Wartungs-Abo ist bestellt.<!-- WENN anhang --> Alle Vertragsunterlagen finden Sie im Anhang.<!-- ENDE anhang -->&#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;">
@@ -47,7 +48,12 @@ a{color:#111111;}
<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;">Wechseltag</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{wechseltag}}</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;">Heute fällig</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{betrag_heute}}</strong><br><span class="mut" style="color:#6b6964;">Monatsgebühr des Wartungs-Abos<!-- WENN zusatzspeicher --> mit gebuchtem Zusatzspeicher<!-- ENDE zusatzspeicher -->, abzüglich der Gutschrift für die nicht genutzten Tage Ihres Hosting-Abos</span></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;">Abbuchung</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">monatlich am Kalendertag des Wechsels</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;">Laufzeit</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Mindestlaufzeit 12 Monate bis {{mindestlaufzeit_bis}}.</strong> Kündigung zu diesem Termin bis einen Monat vorher. Danach läuft das Abo unbefristet und ist jederzeit zum Ende des Abrechnungsmonats kündbar.</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Enthalten</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">&#8226;&nbsp; Betrieb und Pflege Ihrer Website bzw. Web-Anwendung<br>&#8226;&nbsp; 1 Domain<br>&#8226;&nbsp; beliebig viele E-Mail-Postfächer (zusammen 5 GB)<br>&#8226;&nbsp; 1 Stunde Arbeitszeit je bezahltem Monat <span class="mut" style="color:#6b6964;">(12 Monate nutzbar)</span></td></tr></table></td></tr>
<tr><td class="px mut" style="padding:20px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;color:#4a4843;">Ihre Postfächer und gebuchter Zusatzspeicher bleiben unverändert. Weitere Postfächer richten wir ein, sobald die Wechselrechnung bezahlt ist; dann steht auch Ihre erste Stunde Arbeitszeit bereit.</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="https://project.webklar.com/dashboard.html" 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>
<!-- WENN anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen im Anhang (PDF)</div><div class="mut" style="color:#4a4843;">&#128206;&nbsp; Vertragsbedingungen mit Leistungsbeschreibung<br>&#128206;&nbsp; Widerrufsbelehrung mit Muster-Widerrufsformular<br>&#128206;&nbsp; Auftragsverarbeitungsvertrag</div></td></tr></table></td></tr>
<!-- ENDE anhang -->
<!-- WENN ohne_anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen</div><div class="mut" style="color:#4a4843;">Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung mit Muster-Widerrufsformular und Auftragsverarbeitungsvertrag senden wir Ihnen gesondert zu.</div></td></tr></table></td></tr>
<!-- ENDE ohne_anhang -->
<tr><td class="px mut" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Ihre Zustimmung zu den Vertragsbedingungen (Version {{bedingungen_version}}) einschließlich des Auftragsverarbeitungsvertrags haben wir am {{zustimmung_zeitpunkt}} Uhr erhalten.<!-- WENN sofortverlangen --><br><br>Sie haben ausdrücklich verlangt, dass wir vor Ablauf der Widerrufsfrist mit dem Wartungs-Abo beginnen, und wurden darüber informiert, dass Sie bei einem Widerruf des Wechsels Wertersatz für die bis dahin erbrachte Leistung zahlen: den anteiligen Mehrpreis des Wartungs-Abos gegenüber dem Hosting-Abo für die Tage bis zum Widerruf. Widerrufen Sie den Wechsel, läuft Ihr Hosting-Abo zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit; den gezahlten Mehrbetrag erstatten wir abzüglich dieses Wertersatzes.<!-- ENDE sofortverlangen --></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,7 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern · Anhang bei einer Bestellung (Plus): Vertragsunterlagen der zugestimmten Fassung (PDF)
Platzhalter: {{name}}, {{pakete}}, {{gesamt_gb}}, {{monatspreis_zusatz}}, {{wirksam_ab}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): plus, plus_bezahlt, bestellung, minus
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): plus, plus_bezahlt, bestellung, minus, anhang, ohne_anhang
anhang = die Vertragsunterlagen hängen als PDF an (immer, außer bei Fassung 6 ohne PDF); ohne_anhang = Fassung 6 ohne PDF: statt der Anhangsaussage der Satz, dass die Unterlagen gesondert kommen (T13 meldet es dem Team, Block anhang_fehlt).
plus = Bestellung über die schon bezahlte Menge (Einrichtung, anteilige Berechnung), plus_bezahlt = Bestellung bis zur schon bezahlten Menge (keine Kosten, nichts einzurichten), bestellung = Unterlagen und Widerrufshinweis (bei beiden Bestellungen), minus = Verringerung zum Ende des Abrechnungsmonats (Spec 3.4) (Bedingungen § 7 Abs. 3, § 10).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -54,8 +55,13 @@ a{color:#111111;}
<!-- ENDE minus -->
<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="https://project.webklar.com/dashboard.html" 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>
<!-- WENN bestellung -->
<!-- WENN anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen im Anhang (PDF)</div><div class="mut" style="color:#4a4843;">&#128206;&nbsp; Vertragsbedingungen mit den Regeln zum E-Mail-Zusatzspeicher<br>&#128206;&nbsp; Widerrufsbelehrung mit Muster-Widerrufsformular</div></td></tr></table></td></tr>
<tr><td class="px mut" style="padding:28px 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 diese Bestellung innerhalb von 14 Tagen widerrufen; die Widerrufsbelehrung finden Sie in den beigefügten Vertragsunterlagen. Widerrufen Sie, nehmen wir die Pakete zurück und erstatten alles, was Sie dafür gezahlt haben. Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist.</td></tr>
<!-- ENDE anhang -->
<!-- WENN ohne_anhang -->
<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="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen</div><div class="mut" style="color:#4a4843;">Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung mit Muster-Widerrufsformular und Auftragsverarbeitungsvertrag senden wir Ihnen gesondert zu.</div></td></tr></table></td></tr>
<!-- ENDE ohne_anhang -->
<tr><td class="px mut" style="padding:28px 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 diese Bestellung innerhalb von 14 Tagen widerrufen; <!-- WENN anhang -->die Widerrufsbelehrung finden Sie in den beigefügten Vertragsunterlagen.<!-- ENDE anhang --><!-- WENN ohne_anhang -->die Widerrufsbelehrung mit Muster-Widerrufsformular senden wir Ihnen gesondert zu.<!-- ENDE ohne_anhang --> Widerrufen Sie, nehmen wir die Pakete zurück und erstatten alles, was Sie dafür gezahlt haben. Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist.</td></tr>
<!-- ENDE bestellung -->
<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>
+5 -1
View File
@@ -8,7 +8,8 @@
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{produkt}}, {{email}}, {{kunde_name}}, {{appwrite_id}}, {{stripe_abo_id}}, {{angebot_text}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, angebot_fehler, nachweis_fehlt
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, angebot_fehler, nachweis_fehlt, anhang_fehlt
anhang_fehlt = Fassung 6, die PDFs liegen nicht vor: K01 ging ohne Anhang raus, das Team sendet die Unterlagen von Hand nach.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -48,6 +49,9 @@ 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;">Nachweis nicht gespeichert</div><div class="mut" style="color:#4a4843;">Das Vertragsereignis „Abschluss“ (Nachweis der Zustimmung, Beginn der Widerrufsfrist) konnte nicht in Appwrite gespeichert werden. Bitte prüfen, ob die Collection contractEvents mit allen Attributen angelegt ist (scripts/setup-abo-collections.mjs), und den Nachweis zu diesem Abo nachtragen. Vertragsbestätigung und Angebot sind davon nicht betroffen.</div></td></tr></table></td></tr>
<!-- ENDE nachweis_fehlt -->
<!-- WENN anhang_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;">Unterlagen von Hand nachsenden</div><div class="mut" style="color:#4a4843;">Die Vertragsunterlagen (Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung mit Muster-Widerrufsformular und Auftragsverarbeitungsvertrag der Fassung 6, jeweils als PDF) sind nicht mit der Bestätigung beim Kunden angekommen: Die PDFs der Fassung 6 liegen noch nicht im Portal oder sind nicht lesbar, die Kundenmail hat deshalb keinen Anhang und sagt, dass die Unterlagen gesondert kommen. Bitte dem Kunden die Unterlagen per E-Mail nachsenden. Der Grund steht im Log des Portals.</div></td></tr></table></td></tr>
<!-- ENDE anhang_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:#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;">Zu tun</div><div class="mut" style="color:#4a4843;">Website als Monitor in UptimeRobot anlegen.</div></td></tr></table></td></tr>
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
+5 -1
View File
@@ -8,7 +8,8 @@
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{email}}, {{name}}, {{stripe_abo_id}}, {{wechseltag}}, {{betrag_heute}}, {{monatspreis}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wirksam, lastschrift, gescheitert, lastschrift_gescheitert, nachweis_fehlt
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wirksam, lastschrift, gescheitert, lastschrift_gescheitert, nachweis_fehlt, anhang_fehlt
anhang_fehlt = Fassung 6, die PDFs liegen nicht vor: K12 ging ohne Anhang raus, das Team sendet die Unterlagen von Hand nach.
wirksam (Wechsel gilt), lastschrift (SEPA, noch nicht eingelöst), gescheitert (K14 verschickt), lastschrift_gescheitert (Handgriffe), nachweis_fehlt; monatspreis = Monatsgebühr des Wartungs-Abos mit gebuchtem Zusatzspeicher (Spec 3.5).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -57,6 +58,9 @@ 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;">Nachweis nicht gespeichert</div><div class="mut" style="color:#4a4843;">Das Vertragsereignis „wechsel“ (Nachweis der Zustimmung, Stand der Zahlung) konnte nicht in Appwrite gespeichert werden. Bitte in contractEvents nachtragen; bis dahin schließen die Stripe-Webhooks diesen Wechsel nicht ab. Ob die Zahlung noch offen ist, zeigt das Abo in Stripe (ausstehende Aktualisierung).</div></td></tr></table></td></tr>
<!-- ENDE nachweis_fehlt -->
<!-- WENN anhang_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;">Unterlagen von Hand nachsenden</div><div class="mut" style="color:#4a4843;">Die Vertragsunterlagen (Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung mit Muster-Widerrufsformular und Auftragsverarbeitungsvertrag der Fassung 6, jeweils als PDF) sind nicht mit der Bestätigung beim Kunden angekommen: Die PDFs der Fassung 6 liegen noch nicht im Portal oder sind nicht lesbar, die Kundenmail hat deshalb keinen Anhang und sagt, dass die Unterlagen gesondert kommen. Bitte dem Kunden die Unterlagen per E-Mail nachsenden. Der Grund steht im Log des Portals.</div></td></tr></table></td></tr>
<!-- ENDE anhang_fehlt -->
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
</td></tr>
@@ -8,7 +8,8 @@
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{aktion}}, {{email}}, {{name}}, {{stripe_abo_id}}, {{postfaecher_hinweis}}, {{neue_groesse_gb}}, {{pakete}}, {{faellig}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): einrichten, periodenende, teilweise, nachweis_fehlt
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): einrichten, periodenende, teilweise, nachweis_fehlt, anhang_fehlt
anhang_fehlt = Fassung 6, die PDFs liegen nicht vor: K13 ging ohne Anhang raus, das Team sendet die Unterlagen von Hand nach (auch wenn es sonst keine Aufgabe gibt: dann ist die Aktion „Unterlagen nachsenden“).
aktion ist „einrichten“ oder „reduzieren zum TT.MM.JJJJ“; einrichten (Plus über die bezahlte Menge, sofort), periodenende (Minus oder Plus bis zur bezahlten Menge, zum Periodenende), teilweise (Schritt 2 bei Stripe fehlgeschlagen), nachweis_fehlt.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -53,6 +54,9 @@ 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;">Nachweis nicht gespeichert</div><div class="mut" style="color:#4a4843;">Das Vertragsereignis „speicher“ (Nachweis der Bestellung und Aufgabe fürs Team) konnte nicht in Appwrite gespeichert werden. Bitte in contractEvents nachtragen; bis dahin ist diese Mail die einzige Aufgabe, im Ticketsystem (Kunden → E-Mail-Speicher) erscheint sie nicht.</div></td></tr></table></td></tr>
<!-- ENDE nachweis_fehlt -->
<!-- WENN anhang_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;">Unterlagen von Hand nachsenden</div><div class="mut" style="color:#4a4843;">Die Vertragsunterlagen (Vertragsbedingungen mit den Regeln zum E-Mail-Zusatzspeicher und Widerrufsbelehrung mit Muster-Widerrufsformular der Fassung 6, jeweils als PDF) sind nicht mit der Bestätigung beim Kunden angekommen: Die PDFs der Fassung 6 liegen noch nicht im Portal oder sind nicht lesbar, die Kundenmail hat deshalb keinen Anhang und sagt, dass die Unterlagen gesondert kommen. Bitte dem Kunden die Unterlagen per E-Mail nachsenden. Der Grund steht im Log des Portals.</div></td></tr></table></td></tr>
<!-- ENDE anhang_fehlt -->
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
</td></tr>
+13 -11
View File
@@ -1,4 +1,3 @@
import { readFile } from 'node:fs/promises'
import { Router } from 'express'
import { config } from '../config.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
@@ -29,8 +28,9 @@ import { neueAnsicht } from '../services/kundenansicht/schalter.js'
import { FEHLER_BEDINGUNGEN, FEHLER_NACHWEIS, FEHLER_UNVOLLSTAENDIG, WECHSEL_GESPERRT, bestelltexteFuerClient, bestellZeile, nachweisTexte } from '../services/bestelltexte.js'
import { kundenartWirksam } from '../services/kundenart.js'
import { fassungDerAnfrage, istNeueFassung, speichereNachweisFuerRoute } from '../services/vertragsNachweis.js'
import { ladeVertragsAnhang } from '../services/vertragsAnhang.js'
import { duFehlerAntworten } from '../services/duFehler.js'
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
import { PRODUCT_KEYS, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
import {
ACTIVE_STATUSES,
cancelAtPeriodEnd,
@@ -636,14 +636,12 @@ async function customerNameOf(sub, fallback = '') {
* v4 Archiv-PDF, v5 aktuelles PDF). Versionsangabe und Anhang kommen aus derselben Quelle (termsDocFor.version): ohne gespeicherte
* Fassung beides Fassung 3. Ohne lesbare Datei geht die Mail ohne Anhang raus.
*/
async function sendContractConfirmation(subDoc, fallbackName = '') {
async function sendContractConfirmation(subDoc, fallbackName = '', unterlagen = null) {
if (!subDoc || subDoc.confirmationSentAt || !subDoc.email) return
const terms = termsDocFor(subDoc.termsVersion)
const attachment = contractAttachmentFor(terms.version)
const pdf = await readFile(attachment.path).catch((err) => {
console.error(`[abo] Vertragsunterlagen ${attachment.filename} nicht lesbar:`, err.message)
return null
})
// Fassung 6 ohne PDF: keine Behauptung "im Anhang" (Block ohne_anhang), T02 sagt es dem Team (F5); ältere Fassungen wie bisher.
// Der Aufrufer (Webhook) hat die Unterlagen für T02 schon geladen und reicht sie durch.
const { attachments, fehlt: anhangFehlt } = unterlagen ?? (await ladeVertragsAnhang(terms.version, 'abo'))
const lateStart = subDoc.startChoice === 'nach_frist'
const productKey = productKeyOf(subDoc)
await sendCustomerMail(subDoc.email, {
@@ -668,8 +666,10 @@ async function sendContractConfirmation(subDoc, fallbackName = '') {
// Fassung 6: ein Betrieb (Sofortbeginn ohne Zeitstempel) hat nichts verlangt; die Mail behauptet es nicht
sofortbeginn: !lateStart && !(istNeueFassung(subDoc.termsVersion) && !subDoc.earlyStartConsentAt),
beginn_nach_widerrufsfrist: lateStart,
anhang: !anhangFehlt,
ohne_anhang: anhangFehlt,
},
attachments: pdf ? [{ filename: attachment.filename, content: pdf, contentType: 'application/pdf' }] : [],
attachments,
})
await updateDocument(config.collections.subscriptions, subDoc.$id, {
confirmationSentAt: new Date().toISOString(),
@@ -768,6 +768,8 @@ async function handleStripeEvent(event) {
return true
}
)
// Vertragsunterlagen der zugestimmten Fassung einmal laden: T02 meldet fehlende PDFs der Fassung 6 (anhang_fehlt), K01 hängt sie an
const unterlagen = sub ? await ladeVertragsAnhang(termsDocFor(sub.termsVersion).version, 'abo') : null
await sendAdminNotice({
template: 'T02',
values: {
@@ -778,9 +780,9 @@ async function handleStripeEvent(event) {
stripe_abo_id: obj.subscription,
angebot_text: obj.metadata?.offerText || '',
},
blocks: { angebot: Boolean(obj.metadata?.offerText), angebot_fehler: offerProblem, nachweis_fehlt: proofProblem },
blocks: { angebot: Boolean(obj.metadata?.offerText), angebot_fehler: offerProblem, nachweis_fehlt: proofProblem, anhang_fehlt: unterlagen?.fehlt === true },
})
await sendContractConfirmation(sub, obj.customer_details?.name).catch((e) =>
await sendContractConfirmation(sub, obj.customer_details?.name, unterlagen).catch((e) =>
console.error('[abo] Vertragsbestaetigung fehlgeschlagen:', e.message)
)
return
+16 -19
View File
@@ -1,9 +1,9 @@
import { readFile } from 'node:fs/promises'
import { config } from '../config.js'
import { getDocument, updateDocument } from './appwriteAdmin.js'
import { listContractEvents, recordContractEvent, updateContractEvent } from './contractEvents.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { contractAttachmentFor, describeItems, priceIdsOfInvoice, regularStorageLine } from './products.js'
import { describeItems, priceIdsOfInvoice, regularStorageLine } from './products.js'
import { ladeVertragsAnhang } from './vertragsAnhang.js'
import {
ACTIVE_STATUSES,
ensurePrice,
@@ -315,16 +315,6 @@ async function supersedeStorageTasks(events, change) {
for (const e of superseded) await updateContractEvent(e.$id, { status: 'erledigt', doneAt: now, doneBy: 'ersetzt' })
}
/** Vertragsunterlagen (PDF) in der zugestimmten Fassung als Mail-Anhang; ohne Datei geht die Mail ohne Anhang raus. */
async function contractPdfAttachment(termsVersion) {
const { filename, path } = contractAttachmentFor(termsVersion)
const content = await readFile(path).catch((err) => {
console.error(`[speicher] Vertragsunterlagen ${filename} nicht lesbar:`, err.message)
return null
})
return content ? [{ filename, content, contentType: 'application/pdf' }] : []
}
/**
* K13 an den Kunden (Plus: mit PDF der zugestimmten Fassung, F22) und T13 ans Team (Aufgabe nach Art, dazu
* die Bloecke teilweise und nachweis_fehlt). Ueber das Kuendigungsformular bestaetigt K03, K13 entfaellt dort
@@ -335,8 +325,13 @@ async function sendStorageMails({ doc, change, consent, viaCancellation, proofMi
const customer = await getDocument(config.collections.customers, doc.customerId).catch(() => null)
const email = customer?.email || doc.email || ''
const dueLabel = fmtDate(change.dueAt)
let anhangFehlt = false
if (!viaCancellation && email) {
const attachments = plus ? await contractPdfAttachment(consent.termsVersion) : []
// Plus: Vertragsunterlagen der zugestimmten Fassung. Fehlt das PDF der Fassung 6, geht K13 ohne Anhang und ohne die Behauptung "beigefügt"
// raus (Block ohne_anhang), T13 sagt es dem Team (anhang_fehlt, F5); ältere Fassungen wie bisher
const unterlagen = plus ? await ladeVertragsAnhang(consent.termsVersion, 'speicher') : { attachments: [], fehlt: false }
anhangFehlt = unterlagen.fehlt
const attachments = unterlagen.attachments
await sendCustomerMail(email, {
template: 'K13',
values: {
@@ -348,16 +343,17 @@ async function sendStorageMails({ doc, change, consent, viaCancellation, proofMi
},
// plus: Einrichtung mit anteiliger Berechnung; plus_bezahlt: bis zur schon bezahlten Menge (keine Kosten);
// bestellung: Unterlagen und Widerruf bei beiden; minus: Verringerung zum Periodenende
blocks: { plus: plus && change.kind === 'einrichten', plus_bezahlt: plus && change.kind !== 'einrichten', bestellung: plus, minus: !plus },
blocks: { plus: plus && change.kind === 'einrichten', plus_bezahlt: plus && change.kind !== 'einrichten', bestellung: plus, minus: !plus, anhang: !anhangFehlt, ohne_anhang: anhangFehlt },
attachments,
}).catch((e) => console.error(`[speicher] K13 an ${email} fehlgeschlagen:`, e.message))
}
// Ohne Auftrag fuers Team (Plus genau zurueck auf die bezahlte Menge) kein T13 - es gibt nichts zu tun
if (!change.task && !proofMissing) return
// Ohne Auftrag fuers Team (Plus genau zurueck auf die bezahlte Menge) kein T13 - es gibt nichts zu tun; ausser die Unterlagen muessen nachgesendet werden
if (!change.task && !proofMissing && !anhangFehlt) return
const nurUnterlagen = !change.task && !proofMissing
await sendAdminNotice({
template: 'T13',
values: {
aktion: change.kind === 'einrichten' ? 'einrichten' : `reduzieren zum ${dueLabel}`,
aktion: nurUnterlagen ? 'Unterlagen nachsenden' : change.kind === 'einrichten' ? 'einrichten' : `reduzieren zum ${dueLabel}`,
email,
name: customer?.name || '',
stripe_abo_id: doc.$id,
@@ -368,10 +364,11 @@ async function sendStorageMails({ doc, change, consent, viaCancellation, proofMi
faellig: dueLabel,
},
blocks: {
einrichten: change.kind === 'einrichten',
periodenende: change.kind === 'periodenende',
einrichten: !nurUnterlagen && change.kind === 'einrichten',
periodenende: !nurUnterlagen && change.kind === 'periodenende',
teilweise: change.partial,
nachweis_fehlt: proofMissing,
anhang_fehlt: anhangFehlt,
},
})
}
+15 -19
View File
@@ -1,9 +1,9 @@
import { readFile } from 'node:fs/promises'
import { config } from '../config.js'
import { getDocument } from './appwriteAdmin.js'
import { eventIdFor, getContractEvent, listContractEvents, recordContractEvent, updateContractEvent } from './contractEvents.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { contractAttachmentFor, describeItems } from './products.js'
import { describeItems } from './products.js'
import { anhangFehlt, ladeVertragsAnhang } from './vertragsAnhang.js'
import { getFeatureDoc } from './siteFeatures.js'
import {
addMonths,
@@ -276,23 +276,17 @@ async function previewFor(ctx, prorationDate) {
}
}
/** Vertragsunterlagen (PDF) in der zugestimmten Fassung; ohne Datei geht K12 ohne Anhang raus (Fehler im Log). */
async function contractPdfAttachment(termsVersion) {
const { filename, path } = contractAttachmentFor(termsVersion)
const content = await readFile(path).catch((err) => {
console.error(`[wechsel] Vertragsunterlagen ${filename} nicht lesbar:`, err.message)
return null
})
return content ? [{ filename, content, contentType: 'application/pdf' }] : []
}
/** K12 an den Kunden: Vertragsbestaetigung mit PDF. Ein Mailfehler bricht den Wechsel nicht ab. */
/**
* K12 an den Kunden: Vertragsbestaetigung mit PDF der zugestimmten Fassung. Ein Mailfehler bricht den Wechsel nicht ab.
* Fehlt das PDF der Fassung 6, geht K12 ohne Anhang und ohne die Behauptung "im Anhang" raus (Block ohne_anhang, F5); die Rueckgabe
* { anhangFehlt } sagt es dem Aufrufer fuer T12 (Block anhang_fehlt). Ohne Datei der alten Fassungen bleibt es beim bisherigen Verhalten.
*/
async function sendSwitchConfirmation(contact, consent, facts, blocks) {
if (!contact.email) {
console.error('[wechsel] K12 nicht verschickt: keine E-Mail-Adresse zum Kunden')
return
return { anhangFehlt: false }
}
const attachments = await contractPdfAttachment(consent.termsVersion)
const { attachments, fehlt } = await ladeVertragsAnhang(consent.termsVersion, 'wechsel')
await sendCustomerMail(contact.email, {
template: 'K12',
values: {
@@ -304,9 +298,10 @@ async function sendSwitchConfirmation(contact, consent, facts, blocks) {
bedingungen_version: consent.termsVersion,
zustimmung_zeitpunkt: fmtDateTime(consent.consentAt),
},
blocks,
blocks: { ...blocks, anhang: !fehlt, ohne_anhang: fehlt },
attachments,
}).catch((err) => console.error(`[wechsel] K12 an ${contact.email} fehlgeschlagen:`, err.message))
return { anhangFehlt: fehlt }
}
/** T12 ans Team (sendAdminNotice protokolliert Fehler selbst und wirft nie). */
@@ -468,10 +463,10 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
const facts = { switchedAt, amountDueCents, monthlyCents }
const lastschrift = sepa && effective && !paid
// K12 sofort; zahlung_offen nur bei zurueckgehaltener Aenderung (Karte), lastschrift bei SEPA (F37); sofortverlangen nur mit Zeitstempel (kein Betrieb in Fassung 6)
await sendSwitchConfirmation(contact, consent, facts, { zahlung_offen: pending, lastschrift, zusatzspeicher: ctx.storageUnits > 0, sofortverlangen: Boolean(consent.earlyStartConsentAt) })
const { anhangFehlt: ohneAnhang } = await sendSwitchConfirmation(contact, consent, facts, { zahlung_offen: pending, lastschrift, zusatzspeicher: ctx.storageUnits > 0, sofortverlangen: Boolean(consent.earlyStartConsentAt) })
// T12 erst, wenn der Wechsel wirksam ist - ohne Nachweis sofort, damit das Team ihn von Hand verfolgt
if (effective || !event) {
await sendSwitchNotice(contact, doc.$id, facts, { wirksam: effective, lastschrift, nachweis_fehlt: !event })
await sendSwitchNotice(contact, doc.$id, facts, { wirksam: effective, lastschrift, nachweis_fehlt: !event, anhang_fehlt: ohneAnhang })
}
return { subscription, paymentUrl, event }
}
@@ -544,7 +539,8 @@ async function completeSwitch(eventId, contact, doneBy = 'stripe') {
doneBy,
details: { ...(wasEffective ? {} : { effectiveAt: now }), paidAt: now },
})
if (!wasEffective) await sendSwitchNotice(contact, event.subscriptionId, factsOf(event), { wirksam: true })
// Das T12 "wirksam" kommt hier erst nach K12: fehlen die PDFs der Fassung 6 noch, sagt es auch dieses T12 dem Team (F5)
if (!wasEffective) await sendSwitchNotice(contact, event.subscriptionId, factsOf(event), { wirksam: true, anhang_fehlt: await anhangFehlt(event.termsVersion) })
return { completed: true, eventId }
})
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Vertragsunterlagen (PDF) als Mail-Anhang der Bestaetigungen K01 (Abo), K12 (Wechsel) und K13 (Zusatzspeicher-Plus), mit der Auskunft, ob sie fehlen.
*
* Fassung 6 (config.abo.termsVersionNeu) liefert die PDFs erst nach der Freigabe (build-legal-pdfs.sh abo6); bis dahin fehlt die Datei. Dann darf die
* Kundenmail nicht behaupten, die Unterlagen haengten an (Block ohne_anhang statt anhang), und das Team bekommt in T02, T12 bzw. T13 den Block
* anhang_fehlt und sendet sie von Hand nach (Muster K38/T24). Alle aelteren Fassungen bleiben, wie sie waren: ohne Datei geht die Mail ohne Anhang
* raus, Vorlage und Text unveraendert (fehlt bleibt false).
*/
import { access, readFile } from 'node:fs/promises'
import { contractAttachmentFor } from './products.js'
import { istNeueFassung } from './vertragsNachweis.js'
/**
* @param {string} termsVersion zugestimmte Fassung (die Fassung der Zustimmung, nicht die des Abos)
* @param {string} tag Kennung fuer das Log, z. B. 'abo'
* @returns {Promise<{ attachments: Array<{ filename: string, content: Buffer, contentType: string }>, fehlt: boolean }>}
* fehlt: nur bei Fassung 6 und nicht lesbarer Datei
*/
export async function ladeVertragsAnhang(termsVersion, tag) {
const { filename, path } = contractAttachmentFor(termsVersion)
const content = await readFile(path).catch((err) => {
console.error(`[${tag}] Vertragsunterlagen ${filename} nicht lesbar:`, err.message)
return null
})
return { attachments: content ? [{ filename, content, contentType: 'application/pdf' }] : [], fehlt: !content && istNeueFassung(termsVersion) }
}
/** Fehlen die PDFs der Fassung 6 (ohne Log, ohne Lesen der Datei)? Für ein später gesendetes T12; ältere Fassungen: nie. */
export async function anhangFehlt(termsVersion) {
if (!istNeueFassung(termsVersion)) return false
const { path } = contractAttachmentFor(termsVersion)
return access(path).then(() => false, () => true)
}
+237
View File
@@ -0,0 +1,237 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { mailSink } from './support/mailSink.js'
// Abschlussprüfung F5: Fassung 6 ohne PDF. Fehlt der Anhang, behaupten K01, K12 und K13 kein „im Anhang“, und das Team bekommt in T02, T12 bzw. T13
// den Block anhang_fehlt (Muster K38/T24). Alte Fassungen bleiben unverändert (Hash-Pins auf die Wiedergabe der alten Vorlagen).
const aw = await startAppwriteMock({
schema: {
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
hourCredits: schemaKeys('hourCredits'),
featureConsents: schemaKeys('featureConsents'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
Object.assign(process.env, { KUNDENANSICHT_NEU: 'pilot', KUNDENANSICHT_NEU_PILOT_EMAILS: 'K@example.invalid, k3@example.invalid' })
const { config } = await import('../server/config.js')
const { startTestApp, postWebhook } = await import('./support/testApp.js')
const { hostingDoc, consent, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
const { default: aboRoutes } = await import('../server/routes/abo.js')
const { switchToWartung } = await import('../server/services/planSwitch.js')
const { changeStorage } = await import('../server/services/mailStorage.js')
const { listContractEvents } = await import('../server/services/contractEvents.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { loadTemplate, renderMail } = await import('../server/services/mailTemplates.js')
const app = await startTestApp({ '/api/abo': aboRoutes })
const mails = mailSink()
setMailTransportForTests(mails)
after(() => {
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
seedFixtures(aw)
const price = (id, lookup_key, unit_amount) => ({ id, object: 'price', active: true, lookup_key, unit_amount, recurring: { interval: 'month' } })
st.prices.set('price_hosting', price('price_hosting', config.products.hosting.lookupKey, config.products.hosting.amountCents))
st.prices.set('price_wartung', price('price_wartung', config.products.wartung.lookupKey, config.products.wartung.amountCents))
st.prices.set('price_mailStorage', price('price_mailStorage', config.mailStorage.lookupKey, config.mailStorage.amountCents))
const NEU = config.abo.termsVersionNeu
const MIT_PDF = '2099-01-01-v4' // eine Fassung, deren PDF im Repo liegt (Archiv v4), um „Fassung 6 mit PDF“ nachzustellen
const nowUnix = () => Math.floor(Date.now() / 1000)
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
const sha = (html) => createHash('sha256').update(html).digest('hex')
const HINWEIS = 'Unterlagen von Hand nachsenden'
/** Fassung 6 ist die neue Fassung dieses Laufs: mit termsVersionNeu auf eine Fassung mit PDF zeigt die Neu-Fassung eine vorhandene Datei. */
async function mitNeuFassung(fassung, fn) {
const vorher = config.abo.termsVersionNeu
config.abo.termsVersionNeu = fassung
try {
return await fn()
} finally {
config.abo.termsVersionNeu = vorher
}
}
// ---------------------------------------------------------------- Vorlagen
test('Vorlagen: K01, K12 und K13 kennen anhang und ohne_anhang, T02, T12 und T13 den Block anhang_fehlt', () => {
for (const id of ['K01', 'K12', 'K13']) assert.deepEqual(['anhang', 'ohne_anhang'].filter((b) => !loadTemplate(id).blocks.includes(b)), [], id)
for (const id of ['T02', 'T12', 'T13']) assert.ok(loadTemplate(id).blocks.includes('anhang_fehlt'), id)
// ohne Anhang keine Behauptung „im Anhang“ / „beigefügt“, dafür der Satz, dass die Unterlagen gesondert kommen
const K13_BESTELLUNG = { plus: true, bestellung: true }
for (const [id, blocks] of [['K01', { wartung: true, sofortbeginn: true }], ['K12', {}], ['K13', K13_BESTELLUNG]]) {
const ohne = renderMail(id, sampleValues(loadTemplate(id)), { ...blocks, ohne_anhang: true })
assert.doesNotMatch(ohne.text, /Anhang|beigefügt/i, `${id} ohne Anhang`)
assert.match(ohne.text, /senden wir Ihnen gesondert zu/, id)
assert.deepEqual(ohne.problems, [], id)
const mit = renderMail(id, sampleValues(loadTemplate(id)), { ...blocks, anhang: true })
assert.match(mit.text, /im Anhang|beigefügten/, `${id} mit Anhang`)
assert.doesNotMatch(mit.text, /gesondert zu/, id)
}
for (const id of ['T02', 'T12', 'T13']) {
const mit = renderMail(id, sampleValues(loadTemplate(id)), { anhang_fehlt: true })
assert.match(mit.text, new RegExp(HINWEIS), id)
assert.match(mit.text, /nicht mit der Bestätigung beim Kunden angekommen/, id)
assert.doesNotMatch(renderMail(id, sampleValues(loadTemplate(id)), {}).text, new RegExp(HINWEIS), id)
}
})
// Hash der Wiedergabe der Vorlagen vor der Änderung (Stand kn-p7 plus P8-Nachträge, ohne anhang-Blöcke): die alten Fassungen bleiben bytegleich
const ALT = [
['K01', {"hosting": true, "sofortbeginn": true}, 'ea5fed3076d4bf6c4f5aa7042d20d61c1d90d11716f99811e439df09c95abd56'],
['K01', {"wartung": true, "beginn_nach_widerrufsfrist": true, "angebot": true}, '434d4945815af9e8df98039184e09058b992c32f1f7a21844b25923be2130aca'],
['K01', {"wartung_v3": true, "sofortbeginn": true}, '4a5a3252ad7ea5c5eda35b42480f95611a4cb5648e9a847afc6720692240d729'],
['K12', {"zahlung_offen": true, "zusatzspeicher": true, "sofortverlangen": true}, '8c18e9a2455e28ec5467fb68dcdfe9b66afe9cc676ef16cc84281a4b2d97caf2'],
['K12', {"lastschrift": true}, '9731b26f4043f29537e289edf7b00d4d024522c6bafb9166d3a0b6289e592963'],
['K12', {}, '76a6f5f1f350f80a938828ce2cd1d84305aca2251427e2ad0df7bb550dc18ff7'],
['K13', {"plus": true, "bestellung": true}, 'b01d4f6a30f562ccee3739f8f5b77bb9a660a61165366cd3d294a627ccfdc8c8'],
['K13', {"plus_bezahlt": true, "bestellung": true}, '7a41e74fe6bf45a33bb6e9dd35d29f6995b8995961e3b6905ed5ba45e19b0e51'],
['K13', {"minus": true}, 'a82497427b0d7af96aadd380974ce5e73930ec2c9a7f5b6357f5f13f8190d109'],
['T02', {"angebot": true, "angebot_fehler": true, "nachweis_fehlt": true}, '438a623fb53046b34930a30110fa35d3c08c6f27ff84af21466426496202a441'],
['T02', {}, '386199538e7a59eb09b41511f4cab3924c36cacdbbdba281af45b2fa86158755'],
['T12', {"wirksam": true, "nachweis_fehlt": true}, '3777c2edc646e53b5b63280132e0958e31dba5d084848244525882a9b2fb1d07'],
['T12', {"lastschrift": true, "gescheitert": true}, '56e1c70e82d260b6c62788fbed5503d2e4e6c6fff1046281b7e76261ced5f1c9'],
['T12', {}, '928f6ee5118da82fce612d77f6dab22be4c3b0aefc736e64668a9d24b69da98a'],
['T13', {"einrichten": true, "nachweis_fehlt": true}, '2f5543fec1ecdc7726931967b164d42386a9d32f0f062e8d5770002958c33a72'],
['T13', {"periodenende": true, "teilweise": true}, '44fa59476f1edb46f977500b1d8ebed7f07205ca2f6087609c2602e15f10eaeb'],
['T13', {}, '793af7c1cb98df24d914d1a5bf3330ace1751a2794713045b78a9ac2a93f1a9f'],
]
test('Alte Fassungen: die Wiedergabe der Vorlagen bleibt bytegleich (K mit anhang, T ohne anhang_fehlt)', () => {
for (const [id, blocks, soll] of ALT) {
const mitAnhang = id.startsWith('K') ? { ...blocks, anhang: true } : blocks
assert.equal(sha(renderMail(id, sampleValues(loadTemplate(id)), mitAnhang).html), soll, `${id} ${JSON.stringify(blocks)}`)
}
})
// ---------------------------------------------------------------- K01 und T02
const SUB_NEW = 'sub_1SD6vPRxAbCdEfGhIjKlMnOp'
const checkoutCompleted = (subscriptionId, metadata = {}) => ({
id: `evt_${subscriptionId}`, object: 'event', type: 'checkout.session.completed', created: nowUnix(),
data: { object: { id: `cs_${subscriptionId}`, object: 'checkout.session', mode: 'subscription', subscription: subscriptionId, customer: 'cus_c3',
customer_details: { email: 'k3@example.invalid', name: 'Kunde Drei' }, metadata: { webklarCustomerId: 'c3', productKey: 'wartung', ...metadata } } },
})
async function clearEvents(customerId) {
for (const e of await listContractEvents(customerId)) await deleteDocument(config.collections.contractEvents, e.$id)
}
async function k01(termsVersion) {
st.subscriptions.set(SUB_NEW, stripeSubscription({ id: SUB_NEW, customerId: 'c3', productKey: 'wartung', metadata: { termsVersion, startChoice: 'sofort', earlyStartConsentAt: new Date().toISOString() } }))
mails.sent.length = 0
try {
assert.equal((await postWebhook(checkoutCompleted(SUB_NEW))).status, 200)
return { kunde: mails.sent.find((m) => m.subject.includes('Vertragsbestätigung')), team: mails.sent.find((m) => m.subject.startsWith('[Abo] Neues')) }
} finally {
await clearEvents('c3')
await deleteDocument(config.collections.subscriptions, SUB_NEW).catch(() => {})
}
}
test('K01: Fassung 6 ohne PDF behauptet keinen Anhang, T02 sagt es dem Team', async () => {
const { kunde, team } = await k01(NEU)
assert.equal(kunde.attachments.length, 0)
assert.doesNotMatch(kunde.text, /Anhang|beigefügt/i)
assert.match(kunde.text, /senden wir Ihnen gesondert zu/)
assert.match(team.text, new RegExp(HINWEIS))
})
test('K01: Fassung 6 mit PDF und alte Fassung wie bisher (Anhang, kein Hinweis)', async () => {
for (const [name, fassung, neu] of [['Fassung 6 mit PDF', MIT_PDF, MIT_PDF], ['Fassung 5', config.abo.termsVersion, NEU]]) {
const { kunde, team } = await mitNeuFassung(neu, () => k01(fassung))
assert.equal(kunde.attachments.length, 1, name)
assert.match(kunde.text, /im Anhang/, name)
assert.doesNotMatch(kunde.text, /gesondert zu/, name)
assert.doesNotMatch(team.text, new RegExp(HINWEIS), name)
}
})
// ---------------------------------------------------------------- K12 und T12
const CARD = { id: 'pm_card_visa', object: 'payment_method', type: 'card' }
const switchInvoice = (overrides = {}) => ({ id: 'in_wechsel', object: 'invoice', status: 'paid', amount_due: 4100, billing_reason: 'subscription_update', hosted_invoice_url: 'https://invoice.stripe.test/i/in_wechsel', ...overrides })
async function resetHosting() {
await clearEvents('c1')
aw.seed('subscriptions', { ...hostingDoc })
st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting', default_payment_method: CARD }))
st.nextSubscriptionUpdate = null
mails.sent.length = 0
}
async function k12(termsVersion) {
await resetHosting()
st.nextSubscriptionUpdate = { latest_invoice: switchInvoice() }
await switchToWartung('c1', { ...consent, termsVersion }, nowUnix())
return { kunde: mails.sent.find((m) => m.subject === 'Ihr Wechsel ins WEBklar Wartungs-Abo – Bestätigung'), team: mails.sent.find((m) => m.subject.startsWith('[Abo] Wechsel ins Wartungs-Abo')) }
}
test('K12: Fassung 6 ohne PDF behauptet keinen Anhang, T12 sagt es dem Team; mit PDF und alte Fassung wie bisher', async () => {
const ohne = await k12(NEU)
assert.equal(ohne.kunde.attachments.length, 0)
assert.doesNotMatch(ohne.kunde.text, /Anhang|beigefügt/i)
assert.match(ohne.kunde.text, /senden wir Ihnen gesondert zu/)
assert.match(ohne.team.text, new RegExp(HINWEIS))
for (const [name, fassung, neu] of [['Fassung 6 mit PDF', MIT_PDF, MIT_PDF], ['Fassung 5', config.abo.termsVersion, NEU]]) {
const r = await mitNeuFassung(neu, () => k12(fassung))
assert.equal(r.kunde.attachments.length, 1, name)
assert.match(r.kunde.text, /im Anhang/, name)
assert.doesNotMatch(r.team.text, new RegExp(HINWEIS), name)
}
})
// ---------------------------------------------------------------- K13 und T13
async function resetSubH({ units, paidUnits }) {
await clearEvents('c1')
mails.sent.length = 0
aw.seed('subscriptions', { ...hostingDoc, storageUnits: units, storagePaidUnits: paidUnits, storagePaidPeriodEnd: hostingDoc.currentPeriodEnd })
st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting', storageUnits: units }))
}
async function k13({ units, paidUnits, ziel, termsVersion }) {
await resetSubH({ units, paidUnits })
await changeStorage('c1', ziel, { ...consent, termsVersion })
return { kunde: mails.sent.find((m) => m.subject === 'Ihr E-Mail-Zusatzspeicher – Bestätigung'), team: mails.sent.find((m) => m.subject.startsWith('[Speicher]')) }
}
test('K13: Fassung 6 ohne PDF behauptet keinen Anhang, T13 sagt es dem Team (auch ohne Aufgabe); mit PDF und alte Fassung wie bisher', async () => {
const ohne = await k13({ units: 0, paidUnits: 0, ziel: 2, termsVersion: NEU })
assert.equal(ohne.kunde.attachments.length, 0)
assert.doesNotMatch(ohne.kunde.text, /Anhang|beigefügt/i)
assert.match(ohne.kunde.text, /senden wir Ihnen gesondert zu/)
assert.match(ohne.team.text, new RegExp(HINWEIS))
// Plus nur bis zur schon bezahlten Menge: keine Aufgabe, aber die Unterlagen müssen nachgesendet werden
const bezahlt = await k13({ units: 1, paidUnits: 3, ziel: 3, termsVersion: NEU })
assert.equal(bezahlt.kunde.attachments.length, 0)
assert.ok(bezahlt.team, 'T13 geht auch ohne Aufgabe raus, wenn der Anhang fehlt')
assert.match(bezahlt.team.text, new RegExp(HINWEIS))
for (const [name, fassung, neu] of [['Fassung 6 mit PDF', MIT_PDF, MIT_PDF], ['Fassung 5', config.abo.termsVersion, NEU]]) {
const r = await mitNeuFassung(neu, () => k13({ units: 0, paidUnits: 0, ziel: 2, termsVersion: fassung }))
assert.equal(r.kunde.attachments.length, 1, name)
assert.match(r.kunde.text, /beigefügten Vertragsunterlagen/, name)
assert.doesNotMatch(r.team.text, new RegExp(HINWEIS), name)
}
// Verringerung: nie ein Anhang, nie ein Hinweis
const minus = await k13({ units: 3, paidUnits: 3, ziel: 1, termsVersion: NEU })
assert.equal(minus.kunde.attachments.length, 0)
assert.doesNotMatch(minus.team.text, new RegExp(HINWEIS))
})
// Karte mit Bestätigung bei der Bank: K12 geht beim Klick, T12 "wirksam" erst mit pending_update_applied; auch dieses T12 sagt, dass die PDFs fehlen
test('T12 beim späteren Wirksamwerden (Karte, pending_update_applied) trägt anhang_fehlt, solange die PDFs der Fassung 6 fehlen', async () => {
await resetHosting()
const nowU = nowUnix()
st.nextSubscriptionUpdate = {
pending_update: { expires_at: nowU + 23 * 3600, billing_cycle_anchor: nowU, subscription_items: [], trial_end: null, trial_from_plan: null },
latest_invoice: switchInvoice({ status: 'open', hosted_invoice_url: 'https://pay.example/karte' }),
}
await switchToWartung('c1', { ...consent, termsVersion: NEU }, nowU)
assert.ok(mails.sent.find((m) => m.subject === 'Ihr Wechsel ins WEBklar Wartungs-Abo – Bestätigung'), 'K12 ging beim Klick raus')
assert.equal(mails.sent.filter((m) => m.subject.startsWith('[Abo] Wechsel ins Wartungs-Abo')).length, 0, 'T12 wartet auf die Zahlung')
st.applyPendingUpdate('sub_h')
const res = await postWebhook({ id: 'evt_pua', object: 'event', type: 'customer.subscription.pending_update_applied', created: nowU, data: { object: { id: 'sub_h', object: 'subscription' } } })
assert.equal(res.status, 200, JSON.stringify(res.body))
const t12 = mails.sent.filter((m) => m.subject.startsWith('[Abo] Wechsel ins Wartungs-Abo'))
assert.equal(t12.length, 1)
assert.match(t12[0].text, new RegExp(HINWEIS))
})
+8 -8
View File
@@ -260,11 +260,11 @@ test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden <br>', () => {
test('optionale Blöcke: nur eingeschaltete erscheinen', () => {
const values = { name: 'Max', produkt: 'Wartungs-Abo', preis: '50,00 € / Monat', angebot_text: 'Erste 2 Monate je 25,00 €', vertragsschluss: '01.10.2026', leistungsbeginn: '15.10.2026', mindestlaufzeit_bis: '15.10.2027', bedingungen_version: 'v3', zustimmung_zeitpunkt: '01.10.2026, 10:00' }
const early = renderMail('K01', values, { angebot: false, sofortbeginn: true, beginn_nach_widerrufsfrist: false })
const early = renderMail('K01', values, { angebot: false, sofortbeginn: true, beginn_nach_widerrufsfrist: false, anhang: true })
assert.doesNotMatch(early.html, /Ihr Angebot:/)
assert.match(early.html, /Sie haben ausdrücklich verlangt/)
assert.doesNotMatch(early.html, /erst nach Ablauf der Widerrufsfrist/)
const late = renderMail('K01', values, { angebot: true, sofortbeginn: false, beginn_nach_widerrufsfrist: true })
const late = renderMail('K01', values, { angebot: true, sofortbeginn: false, beginn_nach_widerrufsfrist: true, anhang: true })
assert.match(late.html, /Ihr Angebot:<\/strong> Erste 2 Monate je 25,00 € \(wie vereinbart\)/)
assert.doesNotMatch(late.html, /Sie haben ausdrücklich verlangt/)
assert.match(late.html, /Leistungsbeginn und erste Abbuchung: 15\.10\.2026\./)
@@ -298,7 +298,7 @@ test('Textfassung K01: alle Vertragsangaben lesbar', () => {
const mail = renderMail(
'K01',
{ name: 'Erika Musterfrau', produkt: 'Wartungs-Abo', preis: '50,00 € / Monat', angebot_text: '', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026', mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-09-25-v3', zustimmung_zeitpunkt: '01.10.2026, 10:07' },
{ sofortbeginn: true, wartung: true }
{ sofortbeginn: true, wartung: true, anhang: true }
)
assert.deepEqual(mail.problems, [])
const t = mail.text
@@ -373,7 +373,7 @@ test('K12: Block zahlung_offen wörtlich, Block lastschrift mit dem Satz aus §
assert.match(sepa.text, /Wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt/)
assert.match(sepa.text, /Ihr Abbuchungstag bleibt der Wechseltag/)
assert.doesNotMatch(sepa.text, /sobald die Zahlung bestätigt ist/)
const plain = renderMail('K12', values, {})
const plain = renderMail('K12', values, { anhang: true }) // Vertragsunterlagen hängen an (F5: ohne PDF der Fassung 6 stattdessen ohne_anhang)
assert.doesNotMatch(plain.text, /sobald die Zahlung bestätigt ist|Wird die Lastschrift nicht eingelöst/)
// Vertragsbestaetigung: Preis mit § 19 UStG, Mindestlaufzeit, Unterlagen, Zustimmung zum Sofortbeginn
for (const text of ['gemäß § 19 UStG ohne Umsatzsteuer', 'Mindestlaufzeit 12 Monate bis Wert-mindestlaufzeit_bis.', 'Widerrufsbelehrung mit Muster-Widerrufsformular']) {
@@ -406,22 +406,22 @@ test('K13, T13: Betreff, Platzhalter und Blöcke des Zusatzspeichers', () => {
assert.deepEqual(loadTemplate('K13').placeholders, ['name', 'pakete', 'gesamt_gb', 'monatspreis_zusatz', 'wirksam_ab'])
// Fix 2 (3): plus = Einrichtung mit anteiliger Berechnung, plus_bezahlt = Plus bis zur schon bezahlten Menge,
// bestellung = Unterlagen und Widerruf (bei beiden), minus = Verringerung zum Periodenende
assert.deepEqual(loadTemplate('K13').blocks, ['plus', 'plus_bezahlt', 'bestellung', 'minus'])
assert.deepEqual(loadTemplate('K13').blocks, ['plus', 'plus_bezahlt', 'bestellung', 'minus', 'anhang', 'ohne_anhang'])
assert.equal(loadTemplate('T13').subject, '[Speicher] {{aktion}}: {{email}}')
for (const p of ['aktion', 'email', 'name', 'stripe_abo_id', 'postfaecher_hinweis', 'neue_groesse_gb', 'pakete', 'faellig']) {
assert.ok(loadTemplate('T13').placeholders.includes(p), p)
}
assert.deepEqual([...loadTemplate('T13').blocks].sort(), ['einrichten', 'nachweis_fehlt', 'periodenende', 'teilweise'])
assert.deepEqual([...loadTemplate('T13').blocks].sort(), ['anhang_fehlt', 'einrichten', 'nachweis_fehlt', 'periodenende', 'teilweise'])
const values = sampleValues(loadTemplate('K13'))
const plus = renderMail('K13', values, { plus: true, bestellung: true })
const plus = renderMail('K13', values, { plus: true, bestellung: true, anhang: true })
assert.deepEqual(plus.problems, [])
assert.match(plus.text, /gemäß § 19 UStG ohne Umsatzsteuer/)
assert.match(plus.text, /wird eingerichtet/)
assert.match(plus.text, /14 Tagen widerrufen/)
assert.match(plus.text, /Vertragsunterlagen im Anhang/)
assert.doesNotMatch(plus.text, /ohne anteilige Erstattung|keine zusätzlichen Kosten/)
const paid = renderMail('K13', values, { plus_bezahlt: true, bestellung: true })
const paid = renderMail('K13', values, { plus_bezahlt: true, bestellung: true, anhang: true })
assert.ok(paid.text.includes('Die Pakete sind für den laufenden Monat schon bezahlt; es entstehen keine zusätzlichen Kosten. Ab dem nächsten Abrechnungsmonat gelten Wert-pakete Pakete.'))
assert.match(paid.text, /14 Tagen widerrufen/)
assert.match(paid.text, /Vertragsunterlagen im Anhang/)