From 973135d2dedae62f715d1e6a2c090afefbe913c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 14:09:05 +0000 Subject: [PATCH] Mails K22, K23 und Hinweis auf das Ende der Funktionen in K02/K03 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminplaner TP2 E3. K22 "Terminplaner beendet" (Blöcke abo_ende, hosting, vor_einrichtung, daten) und K23 "Terminplaner läuft wieder" im Layout von K19, Du-Form, Texte als Entwurf für die Mail-Überarbeitung. K02 und K03 bekommen den Block funktionen (in K03 innerhalb von abo); routes/abo.js setzt ihn nur beim Wartungs-Abo und nie bei der Speicher-Kündigung. Co-Authored-By: Claude Sonnet 5.5 --- .../K02 Kuendigungsbestaetigung.html | 7 +- .../K03 Eingang Kuendigung.html | 7 +- .../K22 Terminplaner beendet.html | 55 ++++++++ .../K23 Terminplaner laeuft wieder.html | 50 ++++++++ server/routes/abo.js | 5 +- test/mailTemplates.test.js | 118 +++++++++++++++++- test/publicForms.test.js | 48 ++++++- 7 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 server/mail-templates/K22 Terminplaner beendet.html create mode 100644 server/mail-templates/K23 Terminplaner laeuft wieder.html diff --git a/server/mail-templates/K02 Kuendigungsbestaetigung.html b/server/mail-templates/K02 Kuendigungsbestaetigung.html index 763ddb2..ce1b879 100644 --- a/server/mail-templates/K02 Kuendigungsbestaetigung.html +++ b/server/mail-templates/K02 Kuendigungsbestaetigung.html @@ -8,8 +8,8 @@ Absender: „WEBklar“ Empfänger: Kunde (E-Mail am Abo) · BCC intern Platzhalter: {{name}}, {{produkt}}, {{eingang_zeitpunkt}}, {{vertragsende}}, {{vertragsende_paragraph}} - Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung - produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent. + Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung, funktionen + produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent; funktionen = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (nur Wartungs-Abo; Sie-Form wie die Vorlage). --> @@ -39,6 +39,9 @@ a{color:#111111;} Bis dahin bleiben alle Leistungen verfügbar, danach erfolgen keine weiteren Abbuchungen. Gebuchter E-Mail-Zusatzspeicher endet zusammen mit dem Abo. Nicht genutzte Zeit aus dem Stundenkontingent entfällt mit Vertragsende. Ihre Daten, den Domain-AuthCode und die Inhalte Ihrer Postfächer stellen wir Ihnen auf Wunsch gemäß {{vertragsende_paragraph}} der Vertragsbedingungen bereit. + +Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren. + Viele Grüße
Ihr WEBklar-Team diff --git a/server/mail-templates/K03 Eingang Kuendigung.html b/server/mail-templates/K03 Eingang Kuendigung.html index 39f3ca3..2549846 100644 --- a/server/mail-templates/K03 Eingang Kuendigung.html +++ b/server/mail-templates/K03 Eingang Kuendigung.html @@ -8,8 +8,8 @@ Absender: „WEBklar“ Empfänger: E-Mail aus dem Formular · BCC intern Platzhalter: {{name}}, {{produkt}}, {{kuendigungsart}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}, {{vertrag}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{vertragsende_paragraph}}, {{pakete}}, {{pakete_neu}}, {{pakete_angabe}} - Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung - produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1). + Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung, funktionen + produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1); funktionen (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (Sie-Form wie die Vorlage). --> @@ -79,6 +79,9 @@ a{color:#111111;} Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit Vertragsende. Ihre Daten, den Domain-AuthCode und die Inhalte Ihrer Postfächer stellen wir Ihnen auf Wunsch gemäß {{vertragsende_paragraph}} der Vertragsbedingungen bereit. Gebuchter E-Mail-Zusatzspeicher endet zusammen mit dem Abo. + +Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren. + Viele Grüße
Ihr WEBklar-Team diff --git a/server/mail-templates/K22 Terminplaner beendet.html b/server/mail-templates/K22 Terminplaner beendet.html new file mode 100644 index 0000000..a7641ea --- /dev/null +++ b/server/mail-templates/K22 Terminplaner beendet.html @@ -0,0 +1,55 @@ + + + + + + + + +Dein Terminplaner wurde beendet – {{projekt}} + + + + +Dein Terminplaner wurde beendet. ͏ ͏ ͏ + + + + + diff --git a/server/mail-templates/K23 Terminplaner laeuft wieder.html b/server/mail-templates/K23 Terminplaner laeuft wieder.html new file mode 100644 index 0000000..83a48ce --- /dev/null +++ b/server/mail-templates/K23 Terminplaner laeuft wieder.html @@ -0,0 +1,50 @@ + + + + + + + + +Dein Terminplaner läuft wieder – {{projekt}} + + + + +Dein Terminplaner läuft wieder. ͏ ͏ ͏ + + + + + diff --git a/server/routes/abo.js b/server/routes/abo.js index 62fcb42..7d00087 100644 --- a/server/routes/abo.js +++ b/server/routes/abo.js @@ -474,7 +474,8 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) { vertragsende: fmtDate(endsAt), vertragsende_paragraph: endParagraphOf(sub), }, - blocks: { wartung: isWartung(sub) }, + // Hinweis auf nicht genutzte Arbeitszeit und auf das Ende von Website-Analyse und Terminplaner nur beim Wartungs-Abo + blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) }, }) await updateDocument(config.collections.subscriptions, sub.$id, { cancelRequestedAt: receivedAt.toISOString(), @@ -942,6 +943,8 @@ router.post('/public/cancel', async (req, res) => { abo: !storage, // Hinweis auf nicht genutzte Arbeitszeit nur im Wartungs-Abo (Block innerhalb von abo) wartung: !storage && isWartung(sub), + // Website-Analyse und Terminplaner enden mit dem Wartungs-Abo (Block innerhalb von abo, nie bei der Speicher-Kündigung) + funktionen: !storage && isWartung(sub), automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe', sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe', ausserordentlich: !storage && !effectiveAt && d.kind === 'ausserordentlich', diff --git a/test/mailTemplates.test.js b/test/mailTemplates.test.js index a9ef4bd..ab29d6c 100644 --- a/test/mailTemplates.test.js +++ b/test/mailTemplates.test.js @@ -6,9 +6,9 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products. const DIR = new URL('../server/mail-templates/', import.meta.url) // G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb; -// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b) +// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b) const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`) -const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 19), 'T20'] +const EXPECTED = [...ids('G', 8), ...ids('K', 23), ...ids('T', 19), 'T20'] const GUEST = ids('G', 8) const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`])) @@ -114,6 +114,108 @@ test('T16, T19: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => { } }) +// Terminplaner TP2 (E3): K22 „Terminplaner beendet“ je Grund eine Variante, K23 „läuft wieder“; Texte sind Entwurf für die Mail-Überarbeitung +const K22_WERTE = { kunde_name: 'Erika', projekt: 'Praxis Muster', enddatum: '03.10.2026', loeschdatum: '02.11.2026', preis: '149 €', portal_url: 'https://portal.example/#termine' } +const k22 = (blocks) => renderMail('K22', K22_WERTE, blocks) + +test('K22, K23: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => { + const tpl22 = loadTemplate('K22') + assert.equal(tpl22.file, 'K22 Terminplaner beendet.html') + assert.equal(tpl22.subject, 'Dein Terminplaner wurde beendet – {{projekt}}') + assert.deepEqual(tpl22.placeholders, ['kunde_name', 'projekt', 'enddatum', 'loeschdatum', 'preis', 'portal_url']) + assert.deepEqual(tpl22.blocks, ['abo_ende', 'hosting', 'vor_einrichtung', 'daten']) + const tpl23 = loadTemplate('K23') + assert.equal(tpl23.file, 'K23 Terminplaner laeuft wieder.html') + assert.equal(tpl23.subject, 'Dein Terminplaner läuft wieder – {{projekt}}') + assert.deepEqual(tpl23.placeholders, ['kunde_name', 'projekt', 'portal_url']) + assert.deepEqual(tpl23.blocks, []) + assert.equal(k22({}).subject, 'Dein Terminplaner wurde beendet – Praxis Muster') +}) + +test('K22 abo_ende: nennt das beendete Wartungs-Abo, keine Erstattung', () => { + const mail = k22({ abo_ende: true }) + assert.deepEqual(mail.problems, []) + assert.match(mail.text, /Wartungs-Abo/) + assert.match(mail.text, /03\.10\.2026/, 'Enddatum') + assert.doesNotMatch(mail.text, /erstatten/) + assert.doesNotMatch(mail.text, /Hosting-Abo/, 'nicht die Hosting-Variante') + assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/, 'der Kasten kommt nur mit dem Block daten') +}) + +test('K22 hosting: das Hosting-Abo reicht für den Terminplaner nicht', () => { + const mail = k22({ hosting: true }) + assert.deepEqual(mail.problems, []) + assert.match(mail.text, /Hosting-Abo/) + assert.match(mail.text, /Wartungs-Abo/, 'sagt, was der Terminplaner braucht') + assert.doesNotMatch(mail.text, /erstatten/) + assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/) +}) + +test('K22 vor_einrichtung: Einrichtung wird voll erstattet, nennt den Preis, kein Löschkasten', () => { + const mail = k22({ vor_einrichtung: true }) + assert.deepEqual(mail.problems, []) + assert.match(mail.text, /149 €/) + assert.match(mail.text, /erstatten/) + assert.match(mail.text, /vor der Einrichtung/) + assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt|Rückkehr|löschen/, 'es gibt noch keine Termindaten, also keinen Löschkasten') +}) + +test('K22 daten: Kasten „Was jetzt gilt“ mit Löschdatum, Gästen und Rückkehr', () => { + const mail = k22({ daten: true }) + assert.deepEqual(mail.problems, []) + const t = mail.text + assert.match(t, /Was jetzt gilt/) + assert.match(t, /Bis 02\.11\.2026 kannst Du Deine Termine im Kundenportal ansehen und als CSV exportieren/) + assert.match(t, /Gäste können ihre Termine weiter absagen/) + assert.match(t, /Bestehende Termine bleiben eingetragen, und die Erinnerungen an Deine Gäste laufen bis zur Löschung weiter/) + assert.match(t, /Bitte informiere Deine Gäste selbst/) + assert.match(t, /Ab 02\.11\.2026 löschen wir die Termindaten/) + assert.match(t, /30 Tage/, 'Löschfrist 30 Tage nach dem Abschalten') + assert.match(t, /Rückkehr: Läuft bis dahin wieder ein Wartungs-Abo, schalten wir den Terminplaner automatisch und ohne Gebühr wieder ein/) + assert.match(t, /Nach der Löschung braucht es einen neuen Fragebogen und die Einrichtung für 149 € noch einmal/) + assert.doesNotMatch(t, /erstatten/) +}) + +test('K22: Anrede, Projekt und Knopf ins Kundenportal stehen in jeder Variante', () => { + const mail = k22({ abo_ende: true, daten: true }) + assert.deepEqual(mail.problems, []) + assert.match(mail.text, /Hallo Erika,/) + assert.match(mail.text, /Praxis Muster/) + assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/) + assert.match(mail.html, /href="https:\/\/portal\.example\/#termine"/) +}) + +test('K23: nennt das Projekt und führt ins Kundenportal', () => { + const mail = renderMail('K23', { kunde_name: 'Erika', projekt: 'Praxis Muster', portal_url: 'https://portal.example/#termine' }) + assert.deepEqual(mail.problems, []) + assert.equal(mail.subject, 'Dein Terminplaner läuft wieder – Praxis Muster') + assert.match(mail.text, /Hallo Erika,/) + assert.match(mail.text, /Praxis Muster/) + assert.match(mail.text, /Wartungs-Abo/) + assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/) + assert.match(mail.html, / { + const k02 = { name: 'K', produkt: 'Wartungs-Abo', eingang_zeitpunkt: '01.10.2026, 10:00', vertragsende: '01.10.2027', vertragsende_paragraph: '§ 11' } + assert.ok(loadTemplate('K02').blocks.includes('funktionen')) + const an = renderMail('K02', k02, { wartung: true, funktionen: true }) + assert.deepEqual(an.problems, []) + assert.match(an.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./) + assert.doesNotMatch(renderMail('K02', k02, { wartung: true }).text, /Terminplaner/) + assert.doesNotMatch(renderMail('K02', { ...k02, produkt: 'Hosting-Abo' }, {}).text, /Terminplaner|Wartungs-Abo/, 'Hosting-Abo: kein Hinweis') + + const k03 = { ...sampleValues(loadTemplate('K03')), vertragsende_paragraph: '§ 11' } + assert.ok(loadTemplate('K03').blocks.includes('funktionen')) + const abo = renderMail('K03', k03, { abo: true, wartung: true, funktionen: true }) + assert.deepEqual(abo.problems, []) + assert.match(abo.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./) + assert.doesNotMatch(renderMail('K03', k03, { abo: true, wartung: true }).text, /Terminplaner/) + // Speicher-Kündigung (abo aus): der Block steht innerhalb von abo und fällt mit ihm weg + assert.doesNotMatch(renderMail('K03', k03, { abo: false, funktionen: true }).text, /Terminplaner/) +}) + test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden
', () => { const mail = renderMail('T01', { kunde_name: 'Max &"\'', nachricht: 'Zeile 1\nZeile <2>' }) assert.match(mail.html, /Max <b>&"' hat im Kundenportal geschrieben/) @@ -816,6 +918,18 @@ test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und na assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./) }) +// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest) +test('K22, K23: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => { + for (const id of ['K22', 'K23']) { + const tpl = loadTemplate(id) + assert.ok(!tpl.blocks.includes('sie') && !tpl.blocks.includes('du'), `${id}: Du-Form fest, keine Anredeblöcke`) + const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true)) + assert.doesNotMatch(mail.text, SIE_FORM, `${id}: Du-Form\n${mail.text}`) + assert.match(mail.text, /Hallo Wert-kunde_name,/, id) + assert.match(mail.text, DU_FORM, `${id}: spricht den Kunden mit Du an`) + } +}) + test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fall', () => { for (const id of ['K20', 'K21']) { const tpl = loadTemplate(id) diff --git a/test/publicForms.test.js b/test/publicForms.test.js index 76fcce9..66d92b5 100644 --- a/test/publicForms.test.js +++ b/test/publicForms.test.js @@ -22,7 +22,7 @@ const aw = await startAppwriteMock({ const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') -const { startTestApp, post } = await import('./support/testApp.js') +const { startTestApp, post, postWebhook } = await import('./support/testApp.js') const { hostingDoc, consent, seedFixtures, stripeSubscription } = await import('./support/fixtures.js') const { default: aboRoutes, resetPublicRateLimit, publicRateLimitState, purgeRateLimit } = await import('../server/routes/abo.js') const { listContractEvents, recordContractEvent } = await import('../server/services/contractEvents.js') @@ -147,6 +147,52 @@ test('K03/K04/T08/T09: Produkt des Kunden, Vertragsende nach § 11 der Fassung 4 assert.ok(k03u.text.includes('§ 11 der Vertragsbedingungen'), k03u.text) }) +// Terminplaner TP2 (E3): Website-Analyse und Terminplaner gehören zum Wartungs-Abo; K03 und K02 sagen, dass sie mit ihm enden +const FUNKTIONEN_SATZ = 'Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm.' +const WARTUNG_V4 = { productKey: 'wartung', termsVersion: config.abo.termsVersion } +const k02 = () => bySubject('Bestätigung Ihrer Kündigung') + +test('K03: Hinweis auf das Ende der Funktionen nur bei der Kündigung eines Wartungs-Abos', async () => { + await resetSubH({ patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } }) + assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200) + assert.ok(k03().text.includes(FUNKTIONEN_SATZ), k03().text) + assert.ok(k03().text.includes('Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.'), k03().text) + + // Hosting-Abo: weder Analyse noch Terminplaner + await resetSubH() + assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200) + assert.doesNotMatch(k03().text, /Terminplaner/) + + // nur der Zusatzspeicher eines Wartungs-Abos gekündigt: das Abo läuft weiter, kein Hinweis + await resetSubH({ units: 3, patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } }) + assert.equal((await cancel({ contract: 'zusatzspeicher' })).status, 200) + assert.doesNotMatch(k03().text, /Terminplaner/) + + // unbekannte E-Mail (kein Abo gefunden): kein Hinweis + await resetSubH({ patch: WARTUNG_V4, stripe: { productKey: 'wartung', metadata: { termsVersion: config.abo.termsVersion } } }) + assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich', email: 'niemand@example.invalid' })).status, 200) + assert.doesNotMatch(bySubject('Eingangsbestätigung Ihrer Kündigung').text, /Terminplaner/) +}) + +test('K02: Kündigung im Stripe-Kundenportal, Hinweis auf das Ende der Funktionen nur beim Wartungs-Abo', async () => { + const gekuendigt = (id) => ({ + id, + object: 'event', + type: 'customer.subscription.updated', + created: Math.floor(Date.now() / 1000), + data: { object: { id: 'sub_h', object: 'subscription', cancel_at_period_end: true, metadata: {} }, previous_attributes: { cancel_at_period_end: false } }, + }) + for (const [productKey, mitHinweis] of [['wartung', true], ['hosting', false]]) { + const patch = productKey === 'wartung' ? WARTUNG_V4 : {} + await resetSubH({ patch: { ...patch, cancelAtPeriodEnd: true }, stripe: { productKey, cancel_at_period_end: true, metadata: productKey === 'wartung' ? { termsVersion: config.abo.termsVersion } : {} } }) + const res = await postWebhook(gekuendigt(`evt_k02_${productKey}`)) + assert.equal(res.status, 200, JSON.stringify(res.body)) + assert.ok(k02(), `${productKey}: K02`) + assert.equal(k02().text.includes(FUNKTIONEN_SATZ), mitHinweis, `${productKey}\n${k02().text}`) + assert.equal(/Terminplaner/.test(k02().text), mitHinweis, productKey) + } +}) + test('2 von 3 Paketen ergibt Menge 1', async () => { await resetSubH({ units: 3 }) const res = await cancel({ contract: 'zusatzspeicher', packages: '2' })