From cdc8f5c42411dac5071749bcc146f02bccaf032a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:31:06 +0000 Subject: [PATCH] Kundenansicht P6: K02 und K03 nennen das Ende der Abo-Funktionen Co-Authored-By: Claude Sonnet 5.5 --- .../K02 Kuendigungsbestaetigung.html | 7 +++- .../K03 Eingang Kuendigung.html | 7 +++- server/routes/abo.js | 39 ++++++++++++------- server/services/aboModell.js | 12 ++++++ test/publicForms.test.js | 25 ++++++++++++ 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 server/services/aboModell.js diff --git a/server/mail-templates/K02 Kuendigungsbestaetigung.html b/server/mail-templates/K02 Kuendigungsbestaetigung.html index ce1b879..ee7c5de 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, 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). + Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung, funktionen, funktionen_abo + 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); funktionen_abo = Hinweis, dass die Funktionen des Abo-Modells (Kundenansicht neu: Terminplaner, Webshop, Buchungskalender, News-Pinnwand, Öffnungszeiten live) mit dem Abo enden und die Eingaben 30 bzw. 90 Tage gespeichert bleiben (nur Wartungs-Abo; statt funktionen, nie beide). --> @@ -42,6 +42,9 @@ a{color:#111111;} 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. + +Die Funktionen Ihrer Website, die in Ihrem Abo enthalten sind (zum Beispiel Terminplaner, Webshop, Buchungskalender, News-Pinnwand oder Öffnungszeiten live), enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert (beim Webshop und beim Buchungskalender 90 Tage); mit einem neuen Abo in dieser Zeit schalten wir die Funktionen wieder ein. + 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 2549846..907ec93 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, 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). + 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, funktionen_abo + 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); funktionen_abo (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass die Funktionen des Abo-Modells (Kundenansicht neu) mit dem Abo enden und die Eingaben 30 bzw. 90 Tage gespeichert bleiben (statt funktionen, nie beide). --> @@ -82,6 +82,9 @@ a{color:#111111;} 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. + +Die Funktionen Ihrer Website, die in Ihrem Abo enthalten sind (zum Beispiel Terminplaner, Webshop, Buchungskalender, News-Pinnwand oder Öffnungszeiten live), enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert (beim Webshop und beim Buchungskalender 90 Tage); mit einem neuen Abo in dieser Zeit schalten wir die Funktionen wieder ein. + Viele Grüße
Ihr WEBklar-Team diff --git a/server/routes/abo.js b/server/routes/abo.js index d68b2e3..59e77cd 100644 --- a/server/routes/abo.js +++ b/server/routes/abo.js @@ -22,6 +22,7 @@ import { } from '../services/planSwitch.js' import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent, listCustomerFeatureDocs, paidFeatureOrders } from '../services/siteFeatures.js' import { featureConfig } from '../services/featureRegistry.js' +import { istAboModell } from '../services/aboModell.js' import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js' import { ACTIVE_STATUSES, @@ -413,23 +414,35 @@ router.post('/switch', requireSession, async (req, res) => { // --------------------------------------------------------------------------- /** - * Hat der Kunde dieses Abos die Website-Analyse oder den Terminplaner (Funktionsdokument in portalFeatures mit featureKey analyse - * oder termine, Zustand ausser off; ein Dokument ohne Zustand gilt wie off, wie in der Ansicht)? Nur dann gilt der Hinweis in K02 und - * K03 (Block funktionen), dass beide mit dem Wartungs-Abo enden (Entscheidung 2026-10-05). Der Kunde ist der des Abos (sub.customerId, - * wie in customerNameOf); gelesen wird wie sonst auch ueber listCustomerFeatureDocs. Wirft nie: ein Lesefehler zaehlt als ja (der Kunde - * wird eher informiert als nicht), eine Logzeile, die Mail geht in jedem Fall raus. + * Welche Funktionen hat der Kunde dieses Abos? `alt`: die Website-Analyse oder der Terminplaner (Funktionsdokument in portalFeatures + * mit featureKey analyse oder termine, Zustand ausser off; ein Dokument ohne Zustand gilt wie off, wie in der Ansicht); dann gilt der + * Hinweis in K02 und K03 (Block funktionen), dass beide mit dem Wartungs-Abo enden (Entscheidung 2026-10-05). `modell`: mindestens + * ein Dokument des Abo-Modells (istAboModell) in einem Zustand ausser off; dann gilt statt dessen der Hinweis funktionen_abo + * (Kundenansicht neu, P6). Der Kunde ist der des Abos (sub.customerId, wie in customerNameOf); gelesen wird wie sonst auch ueber + * listCustomerFeatureDocs. Wirft nie: ein Lesefehler zaehlt als { alt: true, modell: false } (der Kunde wird eher informiert als + * nicht), eine Logzeile, die Mail geht in jedem Fall raus. */ async function hatAboFunktionen(sub) { try { if (!sub?.customerId) throw new Error('Abo ohne Kunden-ID') const docs = await listCustomerFeatureDocs(sub.customerId) - return docs.some((doc) => ABO_FUNKTIONEN.includes(doc.featureKey) && (doc.state || 'off') !== 'off') + const laeuft = (doc) => (doc.state || 'off') !== 'off' + return { + alt: docs.some((doc) => ABO_FUNKTIONEN.includes(doc.featureKey) && laeuft(doc)), + modell: docs.some((doc) => istAboModell(doc) && laeuft(doc)), + } } catch (err) { console.error(`[abo] Funktionen des Kunden nicht lesbar, der Hinweis auf das Ende von Analyse und Terminplaner bleibt in der Kündigungsbestätigung (Abo ${sub?.$id || '-'}): ${err.message}`) - return true + return { alt: true, modell: false } } } +/** Blöcke funktionen (alter Hinweis) und funktionen_abo (Abo-Modell) der Kündigungsmails K02 und K03; nur beim Wartungs-Abo, sonst wird nichts gelesen. */ +async function funktionenBloecke(sub) { + const { alt, modell } = isWartung(sub) ? await hatAboFunktionen(sub) : { alt: false, modell: false } + return { funktionen: alt && !modell, funktionen_abo: modell } +} + /** Anrede in Kundenmails: Name aus den Kundendaten des Portals, sonst der übergebene Ersatz. */ async function customerNameOf(sub, fallback = '') { const customer = sub?.customerId @@ -497,9 +510,9 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) { vertragsende: fmtDate(endsAt), vertragsende_paragraph: endParagraphOf(sub), }, - // Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo; auf das Ende von Website-Analyse und Terminplaner nur beim - // Wartungs-Abo eines Kunden, der eine von beiden hat (hatAboFunktionen) - blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) && (await hatAboFunktionen(sub)) }, + // Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo; auf das Ende von Website-Analyse und Terminplaner (funktionen) + // bzw. der Funktionen des Abo-Modells (funktionen_abo) nur beim Wartungs-Abo eines Kunden, der sie hat (funktionenBloecke) + blocks: { wartung: isWartung(sub), ...(await funktionenBloecke(sub)) }, }) await updateDocument(config.collections.subscriptions, sub.$id, { cancelRequestedAt: receivedAt.toISOString(), @@ -976,9 +989,9 @@ 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), - // nur fuer einen Kunden, der eine von beiden hat (hatAboFunktionen) - funktionen: !storage && isWartung(sub) && (await hatAboFunktionen(sub)), + // Website-Analyse und Terminplaner (funktionen) bzw. die Funktionen des Abo-Modells (funktionen_abo) enden mit dem + // Wartungs-Abo (Bloecke innerhalb von abo, nie bei der Speicher-Kündigung), nur fuer einen Kunden, der sie hat (funktionenBloecke) + ...(storage ? { funktionen: false, funktionen_abo: false } : await funktionenBloecke(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/server/services/aboModell.js b/server/services/aboModell.js new file mode 100644 index 0000000..71a635a --- /dev/null +++ b/server/services/aboModell.js @@ -0,0 +1,12 @@ +/** + * Abo-Modell der Funktionen (Kundenansicht neu, P5/P6). Blatt-Modul ohne Importe: Routen, Abgleiche und Mails nutzen es gemeinsam. + * + * Ein portalFeatures-Dokument gehört zum Abo-Modell, wenn `unlockedBy` UND `einrichtungAbrechnung` beide 'abo' oder 'projekt' sind + * (Marker, den das Einschalten aus P5 setzt). Dokumente ohne Marker (Kauf-Bestand, Team, die Gratis-Zeiten der alten Ansicht mit + * unlockedBy 'abo') berührt keine neue Logik. + */ + +export const ABRECHNUNGEN = Object.freeze(['abo', 'projekt']) + +/** true, wenn das Dokument zum Abo-Modell gehört; null und undefined sind false. */ +export const istAboModell = (doc) => ABRECHNUNGEN.includes(doc?.unlockedBy) && ABRECHNUNGEN.includes(doc?.einrichtungAbrechnung) diff --git a/test/publicForms.test.js b/test/publicForms.test.js index a87e953..91df2f0 100644 --- a/test/publicForms.test.js +++ b/test/publicForms.test.js @@ -1464,3 +1464,28 @@ test('legal-form.js: Auswahl statt festem Vertrag, packages nur beim Zusatzspeic assert.doesNotMatch(src, /\['Vertrag', 'WEBklar Wartungs-Abo'\]/) assert.match(src, /packages/) }) + +// Kundenansicht P6 (Task 5): Funktionen des Abo-Modells enden mit dem Abo; K02 und K03 sagen es mit dem Block funktionen_abo +test('istAboModell: nur mit beiden Markern aus abo und projekt', async () => { + const { istAboModell } = await import('../server/services/aboModell.js') + for (const [doc, soll] of [[{ unlockedBy: 'abo', einrichtungAbrechnung: 'abo' }, true], [{ unlockedBy: 'projekt', einrichtungAbrechnung: 'projekt' }, true], [{ unlockedBy: 'abo', einrichtungAbrechnung: 'projekt' }, true], + [{ unlockedBy: 'abo' }, false], [{ unlockedBy: 'kauf', einrichtungAbrechnung: 'abo' }, false], [{ unlockedBy: 'team', einrichtungAbrechnung: 'abo' }, false], [{ einrichtungAbrechnung: 'abo' }, false], [{ unlockedBy: 'abo', einrichtungAbrechnung: '' }, false], [null, false], [undefined, false]]) assert.equal(istAboModell(doc), soll, JSON.stringify(doc)) +}) +const MODELL_SATZ = 'enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert' +const MODELL_DOC = { featureKey: 'news', state: 'setup', unlockedBy: 'abo', einrichtungAbrechnung: 'abo' } +for (const [name, doc, erwartet] of [['Abo-Modell-Dokument (news, setup)', MODELL_DOC, true], ['Abo-Modell, Projektkosten', { ...MODELL_DOC, unlockedBy: 'projekt', einrichtungAbrechnung: 'projekt' }, true], ['News ohne Marker (Altbestand)', { featureKey: 'news', state: 'active' }, false], ['Abo-Modell-Dokument im Zustand off', { ...MODELL_DOC, state: 'off' }, false]]) { + test(`K03 und K02, Wartungs-Abo: ${name} => ${erwartet ? 'neuer Hinweis, nicht der alte' : 'kein neuer Hinweis'}`, async () => { + for (const mail of [await mitFunktion(doc, () => kuendigeMitK03(WARTUNG)), await mitFunktion(doc, () => kuendigeMitK02(WARTUNG))]) { + assert.equal(mail.text.includes(MODELL_SATZ), erwartet, mail.text); assert.equal(mail.html.includes('Terminplaner, Webshop'), erwartet) + assert.equal(mail.text.includes(FUNKTIONEN_SATZ), false) + } + }) +} +test('K03: Abo-Modell-Dokument und alte Analyse zusammen: nur der neue Satz; Hosting-Abo: kein Hinweis und kein Lesen', async () => { + await mitFunktion(MODELL_DOC, async () => { + aw.seed('portalFeatures', { $id: 'fk_zweit', customerId: 'c1', projectId: 'fk_p2', featureKey: 'analyse', state: 'active', enabled: true }) + const mail = await kuendigeMitK03(WARTUNG); assert.ok(mail.text.includes(MODELL_SATZ)); assert.equal(mail.text.includes(FUNKTIONEN_SATZ), false) + await deleteDocument(config.collections.portalFeatures, 'fk_zweit').catch(() => {}) + aw.resetListCalls(); assert.equal((await kuendigeMitK03()).text.includes(MODELL_SATZ), false); assert.equal(aw.listCalls('portalFeatures').length, 0) + }) +})