Mail T16 je Funktion, neue Vorlage T19 Erstattung faellig
T16 (Funktion bestellt) nennt die Funktion aus dem Register statt fest "News-Pinnwand": neue Platzhalter funktion, zustand, anleitung_url und gesundheit_text, neue Bloecke kein_abo, bereits_aktiv, anleitung (Knopf "Im Ticketsystem oeffnen" zur Einbauanleitung) und gesundheit. teamNotice und unmatchedNotice fuellen funktion aus featureConfig (ohne bekannten Schluessel "Funktion"); die neuen Bloecke kein_abo, bereits_aktiv und gesundheit bleiben aus, bis B10a und D4 sie fuellen. Die News-Mails bleiben woertlich (HTML, Text und Betreff byte-gleich). T19 (Erstattung faellig, Team-Hinweis) ist neu: volle Erstattung von Hand in Stripe, Bloecke zahlung_nach_ende, ticket_offen und rechnung. Gesendet wird sie spaeter ueber meldeErstattung (B10b). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e200efb7e5
commit
992d7358fa
5 files changed
+419
-25
No files matched your search
@@ -3,19 +3,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
T16 · Funktion bestellt (News-Pinnwand)
|
||||
Betreff: Neue Funktion bestellt: News-Pinnwand
|
||||
T16 · Funktion bestellt (je Funktion)
|
||||
Betreff: Neue Funktion bestellt: {{funktion}}
|
||||
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
|
||||
Empfänger: kenso@webklar.com, andrej@webklar.com
|
||||
Platzhalter: {{kunde_name}}, {{email}}, {{projekt}}, {{woid}}, {{sitekey}}, {{sitzung}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_fehlgeschlagen, ungeklaert
|
||||
Platzhalter: {{kunde_name}}, {{email}}, {{projekt}}, {{funktion}}, {{woid}}, {{sitekey}}, {{sitzung}}, {{zustand}}, {{anleitung_url}}, {{gesundheit_text}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_fehlgeschlagen, ungeklaert, kein_abo, bereits_aktiv, anleitung, gesundheit
|
||||
funktion = Name der Funktion aus dem Register (zum Beispiel News-Pinnwand, Terminplaner); ohne bekannte Funktion steht „Funktion“.
|
||||
ungeklaert = eine bezahlte Stripe-Sitzung passt zu keiner offenen Bestellung (Betreff und Inhalt: von Hand pruefen); sitzung nennt Sitzungs-ID, Kunde und Projekt.
|
||||
zahlung_fehlgeschlagen = die Zahlung (SEPA) ist nicht eingegangen; es wurde nichts bestellt, woid und sitekey sind dann leer.
|
||||
kein_abo = die Zahlung ist da und das Ticket angelegt, aber der Kunde hat kein laufendes Wartungs-Abo; die Freischaltung bleibt gesperrt, endet das Abo vor der Fertigstellung, ist voll zu erstatten (Mail „Erstattung fällig“, T19).
|
||||
bereits_aktiv = die Zahlung ist da, aber die Funktion war schon in dem Zustand {{zustand}}; es wurde kein Ticket angelegt und nichts eingerichtet (Einbau nachholen oder erstatten); sitzung nennt die Stripe-Sitzung.
|
||||
anleitung = Knopf „Im Ticketsystem öffnen“ zur Einbauanleitung des Projekts (anleitung_url); nur bei Funktionen mit Anleitung und angelegtem Ticket.
|
||||
gesundheit = Warnzeile „Gesundheitsbereich prüfen“ mit gesundheit_text (Hinweise zur Branche des Betriebs).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Neue Funktion bestellt: News-Pinnwand</title>
|
||||
<title>Neue Funktion bestellt: {{funktion}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@@ -24,7 +29,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;">{{kunde_name}}: News-Pinnwand, Projekt {{projekt}}. ͏ ͏ ͏</span>
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{kunde_name}}: {{funktion}}, Projekt {{projekt}}. ͏ ͏ ͏</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;">
|
||||
@@ -35,7 +40,7 @@ a{color:#111111;}
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Funktionen</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Neue Funktion bestellt: News-Pinnwand</td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Neue Funktion bestellt: {{funktion}}</td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Kunde</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kunde_name}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">E-Mail</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{email}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Ticket</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{woid}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Site-Key</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{sitekey}}</td></tr></table></td></tr>
|
||||
<!-- WENN zahlung_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;">Zahlung fehlgeschlagen</div><div class="mut" style="color:#4a4843;">Die Zahlung ist nicht eingegangen (zum Beispiel SEPA-Lastschrift abgelehnt). Es wurde nichts eingerichtet und kein Ticket angelegt; die Bestellung steht im Portal wieder auf „nicht bestellt“.</div></td></tr></table></td></tr>
|
||||
@@ -43,6 +48,18 @@ a{color:#111111;}
|
||||
<!-- WENN ungeklaert -->
|
||||
<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;">Bezahlt, aber nicht zuzuordnen</div><div class="mut" style="color:#4a4843;">Der Kunde hat bezahlt, die Zahlung passt aber zu keiner offenen Bestellung. Es wurde nichts eingerichtet und kein Ticket angelegt. Bitte von Hand prüfen: {{sitzung}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE ungeklaert -->
|
||||
<!-- WENN kein_abo -->
|
||||
<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;">Kein laufendes Wartungs-Abo</div><div class="mut" style="color:#4a4843;">Die Zahlung ist eingegangen und das Ticket {{woid}} ist angelegt, aber der Kunde hat kein laufendes Wartungs-Abo. Die Freischaltung bleibt gesperrt. Endet das Abo vor der Fertigstellung, wird die Einrichtung voll erstattet (Mail „Erstattung fällig“).</div></td></tr></table></td></tr>
|
||||
<!-- ENDE kein_abo -->
|
||||
<!-- WENN bereits_aktiv -->
|
||||
<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;">Bezahlt, aber schon vorhanden</div><div class="mut" style="color:#4a4843;">Zahlung eingegangen, aber die Funktion war schon {{zustand}}. Es wurde kein Ticket angelegt und nichts eingerichtet. Bitte prüfen: Einbau nachholen oder erstatten. Sitzung: {{sitzung}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE bereits_aktiv -->
|
||||
<!-- WENN gesundheit -->
|
||||
<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:16px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;">Gesundheitsbereich prüfen: {{gesundheit_text}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE gesundheit -->
|
||||
<!-- WENN anleitung -->
|
||||
<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="{{anleitung_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Im Ticketsystem öffnen →</a></td></tr></table></td></tr>
|
||||
<!-- ENDE anleitung -->
|
||||
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;"> </td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
T19 · Erstattung fällig
|
||||
Betreff: [Abo] Erstattung fällig: {{funktion}}-Einrichtung
|
||||
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
|
||||
Empfänger: kenso@webklar.com, andrej@webklar.com
|
||||
Platzhalter: {{kunde_name}}, {{email}}, {{projekt}}, {{funktion}}, {{betrag}}, {{sitzung}}, {{woid}}, {{rechnung_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_nach_ende, ticket_offen, rechnung
|
||||
Der Kunde hat die Einrichtung einer Funktion bezahlt (betrag, sitzung), sein Wartungs-Abo lief aber nicht mehr bzw. endete vor der Freischaltung: volle Erstattung von Hand in Stripe, keine automatische Rückzahlung.
|
||||
zahlung_nach_ende = die Zahlung kam erst nach dem Abo-Ende an; es wurde kein Ticket angelegt (woid ist leer).
|
||||
ticket_offen = das Ticket woid ist noch offen: mit Vermerk schließen (die Freischaltung beim Schließen schaltet dann nicht frei, weil die Funktion abgeschaltet ist).
|
||||
rechnung = Knopf „Rechnung ansehen“ zur Stripe-Rechnung (rechnung_url).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>[Abo] Erstattung fällig: {{funktion}}-Einrichtung</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{kunde_name}}: {{betrag}} für die {{funktion}}-Einrichtung erstatten. ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Intern · Kundenportal</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Erstattung</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Erstattung fällig: {{funktion}}-Einrichtung</td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Kunde</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kunde_name}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">E-Mail</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{email}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Betrag</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{betrag}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Sitzung</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{sitzung}}</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;">Volle Erstattung von Hand</div><div class="mut" style="color:#4a4843;">Der Kunde hat die Einrichtung bezahlt, sein Wartungs-Abo lief aber nicht mehr bzw. endete vor der Freischaltung. Die Einrichtung wird deshalb voll erstattet: bitte von Hand in Stripe zurückzahlen. Eine automatische Rückzahlung gibt es nicht.</div></td></tr></table></td></tr>
|
||||
<!-- WENN zahlung_nach_ende -->
|
||||
<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;">Zahlung nach dem Abo-Ende</div><div class="mut" style="color:#4a4843;">Die Zahlung ist erst nach dem Abo-Ende eingegangen, es wurde kein Ticket angelegt.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE zahlung_nach_ende -->
|
||||
<!-- WENN ticket_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;">Ticket noch offen</div><div class="mut" style="color:#4a4843;">Das Ticket {{woid}} ist noch offen. Bitte das Ticket mit einem Vermerk schließen; die Freischaltung beim Schließen schaltet dann nicht frei, weil die Funktion abgeschaltet ist.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE ticket_offen -->
|
||||
<!-- WENN rechnung -->
|
||||
<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="{{rechnung_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Rechnung ansehen →</a></td></tr></table></td></tr>
|
||||
<!-- ENDE rechnung -->
|
||||
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;"> </td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https://ticket.webklar.com/customers" style="color:#4a4843;text-decoration:underline;">Ticketsystem</a></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@ import { createDocument, getDocument, listDocuments, updateDocument, Query } fro
|
||||
import { withLocks } from './keyedLock.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
|
||||
import { featureConfig, featureKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { anleitungsLink, featureConfig, featureKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { zustimmungFuerSitzung } from './featureConsents.js'
|
||||
import { createFeatureTicket } from './ticketAdmin.js'
|
||||
|
||||
@@ -572,37 +572,63 @@ async function ticketWoidOf(doc) {
|
||||
return doc.ticketId ? (await ticketWoid(doc.ticketId)) || '' : ''
|
||||
}
|
||||
|
||||
async function teamNotice(doc, customer, { failed = false } = {}) {
|
||||
await sendAdminNotice({
|
||||
/**
|
||||
* T16 an das Team (Funktion bestellt): alle Platzhalter und Bloecke der Vorlage; was nicht genannt wird, bleibt leer bzw. aus.
|
||||
* Ohne bekannte Funktion heisst sie in Betreff und Text "Funktion". kein_abo, bereits_aktiv und gesundheit sind in der Vorlage
|
||||
* deklariert und bleiben hier aus (B10a und D4 fuellen sie).
|
||||
*/
|
||||
function sendT16({ funktion, ...werte }, bloecke) {
|
||||
return sendAdminNotice({
|
||||
template: 'T16',
|
||||
values: {
|
||||
kunde_name: '',
|
||||
email: '',
|
||||
projekt: '',
|
||||
funktion: funktion || 'Funktion',
|
||||
woid: '',
|
||||
sitekey: '',
|
||||
sitzung: '',
|
||||
zustand: '',
|
||||
anleitung_url: '',
|
||||
gesundheit_text: '',
|
||||
...werte,
|
||||
},
|
||||
blocks: { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false, ...bloecke },
|
||||
})
|
||||
}
|
||||
|
||||
async function teamNotice(doc, customer, { failed = false } = {}) {
|
||||
const mails = featureConfig(doc.featureKey)?.mails
|
||||
// Knopf zur Einbauanleitung nur bei Funktionen mit Anleitung und nur, wenn bestellt und ein Ticket angelegt wurde
|
||||
const anleitung = Boolean(mails?.anleitung) && !failed
|
||||
await sendT16(
|
||||
{
|
||||
kunde_name: customer?.name || customer?.email || doc.customerId,
|
||||
email: customer?.email || '',
|
||||
projekt: await projectName(doc.projectId),
|
||||
funktion: mails?.funktion,
|
||||
woid: failed ? '' : await ticketWoidOf(doc),
|
||||
sitekey: failed ? '' : doc.siteKey || '',
|
||||
sitzung: '',
|
||||
anleitung_url: anleitung ? anleitungsLink(doc.customerId, doc.projectId) : '',
|
||||
},
|
||||
blocks: { zahlung_fehlgeschlagen: failed, ungeklaert: false },
|
||||
})
|
||||
{ zahlung_fehlgeschlagen: failed, anleitung }
|
||||
)
|
||||
}
|
||||
|
||||
/** Bezahlte Sitzung ohne passende Bestellung: das Team muss von Hand klaeren (T16, Block ungeklaert). */
|
||||
async function unmatchedNotice(session) {
|
||||
const { customerId, projectId } = session.metadata || {}
|
||||
const { customerId, projectId, featureKey } = session.metadata || {}
|
||||
const customer = customerId ? await customerOf(customerId).catch(() => null) : null
|
||||
await sendAdminNotice({
|
||||
template: 'T16',
|
||||
values: {
|
||||
await sendT16(
|
||||
{
|
||||
kunde_name: customer?.name || customer?.email || customerId || 'unbekannt',
|
||||
email: customer?.email || '',
|
||||
projekt: projectId ? (await projectName(projectId).catch(() => '')) || projectId : '',
|
||||
woid: '',
|
||||
sitekey: '',
|
||||
funktion: featureConfig(featureKey)?.mails.funktion,
|
||||
sitzung: `${session.id} (Kunde ${customerId || '-'}, Projekt ${projectId || '-'})`,
|
||||
},
|
||||
blocks: { zahlung_fehlgeschlagen: false, ungeklaert: true },
|
||||
})
|
||||
{ ungeklaert: true }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+275
-1
@@ -7,7 +7,7 @@ import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Mails K15 (Bestellung eingegangen) und K16 (Funktion bereit) je Funktion (Task B9a). Der News-Wortlaut ist als Literal
|
||||
// eingefroren (vor dem Umbau der Vorlagen aus den damaligen Mails uebernommen): "News-Mails bleiben woertlich".
|
||||
// B9b ergaenzt T16 und T19 in dieser Datei. websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema.
|
||||
// B9b ergaenzt T16 (Team-Hinweis je Funktion) und T19 (Erstattung faellig) in dieser Datei. websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema.
|
||||
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } })
|
||||
const st = await startFakeStripe()
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
@@ -80,6 +80,51 @@ async function bezahle(key, projectName, extra = {}) {
|
||||
return { projectId, sessionId, mail: k15[0] }
|
||||
}
|
||||
|
||||
const failedEvent = (sessionId, projectId, key) => ({
|
||||
id: `evt_f_${sessionId}`,
|
||||
object: 'event',
|
||||
type: 'checkout.session.async_payment_failed',
|
||||
data: {
|
||||
object: { id: sessionId, object: 'checkout.session', mode: 'payment', payment_status: 'unpaid', metadata: { customerId: 'c1', projectId, featureKey: key } },
|
||||
},
|
||||
})
|
||||
|
||||
/** Die T16-Mails (Team-Hinweis "Neue Funktion bestellt") ab einer Position von mails.sent. */
|
||||
const t16Seit = (vor) => mails.sent.slice(vor).filter((m) => m.subject.startsWith('Neue Funktion bestellt'))
|
||||
|
||||
/** Bestellung bezahlen: die eine T16-Mail an das Team, dazu Projekt, Ticketnummer und Site-Key aus dem Dokument. */
|
||||
async function t16Bezahlt(key, projectName) {
|
||||
const { projectId, sessionId } = await bestelle(key, projectName)
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, key))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||||
const doc = await sf.getFeatureDoc('c1', projectId, key)
|
||||
const woid = aw.docs('workorders').get(doc.ticketId).woid
|
||||
return { projectId, sessionId, doc, woid, mail: t16[0] }
|
||||
}
|
||||
|
||||
/** Zahlung fehlgeschlagen (SEPA): bestellen, dann async_payment_failed. */
|
||||
async function t16Fehlgeschlagen(key, projectName) {
|
||||
const { projectId, sessionId } = await bestelle(key, projectName)
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(failedEvent(sessionId, projectId, key))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||||
return { projectId, mail: t16[0] }
|
||||
}
|
||||
|
||||
/** Bezahlte Sitzung ohne Bestellung zum Projekt (Schlüssel der Funktion wie in den Metadaten, auch fremd oder leer): T16 mit Block "ungeklaert". */
|
||||
async function t16Ungeklaert(sessionId, projectId, key) {
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, key))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||||
return t16[0]
|
||||
}
|
||||
|
||||
const T16_FUSS = 'Automatische Benachrichtigung aus dem WEBklar Kundenportal · Ticketsystem (https://ticket.webklar.com/customers)'
|
||||
|
||||
// ------------------------------------------------------------------------- News bleibt woertlich ---
|
||||
test('News-Mails bleiben wörtlich: K15 mit Rechnung', async () => {
|
||||
const { mail } = await bezahle('news', 'Bäckerei', { invoice: 'in_1' })
|
||||
@@ -185,6 +230,91 @@ test('News-Mails bleiben wörtlich: K16', async () => {
|
||||
assert.deepEqual(mail.to.map((t) => t.address), ['kunde1@example.test'])
|
||||
})
|
||||
|
||||
test('News-Mails bleiben wörtlich: T16', async () => {
|
||||
const { mail, woid, doc } = await t16Bezahlt('news', 'Bäckerei')
|
||||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||||
assert.equal(
|
||||
mail.text,
|
||||
[
|
||||
'WEBklar',
|
||||
'',
|
||||
'Intern · Kundenportal',
|
||||
'',
|
||||
'Funktionen',
|
||||
'',
|
||||
'Neue Funktion bestellt: News-Pinnwand',
|
||||
'',
|
||||
'Kunde: Kunde Eins',
|
||||
'E-Mail: kunde1@example.test',
|
||||
'Projekt: Bäckerei',
|
||||
`Ticket: ${woid}`,
|
||||
`Site-Key: ${doc.siteKey}`,
|
||||
'',
|
||||
T16_FUSS,
|
||||
'',
|
||||
].join('\n')
|
||||
)
|
||||
assert.deepEqual(mail.to.map((t) => t.address), ['kenso@webklar.com', 'andrej@webklar.com'])
|
||||
})
|
||||
|
||||
test('News-Mails bleiben wörtlich: T16 Zahlung fehlgeschlagen', async () => {
|
||||
const { mail } = await t16Fehlgeschlagen('news', 'Bäckerei')
|
||||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||||
assert.equal(
|
||||
mail.text,
|
||||
[
|
||||
'WEBklar',
|
||||
'',
|
||||
'Intern · Kundenportal',
|
||||
'',
|
||||
'Funktionen',
|
||||
'',
|
||||
'Neue Funktion bestellt: News-Pinnwand',
|
||||
'',
|
||||
'Kunde: Kunde Eins',
|
||||
'E-Mail: kunde1@example.test',
|
||||
'Projekt: Bäckerei',
|
||||
'Ticket:',
|
||||
'Site-Key:',
|
||||
'',
|
||||
'Zahlung fehlgeschlagen',
|
||||
'Die Zahlung ist nicht eingegangen (zum Beispiel SEPA-Lastschrift abgelehnt). Es wurde nichts eingerichtet und kein Ticket angelegt; die Bestellung steht im Portal wieder auf „nicht bestellt“.',
|
||||
'',
|
||||
T16_FUSS,
|
||||
'',
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
test('News-Mails bleiben wörtlich: T16 bezahlt, aber nicht zuzuordnen', async () => {
|
||||
const mail = await t16Ungeklaert('cs_unklar_news', 'pX_news', 'news')
|
||||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||||
assert.equal(
|
||||
mail.text,
|
||||
[
|
||||
'WEBklar',
|
||||
'',
|
||||
'Intern · Kundenportal',
|
||||
'',
|
||||
'Funktionen',
|
||||
'',
|
||||
'Neue Funktion bestellt: News-Pinnwand',
|
||||
'',
|
||||
'Kunde: Kunde Eins',
|
||||
'E-Mail: kunde1@example.test',
|
||||
'Projekt: pX_news',
|
||||
'Ticket:',
|
||||
'Site-Key:',
|
||||
'',
|
||||
'Bezahlt, aber nicht zuzuordnen',
|
||||
'Der Kunde hat bezahlt, die Zahlung passt aber zu keiner offenen Bestellung. Es wurde nichts eingerichtet und kein Ticket angelegt. Bitte von Hand prüfen: cs_unklar_news (Kunde c1, Projekt pX_news)',
|
||||
'',
|
||||
T16_FUSS,
|
||||
'',
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------- Termine ---
|
||||
test('Termine-Mails: K15 mit den Werten des Terminplaners', async () => {
|
||||
const termine = config.features.termine.mails
|
||||
@@ -221,6 +351,137 @@ test('Termine-Mails: K16 mit den Werten des Terminplaners', () => {
|
||||
assert.ok(mail.text.includes(`Termine öffnen: ${BASE}/dashboard.html#termine`), mail.text)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------- T16 je Funktion (Team-Hinweis) ---
|
||||
test('Termine-T16: Betreff, Text und Knopf nennen den Terminplaner', async () => {
|
||||
const { mail, projectId, woid, doc } = await t16Bezahlt('termine', 'Salon Haarlinie')
|
||||
assert.equal(mail.subject, 'Neue Funktion bestellt: Terminplaner')
|
||||
for (const teil of [mail.subject, mail.text, mail.html]) assert.doesNotMatch(teil, /News|Beitr/)
|
||||
assert.match(mail.text, /Funktionen\n\nNeue Funktion bestellt: Terminplaner\n\nKunde: Kunde Eins\nE-Mail: kunde1@example\.test\nProjekt: Salon Haarlinie\n/)
|
||||
assert.ok(mail.text.includes(`Ticket: ${woid}\nSite-Key: ${doc.siteKey}\n`), mail.text)
|
||||
// der Knopf führt in das WOMS-Kundenpanel "Funktionen" mit geöffneter Anleitung dieses Projekts (anleitungsLink aus B8)
|
||||
const link = `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=${projectId}`
|
||||
assert.equal(link, reg.anleitungsLink('c1', projectId))
|
||||
assert.ok(mail.text.includes(`Im Ticketsystem öffnen: ${link}`), mail.text)
|
||||
assert.ok(mail.html.includes(`href="${link.replaceAll('&', '&')}"`), mail.html)
|
||||
// die Warnblöcke bleiben aus, solange B10a/D4 sie nicht füllen
|
||||
for (const warnung of ['Zahlung fehlgeschlagen', 'Bezahlt, aber nicht zuzuordnen', 'Kein laufendes Wartungs-Abo', 'Gesundheitsbereich prüfen', 'Bezahlt, aber schon vorhanden']) {
|
||||
assert.ok(!mail.text.includes(warnung), `kein Block "${warnung}"`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Termine-T16: Zahlung fehlgeschlagen und nicht zuzuordnen nennen den Terminplaner, ohne Knopf zur Anleitung', async () => {
|
||||
const { mail: fehl } = await t16Fehlgeschlagen('termine', 'Salon Haarlinie')
|
||||
assert.equal(fehl.subject, 'Neue Funktion bestellt: Terminplaner')
|
||||
assert.match(fehl.text, /Neue Funktion bestellt: Terminplaner\n\nKunde: Kunde Eins/)
|
||||
assert.match(fehl.text, /Zahlung fehlgeschlagen\nDie Zahlung ist nicht eingegangen/)
|
||||
assert.doesNotMatch(fehl.text, /News|Beitr|Im Ticketsystem öffnen/, 'es wurde nichts bestellt: keine Anleitung')
|
||||
|
||||
const unklar = await t16Ungeklaert('cs_unklar_termine', 'pX_termine', 'termine')
|
||||
assert.equal(unklar.subject, 'Neue Funktion bestellt: Terminplaner')
|
||||
assert.match(unklar.text, /Bezahlt, aber nicht zuzuordnen\nDer Kunde hat bezahlt.*cs_unklar_termine \(Kunde c1, Projekt pX_termine\)\n/)
|
||||
assert.doesNotMatch(unklar.text, /News|Beitr|Im Ticketsystem öffnen/)
|
||||
})
|
||||
|
||||
test('T16 ohne bekannten Funktionsschlüssel: "Funktion", sonst leer', async () => {
|
||||
for (const [sessionId, projectId, key] of [
|
||||
['cs_ohne_schluessel', 'pY1', undefined],
|
||||
['cs_fremder_schluessel', 'pY2', 'gibtesnicht'],
|
||||
]) {
|
||||
const mail = await t16Ungeklaert(sessionId, projectId, key)
|
||||
assert.equal(mail.subject, 'Neue Funktion bestellt: Funktion', sessionId)
|
||||
assert.match(mail.text, /Neue Funktion bestellt: Funktion\n\nKunde: Kunde Eins/)
|
||||
assert.match(mail.text, /Ticket:\nSite-Key:\n/, 'Ticket und Site-Key leer')
|
||||
assert.match(mail.text, new RegExp(`Bezahlt, aber nicht zuzuordnen\\nDer Kunde hat bezahlt.*${sessionId} \\(Kunde c1, Projekt ${projectId}\\)\\n`))
|
||||
assert.doesNotMatch(mail.text, /News|Beitr|Im Ticketsystem öffnen/, sessionId)
|
||||
}
|
||||
})
|
||||
|
||||
test('T16 Blöcke kein_abo und bereits_aktiv: Wortlaut und Werte', () => {
|
||||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: 'cs_test_1', zustand: 'aktiv', anleitung_url: '', gesundheit_text: '' }
|
||||
const aus = { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false }
|
||||
const keinAbo = renderMail('T16', werte, { ...aus, kein_abo: true })
|
||||
assert.deepEqual(keinAbo.problems, [])
|
||||
assert.match(keinAbo.text, /Kein laufendes Wartungs-Abo\nDie Zahlung ist eingegangen und das Ticket 10042 ist angelegt, aber der Kunde hat kein laufendes Wartungs-Abo\. Die Freischaltung bleibt gesperrt\. Endet das Abo vor der Fertigstellung, wird die Einrichtung voll erstattet \(Mail „Erstattung fällig“\)\.\n/)
|
||||
assert.doesNotMatch(keinAbo.text, /schon aktiv|nicht zuzuordnen/)
|
||||
const aktiv = renderMail('T16', werte, { ...aus, bereits_aktiv: true })
|
||||
assert.deepEqual(aktiv.problems, [])
|
||||
assert.match(
|
||||
aktiv.text,
|
||||
/Bezahlt, aber schon vorhanden\nZahlung eingegangen, aber die Funktion war schon aktiv\. Es wurde kein Ticket angelegt und nichts eingerichtet\. Bitte prüfen: Einbau nachholen oder erstatten\. Sitzung: cs_test_1\n/
|
||||
)
|
||||
assert.doesNotMatch(aktiv.text, /Kein laufendes Wartungs-Abo/)
|
||||
// ohne Block steht nichts davon in der Mail
|
||||
const keiner = renderMail('T16', werte, aus)
|
||||
assert.doesNotMatch(keiner.text, /Wartungs-Abo|schon aktiv|Im Ticketsystem öffnen|Gesundheitsbereich/)
|
||||
})
|
||||
|
||||
test('T16 gesundheit: Block aus ohne Warntext, Block an mit dem Text', () => {
|
||||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: '', gesundheit_text: 'Branche Physiotherapie: Gesundheitsdaten möglich' }
|
||||
const aus = renderMail('T16', werte, { gesundheit: false })
|
||||
assert.deepEqual(aus.problems, [])
|
||||
assert.doesNotMatch(aus.text, /Gesundheitsbereich prüfen|Physiotherapie/)
|
||||
assert.doesNotMatch(aus.html, /Physiotherapie/)
|
||||
const an = renderMail('T16', werte, { gesundheit: true })
|
||||
assert.deepEqual(an.problems, [])
|
||||
assert.ok(an.text.includes('Gesundheitsbereich prüfen: Branche Physiotherapie: Gesundheitsdaten möglich'), an.text)
|
||||
})
|
||||
|
||||
test('T16 anleitung: Knopf nur mit Block, Link trägt &anleitung=<Projekt-ID>', () => {
|
||||
const link = reg.anleitungsLink('c1', 'p42')
|
||||
assert.ok(link.endsWith('&anleitung=p42'))
|
||||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: link, gesundheit_text: '' }
|
||||
const an = renderMail('T16', werte, { anleitung: true })
|
||||
assert.deepEqual(an.problems, [])
|
||||
assert.ok(an.text.includes(`Im Ticketsystem öffnen: ${link}`), an.text)
|
||||
assert.ok(an.html.includes('&anleitung=p42"'), an.html)
|
||||
const aus = renderMail('T16', werte, { anleitung: false })
|
||||
assert.doesNotMatch(aus.text, /Im Ticketsystem öffnen/)
|
||||
assert.ok(!aus.text.includes('anleitung=p42'))
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------ T19 Erstattung fällig ---
|
||||
test('T19 rendert beide Fälle', () => {
|
||||
const werte = {
|
||||
kunde_name: 'Kunde Eins',
|
||||
email: 'kunde1@example.test',
|
||||
projekt: 'Salon Haarlinie',
|
||||
funktion: 'Terminplaner',
|
||||
betrag: '149,00 €',
|
||||
sitzung: 'cs_test_erstattung',
|
||||
woid: '10042',
|
||||
rechnung_url: 'https://invoice.stripe.test/i/1',
|
||||
}
|
||||
const gemeinsam = (mail) => {
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, '[Abo] Erstattung fällig: Terminplaner-Einrichtung')
|
||||
for (const teil of ['Kunde: Kunde Eins', 'E-Mail: kunde1@example.test', 'Projekt: Salon Haarlinie', 'Betrag: 149,00 €', 'Sitzung: cs_test_erstattung']) {
|
||||
assert.ok(mail.text.includes(teil), `T19 enthält: ${teil}\n---\n${mail.text}`)
|
||||
}
|
||||
// volle Erstattung von Hand, keine automatische Rückzahlung
|
||||
assert.match(mail.text, /von Hand in Stripe/)
|
||||
assert.match(mail.text, /Eine automatische Rückzahlung gibt es nicht\./)
|
||||
assert.match(mail.html, /Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https:\/\/ticket\.webklar\.com\/customers"[^>]*>Ticketsystem<\/a>/)
|
||||
assert.doesNotMatch(mail.text, /News|Beitr/)
|
||||
}
|
||||
|
||||
const nachEnde = renderMail('T19', werte, { zahlung_nach_ende: true, ticket_offen: false, rechnung: false })
|
||||
gemeinsam(nachEnde)
|
||||
assert.ok(nachEnde.text.includes('Die Zahlung ist erst nach dem Abo-Ende eingegangen, es wurde kein Ticket angelegt.'), nachEnde.text)
|
||||
assert.doesNotMatch(nachEnde.text, /noch offen|10042|Rechnung ansehen/)
|
||||
|
||||
const ticketOffen = renderMail('T19', werte, { zahlung_nach_ende: false, ticket_offen: true, rechnung: false })
|
||||
gemeinsam(ticketOffen)
|
||||
assert.ok(ticketOffen.text.includes('Das Ticket 10042 ist noch offen.'), ticketOffen.text)
|
||||
assert.match(ticketOffen.text, /Ticket mit einem Vermerk schließen/)
|
||||
assert.match(ticketOffen.text, /schaltet dann nicht frei/)
|
||||
assert.doesNotMatch(ticketOffen.text, /erst nach dem Abo-Ende|Rechnung ansehen/)
|
||||
|
||||
const mitRechnung = renderMail('T19', werte, { zahlung_nach_ende: true, ticket_offen: false, rechnung: true })
|
||||
gemeinsam(mitRechnung)
|
||||
assert.ok(mitRechnung.text.includes('Rechnung ansehen: https://invoice.stripe.test/i/1'), mitRechnung.text)
|
||||
assert.match(mitRechnung.html, /<a href="https:\/\/invoice\.stripe\.test\/i\/1"[^>]*>Rechnung ansehen/)
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------------- Platzhalter, Blöcke ---
|
||||
test('Platzhalter-/Blocklisten exakt: K15 und K16', () => {
|
||||
const k15 = loadTemplate('K15')
|
||||
@@ -247,6 +508,19 @@ test('Platzhalter-/Blocklisten exakt: K15 und K16', () => {
|
||||
assert.equal(k16.subject, '{{funktion_dein}} ist bereit')
|
||||
})
|
||||
|
||||
test('Platzhalter-/Blocklisten exakt: T16 und T19', () => {
|
||||
const t16 = loadTemplate('T16')
|
||||
assert.equal(t16.subject, 'Neue Funktion bestellt: {{funktion}}')
|
||||
assert.deepEqual(t16.placeholders, ['kunde_name', 'email', 'projekt', 'funktion', 'woid', 'sitekey', 'sitzung', 'zustand', 'anleitung_url', 'gesundheit_text'])
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit'])
|
||||
const t19 = loadTemplate('T19')
|
||||
assert.equal(t19.file, 'T19 Erstattung faellig.html')
|
||||
assert.equal(t19.subject, '[Abo] Erstattung fällig: {{funktion}}-Einrichtung')
|
||||
assert.deepEqual(t19.placeholders, ['kunde_name', 'email', 'projekt', 'funktion', 'betrag', 'sitzung', 'woid', 'rechnung_url'])
|
||||
assert.deepEqual(t19.blocks, ['zahlung_nach_ende', 'ticket_offen', 'rechnung'])
|
||||
for (const tpl of [t16, t19]) assert.doesNotMatch(tpl.body, /News|Beitr|Terminplaner/, `${tpl.id}: keine feste Funktion`)
|
||||
})
|
||||
|
||||
test('Die Mail-Werte je Funktion decken genau die Platzhalter der Vorlagen ab', () => {
|
||||
const ctx = { kundeName: 'Kunde Eins', projekt: 'Salon', invoiceUrl: 'https://invoice.stripe.test/i/1' }
|
||||
for (const key of reg.featureKeys()) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
|
||||
|
||||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||||
// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15 und K16 gelten je Funktion, Terminplaner TP2 B9a); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen); T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweis; T19 „Erstattung fällig“ folgt in Block B)
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b)
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 18), 'T20']
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 19), 'T20']
|
||||
const GUEST = ids('G', 8)
|
||||
|
||||
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
|
||||
@@ -97,6 +97,23 @@ test('K15, K16: Platzhalter und Blöcke je Funktion, keine Funktion fest im Text
|
||||
}
|
||||
})
|
||||
|
||||
// Terminplaner TP2 (B9b): T16 gilt je Funktion (neue Platzhalter und Blöcke), T19 ist neu (Erstattung fällig, Team-Hinweis)
|
||||
test('T16, T19: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
|
||||
const t16 = loadTemplate('T16')
|
||||
assert.equal(t16.subject, 'Neue Funktion bestellt: {{funktion}}')
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit'])
|
||||
const t19 = loadTemplate('T19')
|
||||
assert.equal(t19.file, 'T19 Erstattung faellig.html')
|
||||
assert.match(t19.subject, /^\[Abo\] Erstattung fällig: \{\{funktion\}\}-Einrichtung$/)
|
||||
assert.deepEqual(t19.blocks, ['zahlung_nach_ende', 'ticket_offen', 'rechnung'])
|
||||
for (const tpl of [t16, t19]) {
|
||||
for (const key of tpl.placeholders) assert.ok(tpl.body.includes(`{{${key}}}`) || tpl.subject.includes(`{{${key}}}`), `${tpl.id}: {{${key}}} steht in der Vorlage`)
|
||||
for (const block of tpl.blocks) {
|
||||
assert.equal((tpl.body.match(new RegExp(`<!-- WENN ${block} -->`, 'g')) || []).length, 1, `${tpl.id}: Block ${block} genau einmal`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden <br>', () => {
|
||||
const mail = renderMail('T01', { kunde_name: 'Max <b>&"\'', nachricht: 'Zeile 1\nZeile <2>' })
|
||||
assert.match(mail.html, /Max <b>&"' hat im Kundenportal geschrieben/)
|
||||
@@ -530,7 +547,7 @@ test('T07: Hinweis auf nicht genutzte Arbeitszeit nur im Block wartung', () => {
|
||||
})
|
||||
|
||||
// Etappe 4: das Admin-Panel ist abgeschaltet, Team-Mails fuehren ins Ticketsystem
|
||||
const TEAM_FOOTER = ['T01', 'T02', 'T03', 'T04', 'T05', 'T06', 'T07', 'T08', 'T09', 'T10', 'T11', 'T12', 'T13', 'T14', 'T16', 'T20']
|
||||
const TEAM_FOOTER = ['T01', 'T02', 'T03', 'T04', 'T05', 'T06', 'T07', 'T08', 'T09', 'T10', 'T11', 'T12', 'T13', 'T14', 'T16', 'T19', 'T20']
|
||||
|
||||
test('keine Vorlage verlinkt admin.html oder nennt den Admin-Bereich', () => {
|
||||
for (const id of EXPECTED) {
|
||||
|
||||
Reference in new issue
Block a user