Mails: Produktname statt fest Wartungs-Abo, neue Mails für Wechsel und Zusatzspeicher, UptimeRobot-Hinweis
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d2254ca20
commit
19f9c747a2
24 files changed
+407
-100
No files matched your search
+51
-1
@@ -27,9 +27,17 @@ const { recordContractEvent, listContractEvents, eventIdFor } = await import('..
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { withdrawalDeadline } = await import('../server/services/deadlines.js')
|
||||
const { offerText } = await import('../server/services/offers.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { mailSink } = await import('./support/mailSink.js')
|
||||
|
||||
const app = await startTestApp({ '/api/abo': aboRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
// F25: Mails landen in einer Senke statt bei SMTP, damit K01 (Anhang je Fassung) und T02 pruefbar sind (Task 12)
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
after(() => {
|
||||
setMailTransportForTests(null)
|
||||
return Promise.all([app.close(), aw.close(), st.close()])
|
||||
})
|
||||
seedFixtures(aw)
|
||||
|
||||
const DAY_MS = 86400000
|
||||
@@ -399,6 +407,48 @@ test('Webhook checkout.session.completed: Ereignis abschluss ab Vertragsschluss,
|
||||
}
|
||||
})
|
||||
|
||||
// Task 12: K01 je Produkt (Katalogname, Preis, Liste „Enthalten“) mit dem PDF der Fassung, der der Kunde zugestimmt
|
||||
// hat (§ 312f BGB, contractAttachmentFor); T02 nennt das Produkt und die Aufgabe fuer UptimeRobot
|
||||
test('Webhook checkout.session.completed: K01 je Produkt mit PDF der zugestimmten Fassung, T02 mit UptimeRobot', async () => {
|
||||
const subH = 'sub_1SD6y2SxAbCdEfGhIjKlMnOs'
|
||||
st.subscriptions.set(subH, stripeSubscription({ id: subH, customerId: 'c3', productKey: 'hosting' }))
|
||||
try {
|
||||
mails.sent.length = 0
|
||||
const res = await postWebhook(checkoutCompleted(subH))
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const k01 = mails.sent.find((m) => m.subject.includes('Vertragsbestätigung'))
|
||||
assert.ok(k01, 'K01 verschickt')
|
||||
assert.equal(k01.subject, 'Ihr WEBklar Hosting-Abo – Vertragsbestätigung')
|
||||
assert.deepEqual(k01.attachments.map((a) => [a.filename, a.contentType]), [['WEBklar-Abo-Vertragsunterlagen.pdf', 'application/pdf']])
|
||||
assert.ok(k01.attachments[0].content.length > 1000, 'PDF-Inhalt angehaengt')
|
||||
for (const text of ['15,00 € / Monat', 'gemäß § 19 UStG ohne Umsatzsteuer', '1 E-Mail-Postfach', `(Version ${config.abo.termsVersion})`]) {
|
||||
assert.ok(k01.text.includes(text), `${text}\n---\n${k01.text}`)
|
||||
}
|
||||
assert.ok(!k01.text.includes('Stunde Arbeitszeit') && !k01.text.includes('Stundenkontingent'), k01.text)
|
||||
assert.ok(aw.docs('subscriptions').get(subH).confirmationSentAt)
|
||||
const t02 = mails.sent.find((m) => m.subject.startsWith('[Abo] Neues'))
|
||||
assert.ok(t02.subject.startsWith('[Abo] Neues Hosting-Abo: '), t02.subject)
|
||||
assert.ok(t02.text.includes('Website als Monitor in UptimeRobot anlegen'), t02.text)
|
||||
} finally {
|
||||
await cleanupWebhook(subH)
|
||||
}
|
||||
// Wartungs-Abo nach Fassung 3 (Altbestand): das Archiv-PDF der Fassung 3, Stunde und beliebig viele Postfaecher
|
||||
const subW = 'sub_1SD6z3TxAbCdEfGhIjKlMnOt'
|
||||
st.subscriptions.set(subW, stripeSubscription({ id: subW, customerId: 'c3', productKey: 'wartung', metadata: { termsVersion: '2026-09-25-v3' } }))
|
||||
try {
|
||||
mails.sent.length = 0
|
||||
assert.equal((await postWebhook(checkoutCompleted(subW, { 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-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)']) {
|
||||
assert.ok(k01.text.includes(text), `${text}\n---\n${k01.text}`)
|
||||
}
|
||||
} finally {
|
||||
await cleanupWebhook(subW)
|
||||
}
|
||||
})
|
||||
|
||||
// Fix 1, Important: ein dauerhafter Fehler beim Nachweis (4xx, z. B. Collection oder Attribut fehlt) darf
|
||||
// Angebot, T02 und K01 nicht blockieren; ein voruebergehender (5xx, Netz) gibt 500, Stripe wiederholt
|
||||
test('Webhook: Nachweis dauerhaft nicht speicherbar (400) → 200, Angebot trotzdem aktiviert', async () => {
|
||||
|
||||
+148
-7
@@ -1,11 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { test } from 'node:test'
|
||||
import { escapeHtml, htmlToText, loadTemplate, renderMail, templateIds } from '../server/services/mailTemplates.js'
|
||||
import { contractAttachmentFor, termsDocFor } from '../server/services/products.js'
|
||||
|
||||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1);
|
||||
// die Endfassung der Texte folgt in Task 12
|
||||
// Endfassung der Texte in Task 12 (T14 „Löschfristen“ kommt mit Task 21 dazu)
|
||||
const EXPECTED = [
|
||||
...Array.from({ length: 11 }, (_, i) => `K${String(i + 1).padStart(2, '0')}`),
|
||||
'K12',
|
||||
@@ -67,7 +68,7 @@ test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden <br>', () => {
|
||||
})
|
||||
|
||||
test('optionale Blöcke: nur eingeschaltete erscheinen', () => {
|
||||
const values = { name: 'Max', 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 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 })
|
||||
assert.doesNotMatch(early.html, /Ihr Angebot:/)
|
||||
assert.match(early.html, /Sie haben ausdrücklich verlangt/)
|
||||
@@ -102,11 +103,13 @@ test('Anrede ohne Namen, fehlende und unbekannte Werte werden gemeldet', () => {
|
||||
})
|
||||
|
||||
test('Textfassung K01: alle Vertragsangaben lesbar', () => {
|
||||
// F34: Produkt und Preis kommen aus dem Katalog, der Produktblock (hier wartung) bringt die Liste „Enthalten“
|
||||
const mail = renderMail(
|
||||
'K01',
|
||||
{ name: 'Erika Musterfrau', 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 }
|
||||
{ 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 }
|
||||
)
|
||||
assert.deepEqual(mail.problems, [])
|
||||
const t = mail.text
|
||||
for (const line of [
|
||||
'Guten Tag Erika Musterfrau,',
|
||||
@@ -266,11 +269,15 @@ test('K03, K04, T08, T09: Vertrag bzw. Bestellung, Pakete und die Blöcke der Au
|
||||
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.match(renderMail('K03', k03, { pruefen: true }).text, /bestätigen Ihnen den Zeitpunkt, zu dem sie wirkt, gesondert per E-Mail/)
|
||||
assert.match(renderMail('K03', k03, { abo: true }).text, /Stundenguthaben verfällt/)
|
||||
// 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/)
|
||||
assert.match(renderMail('K03', k03, { abo: true }).text, /Domain-AuthCode/)
|
||||
|
||||
const k04 = { ...sampleValues(loadTemplate('K04')), bestellung: 'Wechsel ins Wartungs-Abo' }
|
||||
assert.match(renderMail('K04', k04, { wechsel: true }).text, /Widerruf der Bestellung „Wechsel ins Wartungs-Abo“/)
|
||||
assert.match(renderMail('K04', k04, { wechsel: true }).text, /bisherigen Preis/)
|
||||
// Task 12: Wortlaut der Hinweise zum Widerruf (Fassung 4)
|
||||
assert.match(renderMail('K04', k04, { wechsel: true }).text, /zu den bisherigen Bedingungen weiter/)
|
||||
assert.match(renderMail('K04', k04, { zusatzspeicher: true }).text, /Menge vor der Bestellung/)
|
||||
assert.match(renderMail('K04', k04, { abschluss: true }).text, /keine weiteren Abbuchungen/)
|
||||
assert.match(renderMail('K04', k04, { pruefen: true }).text, /prüfen, ob für diese Bestellung eine Widerrufsfrist läuft/)
|
||||
@@ -304,3 +311,137 @@ test('K03, K04, T08, T09: Vertrag bzw. Bestellung, Pakete und die Blöcke der Au
|
||||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, abschluss_verspaetet: true }).text, /als ordentliche Kündigung behandeln/)
|
||||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, bereits_widerrufen: true }).text, /schon widerrufen/)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 12: Mails je Produkt (Hosting-Abo, Wartungs-Abo), Vertragsunterlagen der zugestimmten Fassung,
|
||||
// Paragraph zum Vertragsende je Fassung, Folgen des Widerrufs wie in den Hinweisen der Fassung 4, UptimeRobot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('Neue Vorlagen vorhanden', () => {
|
||||
for (const id of ['K12', 'K13', 'K14', 'T12', 'T13']) assert.ok(templateIds().includes(id), id)
|
||||
})
|
||||
|
||||
test('K01 Hosting: 1 Postfach, keine Arbeitsstunde; Wartung: beliebig viele Postfächer', () => {
|
||||
const base = { name: 'K', produkt: 'Hosting-Abo', preis: '15,00 € / Monat', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026',
|
||||
mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-10-05-v4', zustimmung_zeitpunkt: '01.10.2026, 10:00', angebot_text: '' }
|
||||
const h = renderMail('K01', base, { hosting: true, sofortbeginn: true })
|
||||
assert.equal(h.subject, 'Ihr WEBklar Hosting-Abo – Vertragsbestätigung')
|
||||
assert.ok(h.text.includes('1 E-Mail-Postfach') && !h.text.includes('Stunde Arbeitszeit'), h.text)
|
||||
assert.ok(h.text.includes('15,00 € / Monat') && h.text.includes('gemäß § 19 UStG ohne Umsatzsteuer'))
|
||||
assert.ok(h.text.includes('Ihr Hosting-Abo ist bestätigt.') && h.text.includes('Abschluss Ihres Hosting-Abos'))
|
||||
assert.doesNotMatch(h.text, /Stundenkontingent/)
|
||||
assert.deepEqual(h.problems, [])
|
||||
const w = renderMail('K01', { ...base, produkt: 'Wartungs-Abo', preis: '50,00 € / Monat' }, { wartung: true, sofortbeginn: true })
|
||||
assert.ok(w.text.includes('beliebig viele Postfächer'), w.text)
|
||||
assert.ok(w.text.includes('1 Stunde Arbeitszeit je bezahltem Monat') && w.text.includes('Stundenkontingent'))
|
||||
assert.doesNotMatch(w.text, /1 E-Mail-Postfach\n/)
|
||||
assert.deepEqual(w.problems, [])
|
||||
})
|
||||
|
||||
test('T02 und T07 erinnern an UptimeRobot', () => {
|
||||
// F38: echte Platzhalter, keine Unstimmigkeiten
|
||||
const t02 = renderMail('T02', { produkt: 'Hosting-Abo', email: 'k@example.invalid', kunde_name: 'K', appwrite_id: 'c1', stripe_abo_id: 'sub_1', angebot_text: '' }, {})
|
||||
assert.deepEqual(t02.problems, [])
|
||||
assert.ok(t02.subject.startsWith('[Abo] Neues Hosting-Abo') && t02.text.includes('Website als Monitor in UptimeRobot anlegen'), t02.text)
|
||||
assert.equal(loadTemplate('T02').file, 'T02 Neues Abo.html')
|
||||
assert.match(renderMail('T02', { produkt: 'Hosting-Abo', email: 'k@example.invalid', kunde_name: 'K', appwrite_id: 'c1', stripe_abo_id: 'sub_1', angebot_text: '' }, { nachweis_fehlt: true }).text, /Nachweis nicht gespeichert/)
|
||||
const t07 = renderMail('T07', { produkt: 'Hosting-Abo', email: 'k@example.invalid', stripe_abo_id: 'sub_1', status: 'canceled', vertragsende_paragraph: '§ 11' }, {})
|
||||
assert.deepEqual(t07.problems, [])
|
||||
assert.ok(t07.text.includes('Monitor in UptimeRobot entfernen'), t07.text)
|
||||
})
|
||||
|
||||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14 und T12', () => {
|
||||
for (const id of templateIds()) if (!['K12', 'K14', 'T12'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||||
})
|
||||
|
||||
test('K01-Anhang: Fassung 3 für v3-Abos, Fassung 4 sonst', () => {
|
||||
// contractAttachmentFor(termsVersion) liefert { filename, path } und wird von sendContractConfirmation benutzt
|
||||
assert.equal(contractAttachmentFor('2026-09-25-v3').filename, 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')
|
||||
assert.equal(contractAttachmentFor('2026-10-05-v4').filename, 'WEBklar-Abo-Vertragsunterlagen.pdf')
|
||||
assert.ok(existsSync(contractAttachmentFor('2026-10-05-v4').path))
|
||||
assert.ok(existsSync(contractAttachmentFor('2026-09-25-v3').path))
|
||||
})
|
||||
|
||||
// Rechtsrunde: In Fassung 4 ist § 9 der Wechsel, das Vertragsende steht in § 11 (F08)
|
||||
test('K02, K03, T05, T07: Paragraph zum Vertragsende aus der Fassung des Kunden', () => {
|
||||
assert.equal(termsDocFor('2026-09-25-v3').endParagraph, '§ 9')
|
||||
assert.equal(termsDocFor(null).endParagraph, '§ 9')
|
||||
assert.equal(termsDocFor('2026-10-05-v4').endParagraph, '§ 11')
|
||||
for (const id of ['K02', 'K03', 'T05', 'T07']) {
|
||||
const tpl = loadTemplate(id)
|
||||
assert.ok(tpl.placeholders.includes('vertragsende_paragraph'), id)
|
||||
const v4 = renderMail(id, { ...sampleValues(tpl), vertragsende_paragraph: '§ 11' }, allBlocks(tpl, true))
|
||||
assert.deepEqual(v4.problems, [], id)
|
||||
assert.doesNotMatch(v4.text, /§ 9 der Vertragsbedingungen/, id)
|
||||
assert.match(v4.text, /§ 11 der Vertragsbedingungen/, id)
|
||||
assert.match(renderMail(id, { ...sampleValues(tpl), vertragsende_paragraph: '§ 9' }, allBlocks(tpl, true)).text, /§ 9 der Vertragsbedingungen/, id)
|
||||
}
|
||||
})
|
||||
|
||||
test('K02–K06, T02, T05–T09: Produkt aus dem Katalog statt fest Wartungs-Abo', () => {
|
||||
for (const id of ['K02', 'K03', 'K04', 'K05', 'K06', 'T02', 'T05', 'T06', 'T07', 'T08', 'T09']) {
|
||||
assert.ok(loadTemplate(id).placeholders.includes('produkt'), id)
|
||||
}
|
||||
const values = { name: 'K', produkt: 'Hosting-Abo', eingang_zeitpunkt: '01.10.2026, 10:00', vertragsende: '01.10.2027', vertragsende_paragraph: '§ 11' }
|
||||
const k02 = renderMail('K02', values, {})
|
||||
assert.deepEqual(k02.problems, [])
|
||||
assert.equal(k02.subject, 'Bestätigung Ihrer Kündigung – WEBklar Hosting-Abo')
|
||||
assert.match(k02.text, /Kündigung des WEBklar Hosting-Abos vom 01\.10\.2026, 10:00 Uhr/)
|
||||
assert.doesNotMatch(k02.text, /Stundenkontingent|Abo verwalten|Wartungs-Abo/)
|
||||
const k02w = renderMail('K02', { ...values, produkt: 'Wartungs-Abo' }, { wartung: true })
|
||||
assert.match(k02w.text, /Nicht genutzte Zeit aus dem Stundenkontingent entfällt mit Vertragsende\./)
|
||||
assert.equal(renderMail('K03', { ...sampleValues(loadTemplate('K03')), produkt: 'Wartungs-Abo' }, {}).subject, 'Eingangsbestätigung Ihrer Kündigung – WEBklar Wartungs-Abo')
|
||||
assert.equal(renderMail('K04', { ...sampleValues(loadTemplate('K04')), produkt: 'Hosting-Abo' }, {}).subject, 'Eingangsbestätigung Ihres Widerrufs – WEBklar Hosting-Abo')
|
||||
for (const id of ['T05', 'T06', 'T07', 'T08', 'T09']) {
|
||||
const tpl = loadTemplate(id)
|
||||
assert.match(renderMail(id, { ...sampleValues(tpl), produkt: 'Hosting-Abo' }, allBlocks(tpl, true)).text, /^Abo: Hosting-Abo$/m, id)
|
||||
}
|
||||
})
|
||||
|
||||
// F36: die Folgen des Widerrufs je Bestellung wie in den Hinweisen zum Widerruf der Fassung 4
|
||||
test('K04: Folgen des Widerrufs je Bestellung (Abschluss, Wechsel, Zusatzspeicher, wir prüfen)', () => {
|
||||
const values = { name: 'K', produkt: 'Hosting-Abo', bestellung: 'Abschluss Ihres Abos', eingang_zeitpunkt: '01.10.2026, 10:00', email: 'k@example.invalid', kundennummer: '' }
|
||||
const abschluss = renderMail('K04', values, { abschluss: true })
|
||||
assert.deepEqual(abschluss.problems, [])
|
||||
assert.match(abschluss.text, /keine weiteren Abbuchungen/)
|
||||
assert.match(abschluss.text, /Wertersatz/)
|
||||
assert.match(abschluss.text, /Zusatzspeicher[^.]*erstatten wir vollständig; Wertersatz verlangen wir dafür nicht/)
|
||||
const wechsel = renderMail('K04', { ...values, bestellung: 'Wechsel ins Wartungs-Abo' }, { wechsel: true }).text
|
||||
assert.match(wechsel, /Hosting-Abo läuft zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit/)
|
||||
assert.match(wechsel, /Mehrbetrag/)
|
||||
assert.match(wechsel, /abzüglich des Wertersatzes/)
|
||||
assert.match(wechsel, /§ 357a Abs\. 2 BGB/)
|
||||
const speicher = renderMail('K04', { ...values, bestellung: 'E-Mail-Zusatzspeicher' }, { zusatzspeicher: true }).text
|
||||
assert.match(speicher, /Menge vor der Bestellung/)
|
||||
assert.match(speicher, /erstatten wir vollständig/)
|
||||
assert.match(speicher, /Wertersatz verlangen wir nicht/)
|
||||
assert.doesNotMatch(speicher, /abzüglich/)
|
||||
assert.match(renderMail('K04', values, { pruefen: true }).text, /prüfen, ob für diese Bestellung eine Widerrufsfrist läuft/)
|
||||
})
|
||||
|
||||
test('K12: Monatspreis mit gebuchtem Zusatzspeicher (Block), Widerruf des Wechsels wie in Fassung 4', () => {
|
||||
const values = sampleValues(loadTemplate('K12'))
|
||||
const mit = renderMail('K12', values, { zusatzspeicher: true })
|
||||
assert.deepEqual(mit.problems, [])
|
||||
assert.match(mit.text, /einschließlich Ihres gebuchten E-Mail-Zusatzspeichers/)
|
||||
const ohne = renderMail('K12', values, {})
|
||||
assert.doesNotMatch(ohne.text, /einschließlich Ihres gebuchten/)
|
||||
assert.match(ohne.text, /Widerrufen Sie den Wechsel, läuft Ihr Hosting-Abo zu den bisherigen Bedingungen weiter/)
|
||||
assert.match(ohne.text, /Mehrbetrag/)
|
||||
// K13: Widerruf des Zusatzspeichers ohne Wertersatz
|
||||
const k13 = renderMail('K13', sampleValues(loadTemplate('K13')), { plus: true, bestellung: true })
|
||||
assert.match(k13.text, /erstatten alles, was Sie dafür gezahlt haben\. Wertersatz verlangen wir nicht\./)
|
||||
})
|
||||
|
||||
// T09: die Handgriffe je Bestellung passen zu den Hinweisen zum Widerruf (Zusatzspeicher ohne Wertersatz)
|
||||
test('T09: Handgriffe je Bestellung ohne Wertersatz beim Zusatzspeicher', () => {
|
||||
const t09 = { ...sampleValues(loadTemplate('T09')), produkt: 'Hosting-Abo' }
|
||||
const speicher = renderMail('T09', t09, { abo_gefunden: true, zusatzspeicher: true }).text
|
||||
assert.match(speicher, /vollständig erstatten/)
|
||||
assert.doesNotMatch(speicher, /Wertersatz für die Tage abziehen/)
|
||||
const wechsel = renderMail('T09', t09, { abo_gefunden: true, wechsel: true }).text
|
||||
assert.match(wechsel, /Mehrbetrag/)
|
||||
assert.match(wechsel, /Wertersatz/)
|
||||
const abschluss = renderMail('T09', t09, { abo_gefunden: true, abschluss: true }).text
|
||||
assert.match(abschluss, /Zusatzspeicher[^.]*vollständig/)
|
||||
})
|
||||
@@ -301,6 +301,32 @@ test('Wechsel: Stripe-Aufruf mit Anker jetzt, Metadaten, Ereignis', async () =>
|
||||
assert.deepEqual(logs.warn.filter((w) => /Vorlage (K12|T12)/.test(w)), [])
|
||||
})
|
||||
|
||||
// Task 12 (Spec 3.5): {{monatspreis}} in K12 und T12 ist die Monatsgebühr des Wartungs-Abos mit gebuchtem
|
||||
// Zusatzspeicher; K12 sagt es im Block zusatzspeicher. Auch nach einem gescheiterten Wechsel (T12 aus dem Ereignis)
|
||||
test('K12/T12: Monatspreis mit gebuchtem Zusatzspeicher, auch im Hinweis zum gescheiterten Wechsel', async () => {
|
||||
await resetHosting({ stripe: { storageUnits: 2 } })
|
||||
st.nextSubscriptionUpdate = { pending_update: pendingUpdate(), latest_invoice: switchInvoice({ id: 'in_speicher', status: 'open', amount_due: 4300, hosted_invoice_url: 'https://pay.example/inv2' }) }
|
||||
await switchToWartung('c1', consent, nowUnix())
|
||||
const [k12] = mailsWithSubject(K12_SUBJECT)
|
||||
assert.ok(k12.text.includes('52,00 € / Monat'), k12.text)
|
||||
assert.ok(k12.text.includes('einschließlich Ihres gebuchten E-Mail-Zusatzspeichers'), k12.text)
|
||||
assert.ok(k12.text.includes('43,00 €'))
|
||||
assert.deepEqual(mailsWithSubject(t12Subject('k@example.invalid')), [])
|
||||
const [ev] = await listContractEvents('c1')
|
||||
assert.equal(ev.details.monthlyCents, 5200)
|
||||
await markSwitchFailed(ev.$id, SWITCH_FAILED_REASONS.abgelaufen)
|
||||
const [t12] = mailsWithSubject(t12Subject('k@example.invalid'))
|
||||
assert.ok(t12.text.includes('52,00 €'), t12.text)
|
||||
assert.ok(t12.text.includes('43,00 €'))
|
||||
// Ohne Zusatzspeicher: 50,00 € und kein Block
|
||||
await resetHosting()
|
||||
st.nextSubscriptionUpdate = { latest_invoice: switchInvoice() }
|
||||
await switchToWartung('c1', consent, nowUnix())
|
||||
const [plain] = mailsWithSubject(K12_SUBJECT)
|
||||
assert.ok(plain.text.includes('50,00 € / Monat'), plain.text)
|
||||
assert.ok(!plain.text.includes('einschließlich Ihres gebuchten'))
|
||||
})
|
||||
|
||||
test('Offene Rechnung liefert paymentUrl', async () => {
|
||||
await resetHosting({ id: 'sub_h2', customerId: 'c2', mirror: hosting2Doc })
|
||||
// Karte, Zahlung nicht sofort erfolgreich (3-D Secure): Stripe haelt die Aenderung in pending_update zurueck
|
||||
|
||||
@@ -115,14 +115,17 @@ test('priceIdsOfInvoice: eindeutige Preis-IDs, ohne Lücken', () => {
|
||||
|
||||
// Fix round 1 / B9: feste Form statt Einzelfelder
|
||||
test('Vertragsunterlagen je Version', () => {
|
||||
assert.deepEqual(termsDocFor('2026-10-05-v4'), { url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf' })
|
||||
// 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' })
|
||||
assert.deepEqual(termsDocFor('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',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -118,6 +118,32 @@ test('Kündigung nur Zusatzspeicher: Menge 0 ohne Erstattung, Ereignis', async (
|
||||
assert.equal(aw.docs('subscriptions').get('sub_h').cancelRequestedAt, undefined)
|
||||
})
|
||||
|
||||
// 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
|
||||
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(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
|
||||
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)
|
||||
// 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)
|
||||
const k03u = mails.sent.find((m) => m.subject.startsWith('Eingangsbestätigung Ihrer Kündigung'))
|
||||
assert.equal(k03u.subject, 'Eingangsbestätigung Ihrer Kündigung – WEBklar Abo')
|
||||
assert.ok(k03u.text.includes('§ 11 der Vertragsbedingungen'), k03u.text)
|
||||
})
|
||||
|
||||
test('2 von 3 Paketen ergibt Menge 1', async () => {
|
||||
await resetSubH({ units: 3 })
|
||||
const res = await cancel({ contract: 'zusatzspeicher', packages: '2' })
|
||||
@@ -362,7 +388,7 @@ test('Widerruf Wechsel in der Frist: Ereignis widerrufen, kein automatischer Pre
|
||||
assert.equal(aw.docs('subscriptions').get('sub_h').withdrawalRequestedAt, undefined)
|
||||
// K04 nennt die Bestellung, T09 die Handgriffe und die Frist
|
||||
assert.match(k04().text, /Wechsel ins Wartungs-Abo/)
|
||||
assert.match(k04().text, /bisherigen Preis/)
|
||||
assert.match(k04().text, /zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben/)
|
||||
assert.doesNotMatch(k04().text, /keine weiteren Abbuchungen/)
|
||||
assert.match(t09().text, /RECHTZEITIG/)
|
||||
assert.match(t09().text, /Preis .*zurück/)
|
||||
|
||||
@@ -218,7 +218,7 @@ test('Mail-Senke: Kunden- und Team-Mails landen in der Senke, null schaltet zur
|
||||
values: { name: 'K', antrag_datum: '01.10.2026', antrag_nr: 'A1', begruendung: 'Nachweis fehlt' },
|
||||
attachments: [{ filename: 'a.pdf', content: Buffer.from('PDF'), contentType: 'application/pdf' }],
|
||||
})
|
||||
assert.deepEqual(await sendAdminNotice({ template: 'T06', values: { email: 'k@example.invalid', stripe_abo_id: 'sub_1' } }), { sent: true })
|
||||
assert.deepEqual(await sendAdminNotice({ template: 'T06', values: { email: 'k@example.invalid', produkt: 'Hosting-Abo', stripe_abo_id: 'sub_1' } }), { sent: true })
|
||||
assert.equal(sink.sent.length, 2)
|
||||
const [customerMail, notice] = sink.sent
|
||||
assert.deepEqual(customerMail.to.map((t) => t.address), ['k@example.invalid'])
|
||||
|
||||
Reference in new issue
Block a user