Mails: Vertragsbestätigung je Fassung, Zustimmung zum AVV, kleinere Textkorrekturen

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-29 14:46:41 +00:00
1 parent 63d3bb11bf
commit 12a24514a4
10 files changed
+172 -36

No files matched your search

+59 -2
View File
@@ -432,7 +432,8 @@ test('Webhook checkout.session.completed: K01 je Produkt mit PDF der zugestimmte
} finally {
await cleanupWebhook(subH)
}
// Wartungs-Abo nach Fassung 3 (Altbestand): das Archiv-PDF der Fassung 3, Stunde und beliebig viele Postfaecher
// Wartungs-Abo nach Fassung 3 (Altbestand): das Archiv-PDF der Fassung 3 und die Zeile „Enthalten“ der Fassung 3
// (1 E-Mail-Postfach, 1 Stunde Arbeitszeit pro Monat; Block wartung_v3, Fix 1)
const subW = 'sub_1SD6z3TxAbCdEfGhIjKlMnOt'
st.subscriptions.set(subW, stripeSubscription({ id: subW, customerId: 'c3', productKey: 'wartung', metadata: { termsVersion: '2026-09-25-v3' } }))
try {
@@ -441,12 +442,68 @@ test('Webhook checkout.session.completed: K01 je Produkt mit PDF der zugestimmte
const k01 = mails.sent.find((m) => m.subject.includes('Vertragsbestätigung'))
assert.equal(k01.subject, 'Ihr WEBklar Wartungs-Abo – Vertragsbestätigung')
assert.deepEqual(k01.attachments.map((a) => a.filename), ['WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'])
for (const text of ['50,00 € / Monat', 'beliebig viele Postfächer', '1 Stunde Arbeitszeit je bezahltem Monat', '(Version 2026-09-25-v3)']) {
for (const text of ['50,00 € / Monat', '• 1 E-Mail-Postfach', '• 1 Stunde Arbeitszeit pro Monat', 'Ihr aktuelles Stundenkontingent', '(Version 2026-09-25-v3) einschließlich des Auftragsverarbeitungsvertrags']) {
assert.ok(k01.text.includes(text), `${text}\n---\n${k01.text}`)
}
assert.ok(!k01.text.includes('beliebig viele') && !k01.text.includes('je bezahltem Monat'), k01.text)
} finally {
await cleanupWebhook(subW)
}
// Gegenprobe: Wartungs-Abo nach Fassung 4 - aktuelles PDF, beliebig viele Postfaecher, Stunde je bezahltem Monat
const subW4 = 'sub_1SD6z4UxAbCdEfGhIjKlMnOu'
st.subscriptions.set(subW4, stripeSubscription({ id: subW4, customerId: 'c3', productKey: 'wartung' }))
try {
mails.sent.length = 0
assert.equal((await postWebhook(checkoutCompleted(subW4, { productKey: 'wartung' }))).status, 200)
const k01 = mails.sent.find((m) => m.subject.includes('Vertragsbestätigung'))
assert.equal(k01.subject, 'Ihr WEBklar Wartungs-Abo – Vertragsbestätigung')
assert.deepEqual(k01.attachments.map((a) => a.filename), ['WEBklar-Abo-Vertragsunterlagen.pdf'])
for (const text of ['50,00 € / Monat', '• beliebig viele Postfächer', '1 Stunde Arbeitszeit je bezahltem Monat', `(Version ${config.abo.termsVersion}) einschließlich des Auftragsverarbeitungsvertrags`]) {
assert.ok(k01.text.includes(text), `${text}\n---\n${k01.text}`)
}
assert.ok(!k01.text.includes('1 E-Mail-Postfach') && !k01.text.includes('pro Monat'), k01.text)
} finally {
await cleanupWebhook(subW4)
}
// Fix 1 (3): ohne gespeicherte Fassung kommen Versionsangabe und PDF aus derselben Quelle - beide Fassung 3
const subW0 = 'sub_1SD6z5VxAbCdEfGhIjKlMnOv'
st.subscriptions.set(subW0, stripeSubscription({ id: subW0, customerId: 'c3', productKey: 'wartung', metadata: { termsVersion: null } }))
try {
mails.sent.length = 0
assert.equal((await postWebhook(checkoutCompleted(subW0, { productKey: 'wartung' }))).status, 200)
const k01 = mails.sent.find((m) => m.subject.includes('Vertragsbestätigung'))
assert.ok(k01, 'K01 verschickt')
assert.deepEqual(k01.attachments.map((a) => a.filename), ['WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'])
assert.ok(k01.text.includes('(Version 2026-09-25-v3)'), k01.text)
assert.ok(!k01.text.includes(config.abo.termsVersion), k01.text)
assert.ok(k01.text.includes('• 1 E-Mail-Postfach'), k01.text)
} finally {
await cleanupWebhook(subW0)
}
})
// Fix 1 (5): T07 nennt die nicht genutzte Arbeitszeit nur beim Wartungs-Abo
test('Webhook customer.subscription.deleted: T07 mit Arbeitszeit-Hinweis nur bei Wartung', async () => {
const deleted = (id) => ({ id: `evt_del_${id}`, object: 'event', type: 'customer.subscription.deleted', created: Math.floor(Date.now() / 1000), data: { object: { id, object: 'subscription', status: 'canceled' } } })
const subW = 'sub_1SD6z6WxAbCdEfGhIjKlMnOw'
const subH = 'sub_1SD6z7XxAbCdEfGhIjKlMnOx'
st.subscriptions.set(subW, stripeSubscription({ id: subW, customerId: 'c3', productKey: 'wartung', status: 'canceled' }))
st.subscriptions.set(subH, stripeSubscription({ id: subH, customerId: 'c3', productKey: 'hosting', status: 'canceled' }))
try {
mails.sent.length = 0
assert.equal((await postWebhook(deleted(subW))).status, 200)
const t07w = mails.sent.find((m) => m.subject.startsWith('[Abo] Abo beendet'))
assert.ok(t07w, 'T07 verschickt')
assert.ok(t07w.text.includes('Abo: Wartungs-Abo') && t07w.text.includes('Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit dem Vertragsende.'), t07w.text)
mails.sent.length = 0
assert.equal((await postWebhook(deleted(subH))).status, 200)
const t07h = mails.sent.find((m) => m.subject.startsWith('[Abo] Abo beendet'))
assert.ok(t07h.text.includes('Abo: Hosting-Abo') && t07h.text.includes('Gebuchter Zusatzspeicher endet mit dem Abo.'), t07h.text)
assert.ok(!t07h.text.includes('Arbeitszeit') && !t07h.text.includes('Stundenkontingent'), t07h.text)
} finally {
await cleanupWebhook(subW)
await cleanupWebhook(subH)
}
})
// Fix 1, Important: ein dauerhafter Fehler beim Nachweis (4xx, z. B. Collection oder Attribut fehlt) darf
+48 -5
View File
@@ -121,7 +121,7 @@ test('Textfassung K01: alle Vertragsangaben lesbar', () => {
'• 1 Domain',
'Zum Kundenportal: https://project.webklar.com/dashboard.html',
'📎 Widerrufsbelehrung mit Muster-Widerrufsformular',
'Ihre Zustimmung zu den Vertragsbedingungen (Version 2026-09-25-v3) haben wir am 01.10.2026, 10:07 Uhr erhalten.',
'Ihre Zustimmung zu den Vertragsbedingungen (Version 2026-09-25-v3) einschließlich des Auftragsverarbeitungsvertrags haben wir am 01.10.2026, 10:07 Uhr erhalten.',
'Vertrag widerrufen (https://project.webklar.com/widerrufen.html)',
'Vertrag kündigen (https://project.webklar.com/kuendigen.html)',
'Telefon 0176 23726355 · support@webklar.com',
@@ -267,11 +267,13 @@ test('K03, K04, T08, T09: Vertrag bzw. Bestellung, Pakete und die Blöcke der Au
assert.match(speicher.text, /Vertrag: E-Mail-Zusatzspeicher zum WEBklar Hosting-Abo/)
assert.match(speicher.text, /Ihr Zusatzspeicher verringert sich zum\n31\.10\.2026/)
assert.match(speicher.text, /um 2 Paket\(e\) gekündigt; danach bleiben 1 Paket\(e\)/)
assert.doesNotMatch(speicher.text, /Wartungs-Abo\n|Stundenguthaben|Wir prüfen Ihre Kündigung/)
assert.doesNotMatch(speicher.text, /Wartungs-Abo\n|Stundenkontingent|Stundenguthaben|Wir prüfen Ihre Kündigung/)
assert.match(renderMail('K03', k03, { pruefen: true }).text, /bestätigen Ihnen den Zeitpunkt, zu dem sie wirkt, gesondert per E-Mail/)
// Task 12: das Stundenguthaben gibt es nur im Wartungs-Abo (Block wartung innerhalb von abo)
assert.match(renderMail('K03', k03, { abo: true, wartung: true }).text, /Stundenguthaben verfällt/)
assert.doesNotMatch(renderMail('K03', k03, { abo: true }).text, /Stundenguthaben/)
// Task 12 (Fix 1, 6): das Stundenkontingent gibt es nur im Wartungs-Abo (Block wartung innerhalb von abo); es ist
// kein Geldguthaben (v4 § 4 Abs. 1), darum „nicht genutzte Arbeitszeit“ statt „Stundenguthaben“
assert.match(renderMail('K03', k03, { abo: true, wartung: true }).text, /Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit Vertragsende\./)
assert.doesNotMatch(renderMail('K03', k03, { abo: true }).text, /Stundenkontingent|Stundenguthaben/)
assert.doesNotMatch(renderMail('K03', k03, { abo: true, wartung: true }).text, /Stundenguthaben/)
assert.match(renderMail('K03', k03, { abo: true }).text, /Domain-AuthCode/)
const k04 = { ...sampleValues(loadTemplate('K04')), bestellung: 'Wechsel ins Wartungs-Abo' }
@@ -445,3 +447,44 @@ test('T09: Handgriffe je Bestellung ohne Wertersatz beim Zusatzspeicher', () =>
const abschluss = renderMail('T09', t09, { abo_gefunden: true, abschluss: true }).text
assert.match(abschluss, /Zusatzspeicher[^.]*vollständig/)
})
// Task 12, Fix round 1: K01 je Fassung (v3-Zeile „1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat“ im Block wartung_v3),
// Zustimmung einschließlich AVV; K12 „Heute fällig“ nennt den Zusatzspeicher nur im Block; T07 nennt die Arbeitszeit nur bei Wartung
test('K01: Block wartung_v3 mit der Zeile der Fassung 3, wartung mit der Fassung 4, Zustimmung einschließlich AVV', () => {
const base = { name: 'K', 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' }
assert.ok(loadTemplate('K01').blocks.includes('wartung_v3'), 'Block wartung_v3 deklariert')
const v3 = renderMail('K01', base, { wartung_v3: true, sofortbeginn: true })
assert.deepEqual(v3.problems, [])
assert.match(v3.text, /• 1 E-Mail-Postfach/)
assert.match(v3.text, /• 1 Stunde Arbeitszeit pro Monat/)
assert.match(v3.text, /Dort sehen Sie jederzeit Ihr aktuelles Stundenkontingent\./)
assert.doesNotMatch(v3.text, /beliebig viele|je bezahltem Monat|Zusatzspeicher/)
assert.match(v3.text, /Ihre Zustimmung zu den Vertragsbedingungen \(Version 2026-09-25-v3\) einschließlich des Auftragsverarbeitungsvertrags haben wir am 01\.10\.2026, 10:07 Uhr erhalten\./)
const v4 = renderMail('K01', { ...base, bedingungen_version: '2026-10-05-v4' }, { wartung: true, sofortbeginn: true })
assert.match(v4.text, /• beliebig viele Postfächer/)
assert.match(v4.text, /1 Stunde Arbeitszeit je bezahltem Monat/)
assert.doesNotMatch(v4.text, /1 E-Mail-Postfach|pro Monat/)
const h = renderMail('K01', { ...base, produkt: 'Hosting-Abo', preis: '15,00 € / Monat' }, { hosting: true, sofortbeginn: true })
assert.doesNotMatch(h.text, /Stundenkontingent|Stunde Arbeitszeit/)
})
test('K12: „Heute fällig“ nennt den Zusatzspeicher nur im Block zusatzspeicher', () => {
const values = sampleValues(loadTemplate('K12'))
assert.match(renderMail('K12', values, { zusatzspeicher: true }).text, /Monatsgebühr des Wartungs-Abos mit gebuchtem Zusatzspeicher, abzüglich der Gutschrift/)
const ohne = renderMail('K12', values, {}).text
assert.match(ohne, /Monatsgebühr des Wartungs-Abos, abzüglich der Gutschrift/)
assert.doesNotMatch(ohne, /mit gebuchtem Zusatzspeicher/)
})
test('T07: Hinweis auf nicht genutzte Arbeitszeit nur im Block wartung', () => {
const values = { produkt: 'Wartungs-Abo', email: 'k@example.invalid', stripe_abo_id: 'sub_1', status: 'canceled', vertragsende_paragraph: '§ 11' }
assert.ok(loadTemplate('T07').blocks.includes('wartung'), 'Block wartung deklariert')
const w = renderMail('T07', values, { wartung: true })
assert.deepEqual(w.problems, [])
assert.match(w.text, /Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit dem Vertragsende\. Gebuchter Zusatzspeicher endet mit dem Abo\./)
const h = renderMail('T07', { ...values, produkt: 'Hosting-Abo' }, {})
assert.deepEqual(h.problems, [])
assert.match(h.text, /Gebuchter Zusatzspeicher endet mit dem Abo\./)
assert.doesNotMatch(h.text, /Arbeitszeit|Stundenkontingent|Restguthaben/)
})
+13 -7
View File
@@ -115,18 +115,24 @@ test('priceIdsOfInvoice: eindeutige Preis-IDs, ohne Lücken', () => {
// Fix round 1 / B9: feste Form statt Einzelfelder
test('Vertragsunterlagen je Version', () => {
// F08 (Task 12): endParagraph = Paragraph zum Vertragsende der Fassung (v4: § 9 ist der Wechsel, § 11 das Vertragsende)
assert.deepEqual(termsDocFor('2026-10-05-v4'), { url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' })
// F08 (Task 12): endParagraph = Paragraph zum Vertragsende der Fassung (v4: § 9 ist der Wechsel, § 11 das Vertragsende);
// Fix 1 (3): version = die Fassung, die K01 nennt - ohne Angabe die archivierte Fassung 3, passend zum PDF
assert.deepEqual(termsDocFor('2026-10-05-v4'), { version: '2026-10-05-v4', url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' })
assert.deepEqual(termsDocFor('2026-09-25-v3'), {
version: '2026-09-25-v3',
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
})
assert.deepEqual(termsDocFor(null), {
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
})
for (const none of [null, undefined, '']) {
assert.deepEqual(termsDocFor(none), {
version: '2026-09-25-v3',
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
}, String(none))
}
assert.equal(termsDocFor('2026-09-25-v2').version, '2026-09-25-v2', 'aeltere Fassung bleibt benannt')
})
test('Produktansicht enthält Leistungen ohne 1-Postfach-Regel bei Wartung', () => {
+4 -4
View File
@@ -119,23 +119,23 @@ test('Kündigung nur Zusatzspeicher: Menge 0 ohne Erstattung, Ereignis', async (
})
// Task 12: Betreff und Text nennen das Abo des Kunden (Katalogname ohne „WEBklar “, F33); das Vertragsende steht
// in der Fassung 4 in § 11, nicht in § 9 (F08); das Stundenguthaben gibt es nur im Wartungs-Abo
// in der Fassung 4 in § 11, nicht in § 9 (F08); das Stundenkontingent gibt es nur im Wartungs-Abo (kein Geldguthaben, Fix 1)
test('K03/K04/T08/T09: Produkt des Kunden, Vertragsende nach § 11 der Fassung 4', async () => {
await resetSubH()
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200)
assert.equal(k03().subject, 'Eingangsbestätigung Ihrer Kündigung – WEBklar Hosting-Abo')
assert.ok(k03().text.includes('§ 11 der Vertragsbedingungen'), k03().text)
assert.ok(!k03().text.includes('§ 9 der Vertragsbedingungen') && !k03().text.includes('Stundenguthaben'))
assert.ok(!k03().text.includes('§ 9 der Vertragsbedingungen') && !k03().text.includes('Stundenkontingent') && !k03().text.includes('Stundenguthaben'))
assert.ok(t08().text.includes('Abo: Hosting-Abo'), t08().text)
await resetSubH()
assert.equal((await withdraw({ order: 'abschluss' })).status, 200)
assert.equal(k04().subject, 'Eingangsbestätigung Ihres Widerrufs – WEBklar Hosting-Abo')
assert.ok(t09().text.includes('Abo: Hosting-Abo'), t09().text)
// Wartungs-Abo nach Fassung 3 (Altbestand): § 9, Stundenguthaben
// Wartungs-Abo nach Fassung 3 (Altbestand): § 9, nicht genutzte Arbeitszeit aus dem Stundenkontingent
await resetSubH({ patch: { productKey: 'wartung', termsVersion: '2026-09-25-v3' }, stripe: { productKey: 'wartung', metadata: { termsVersion: '2026-09-25-v3' } } })
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich' })).status, 200)
assert.equal(k03().subject, 'Eingangsbestätigung Ihrer Kündigung – WEBklar Wartungs-Abo')
assert.ok(k03().text.includes('§ 9 der Vertragsbedingungen') && k03().text.includes('Stundenguthaben'), k03().text)
assert.ok(k03().text.includes('§ 9 der Vertragsbedingungen') && k03().text.includes('Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit Vertragsende.'), k03().text)
// Unbekannte E-Mail: kein Abo, neutrale Anrede „Abo“, Paragraph der aktuellen Fassung (F08)
await resetSubH()
assert.equal((await cancel({ contract: 'abo', kind: 'ordentlich', email: 'niemand@example.invalid' })).status, 200)