diff --git a/server/mail-templates/K01 Vertragsbestaetigung.html b/server/mail-templates/K01 Vertragsbestaetigung.html index de24fbc..e69fe8e 100644 --- a/server/mail-templates/K01 Vertragsbestaetigung.html +++ b/server/mail-templates/K01 Vertragsbestaetigung.html @@ -8,8 +8,10 @@ Absender: „WEBklar“ Empfänger: Kunde (E-Mail aus der Bestellung) · BCC intern · Anhang: Vertragsunterlagen der zugestimmten Fassung (PDF, contractAttachmentFor) Platzhalter: {{name}}, {{produkt}}, {{preis}}, {{angebot_text}}, {{vertragsschluss}}, {{leistungsbeginn}}, {{mindestlaufzeit_bis}}, {{bedingungen_version}}, {{zustimmung_zeitpunkt}} - Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, hosting, wartung, sofortbeginn, beginn_nach_widerrufsfrist - produkt = Katalogname ohne „WEBklar “ („Hosting-Abo“, „Wartungs-Abo“), preis z. B. „15,00 € / Monat“; genau einer der Blöcke hosting/wartung bringt die Liste „Enthalten“ (Spec 3.1). + Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, hosting, wartung, wartung_v3, sofortbeginn, beginn_nach_widerrufsfrist + produkt = Katalogname ohne „WEBklar “ („Hosting-Abo“, „Wartungs-Abo“), preis z. B. „15,00 € / Monat“; genau einer der Blöcke hosting/wartung/wartung_v3 bringt die Liste „Enthalten“: + hosting und wartung nach Fassung 4 (Spec 3.1), wartung_v3 für ein Wartungs-Abo nach Fassung 3 (termsDocFor nicht v4: „1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat“ wie in den Bedingungen v3 § 3). + bedingungen_version und das PDF im Anhang kommen aus derselben Quelle (termsDocFor(subDoc.termsVersion).version): ohne gespeicherte Fassung beides Fassung 3. --> @@ -48,16 +50,22 @@ a{color:#111111;} •  Betrieb und Pflege Ihrer Website bzw. Web-Anwendung
•  1 Domain
•  beliebig viele Postfächer (zusammen 5 GB; mehr über den Zusatzspeicher)
•  1 Stunde Arbeitszeit je bezahltem Monat (12 Monate nutzbar) + +•  Betrieb und Pflege Ihrer Website bzw. Web-Anwendung
•  1 Domain
•  1 E-Mail-Postfach (mindestens 5 GB)
•  1 Stunde Arbeitszeit pro Monat (nicht genutzte Zeit bleibt 12 Monate verfügbar) +
Zum Kundenportal →
Dort sehen Sie jederzeit Ihr aktuelles Stundenkontingent. + +Dort sehen Sie jederzeit Ihr aktuelles Stundenkontingent. + Dort sehen Sie jederzeit Ihre Verträge und Rechnungen. -Ihre Zustimmung zu den Vertragsbedingungen (Version {{bedingungen_version}}) haben wir am {{zustimmung_zeitpunkt}} Uhr erhalten.

+Ihre Zustimmung zu den Vertragsbedingungen (Version {{bedingungen_version}}) einschließlich des Auftragsverarbeitungsvertrags haben wir am {{zustimmung_zeitpunkt}} Uhr erhalten.

Sie haben ausdrücklich verlangt, dass wir vor Ablauf der Widerrufsfrist mit der Leistung beginnen, und wurden darüber informiert, dass Sie bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahlen. diff --git a/server/mail-templates/K03 Eingang Kuendigung.html b/server/mail-templates/K03 Eingang Kuendigung.html index f595400..39f3ca3 100644 --- a/server/mail-templates/K03 Eingang Kuendigung.html +++ b/server/mail-templates/K03 Eingang Kuendigung.html @@ -9,7 +9,7 @@ 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) = Stundenguthaben. + 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). --> @@ -78,7 +78,7 @@ a{color:#111111;} -Ein bestehendes Stundenguthaben 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. +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. Viele Grüße
Ihr WEBklar-Team diff --git a/server/mail-templates/K12 Wechsel Wartungs-Abo.html b/server/mail-templates/K12 Wechsel Wartungs-Abo.html index d5b540a..5beba73 100644 --- a/server/mail-templates/K12 Wechsel Wartungs-Abo.html +++ b/server/mail-templates/K12 Wechsel Wartungs-Abo.html @@ -43,7 +43,7 @@ a{color:#111111;} -
Wechseltag{{wechseltag}}
Heute fällig{{betrag_heute}}
Monatsgebühr des Wartungs-Abos mit gebuchtem Zusatzspeicher, abzüglich der Gutschrift für die nicht genutzten Tage Ihres Hosting-Abos
Abbuchungmonatlich am Kalendertag des Wechsels
LaufzeitMindestlaufzeit 12 Monate bis {{mindestlaufzeit_bis}}. Kündigung zu diesem Termin bis einen Monat vorher. Danach läuft das Abo unbefristet und ist jederzeit zum Ende des Abrechnungsmonats kündbar.
Enthalten•  Betrieb und Pflege Ihrer Website bzw. Web-Anwendung
•  1 Domain
•  beliebig viele E-Mail-Postfächer (zusammen 5 GB)
•  1 Stunde Arbeitszeit je bezahltem Monat (12 Monate nutzbar)
+
Wechseltag{{wechseltag}}
Heute fällig{{betrag_heute}}
Monatsgebühr des Wartungs-Abos mit gebuchtem Zusatzspeicher, abzüglich der Gutschrift für die nicht genutzten Tage Ihres Hosting-Abos
Abbuchungmonatlich am Kalendertag des Wechsels
LaufzeitMindestlaufzeit 12 Monate bis {{mindestlaufzeit_bis}}. Kündigung zu diesem Termin bis einen Monat vorher. Danach läuft das Abo unbefristet und ist jederzeit zum Ende des Abrechnungsmonats kündbar.
Enthalten•  Betrieb und Pflege Ihrer Website bzw. Web-Anwendung
•  1 Domain
•  beliebig viele E-Mail-Postfächer (zusammen 5 GB)
•  1 Stunde Arbeitszeit je bezahltem Monat (12 Monate nutzbar)
Ihre Postfächer und gebuchter Zusatzspeicher bleiben unverändert. Weitere Postfächer richten wir ein, sobald die Wechselrechnung bezahlt ist; dann steht auch Ihre erste Stunde Arbeitszeit bereit.
Zum Kundenportal →
diff --git a/server/mail-templates/T07 Abo beendet.html b/server/mail-templates/T07 Abo beendet.html index 05720d4..16c2dea 100644 --- a/server/mail-templates/T07 Abo beendet.html +++ b/server/mail-templates/T07 Abo beendet.html @@ -8,7 +8,8 @@ Absender: „WEBklar Kundenportal“ Empfänger: kenso@webklar.com, andrej@webklar.com Platzhalter: {{email}}, {{produkt}}, {{stripe_abo_id}}, {{status}}, {{vertragsende_paragraph}} - produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11). + 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 auf nicht genutzte Arbeitszeit (nur Wartungs-Abo). --> @@ -35,7 +36,7 @@ a{color:#111111;}
Abo beendet
Abo beendet
Kunde{{email}}
Abo{{produkt}}
Stripe-Abo{{stripe_abo_id}}
Status{{status}}
-Ein Restguthaben an Arbeitszeit verfällt mit dem Vertragsende; gebuchter Zusatzspeicher endet mit dem Abo. +Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit dem Vertragsende. Gebuchter Zusatzspeicher endet mit dem Abo.   diff --git a/server/routes/abo.js b/server/routes/abo.js index fb14445..9c67190 100644 --- a/server/routes/abo.js +++ b/server/routes/abo.js @@ -18,7 +18,7 @@ import { reconcilePendingSwitch, switchToWartung, } from '../services/planSwitch.js' -import { PRODUCT_KEYS, contractAttachmentFor, describeItems, productLabel, productView, termsDocFor } from '../services/products.js' +import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4, productLabel, productView, termsDocFor } from '../services/products.js' import { ACTIVE_STATUSES, cancelAtPeriodEnd, @@ -416,12 +416,15 @@ async function customerNameOf(sub, fallback = '') { /** * K01 an den Kunden: Vertragsbestaetigung je Produkt (Katalogname, Preis, Liste "Enthalten" ueber die Bloecke - * hosting/wartung) mit den Vertragsunterlagen der Fassung, der der Kunde zugestimmt hat (§ 312f BGB, - * contractAttachmentFor: v3 Archiv-PDF, v4 aktuelles PDF). Ohne lesbare Datei geht die Mail ohne Anhang raus. + * hosting/wartung, fuer ein Wartungs-Abo nach Fassung 3 wartung_v3 mit der Zeile der Fassung 3) mit den + * Vertragsunterlagen der Fassung, der der Kunde zugestimmt hat (§ 312f BGB, contractAttachmentFor: v3 Archiv-PDF, + * v4 aktuelles PDF). Versionsangabe und Anhang kommen aus derselben Quelle (termsDocFor.version): ohne gespeicherte + * Fassung beides Fassung 3. Ohne lesbare Datei geht die Mail ohne Anhang raus. */ async function sendContractConfirmation(subDoc, fallbackName = '') { if (!subDoc || subDoc.confirmationSentAt || !subDoc.email) return - const attachment = contractAttachmentFor(subDoc.termsVersion) + const terms = termsDocFor(subDoc.termsVersion) + const attachment = contractAttachmentFor(terms.version) const pdf = await readFile(attachment.path).catch((err) => { console.error(`[abo] Vertragsunterlagen ${attachment.filename} nicht lesbar:`, err.message) return null @@ -439,13 +442,14 @@ async function sendContractConfirmation(subDoc, fallbackName = '') { leistungsbeginn: fmtDate(lateStart ? subDoc.trialEnd : subDoc.startedAt), // Abos nach Bedingungen ohne Mindestlaufzeit (vor v3) entstehen nicht mehr; sie hätten hier "–" mindestlaufzeit_bis: fmtDate(minTermEndOf(subDoc)), - bedingungen_version: subDoc.termsVersion || config.abo.termsVersion, + bedingungen_version: terms.version, zustimmung_zeitpunkt: fmtDateTime(subDoc.termsAcceptedAt || Date.now()), }, blocks: { angebot: Boolean(subDoc.offerText), hosting: productKey === 'hosting', - wartung: productKey === 'wartung', + wartung: productKey === 'wartung' && isTermsV4(terms.version), + wartung_v3: productKey === 'wartung' && !isTermsV4(terms.version), sofortbeginn: !lateStart, beginn_nach_widerrufsfrist: lateStart, }, @@ -626,6 +630,8 @@ async function handleStripeEvent(event) { await sendAdminNotice({ template: 'T07', values: { email: sub?.email || '', produkt: productLabelOf(sub), stripe_abo_id: obj.id, status: obj.status, vertragsende_paragraph: endParagraphOf(sub) }, + // Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo (Fix 1) + blocks: { wartung: isWartung(sub) }, }) return } @@ -927,7 +933,7 @@ router.post('/public/cancel', async (req, res) => { kundennummer: Boolean(d.customerNumber), grund: Boolean(d.reason), abo: !storage, - // Stundenguthaben nur im Wartungs-Abo (Block innerhalb von abo) + // Hinweis auf nicht genutzte Arbeitszeit nur im Wartungs-Abo (Block innerhalb von abo) wartung: !storage && isWartung(sub), automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe', sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe', diff --git a/server/services/products.js b/server/services/products.js index 6fe53c6..cb09356 100644 --- a/server/services/products.js +++ b/server/services/products.js @@ -25,17 +25,32 @@ export function getProduct(key) { return product } +/** Die archivierte Fassung 3 (public/vertragsunterlagen/v3): gilt fuer jede Zustimmung ohne Fassung "-v4". */ +export const TERMS_VERSION_V3 = '2026-09-25-v3' + +/** Ist die Fassung die aktuelle Version 4 (Hosting- und Wartungs-Abo)? Aeltere und fehlende Fassungen: nein. */ +export function isTermsV4(termsVersion) { + return typeof termsVersion === 'string' && termsVersion.endsWith('-v4') +} + /** * Vertragsunterlagen zur zugestimmten Fassung: Version 4 (Suffix "-v4") zeigt die aktuellen * Bedingungen samt PDF, jede aeltere Version (auch unbekannt/keine) das Archiv der Version 3. + * version: die Fassung, die eine Mail nennt - die gespeicherte, ohne gespeicherte die archivierte Fassung 3, + * damit Versionsangabe und PDF-Anhang immer zusammenpassen (Task 12 Fix 1). * endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) in dieser Fassung, fuer die * Mails K02/K03/T05/T07 (F08) - in Fassung 4 ist § 9 der Wechsel zwischen den Abos, § 11 das Vertragsende. */ export function termsDocFor(termsVersion) { - if (typeof termsVersion === 'string' && termsVersion.endsWith('-v4')) { - return { url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' } + if (isTermsV4(termsVersion)) { + return { version: termsVersion, url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' } + } + return { + version: typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3, + url: '/vertragsunterlagen/v3/abo-bedingungen.html', + pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 9', } - return { url: '/vertragsunterlagen/v3/abo-bedingungen.html', pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 9' } } /** Katalogname ohne "WEBklar " (z. B. "Hosting-Abo") fuer {{produkt}} in den Mails (F33); ohne Schluessel "Abo". */ diff --git a/test/aboRoutes.test.js b/test/aboRoutes.test.js index 6a164e6..083fe6f 100644 --- a/test/aboRoutes.test.js +++ b/test/aboRoutes.test.js @@ -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 diff --git a/test/mailTemplates.test.js b/test/mailTemplates.test.js index e03f073..850825c 100644 --- a/test/mailTemplates.test.js +++ b/test/mailTemplates.test.js @@ -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/) +}) diff --git a/test/products.test.js b/test/products.test.js index 2c50a32..5e9ced6 100644 --- a/test/products.test.js +++ b/test/products.test.js @@ -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', () => { diff --git a/test/publicForms.test.js b/test/publicForms.test.js index e53082b..3ba3d4f 100644 --- a/test/publicForms.test.js +++ b/test/publicForms.test.js @@ -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)