From eae6eee03c7f77ef7a6282d077e910f6e3de1f5f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 14:02:57 +0000 Subject: [PATCH] =?UTF-8?q?Terminplaner=20TP2=20E2:=20Nachbesserung=201=20?= =?UTF-8?q?(L=C3=B6schdatum=20nur=20f=C3=BCr=20den=20Terminplaner)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruling R105: featureView setzt loeschungAm nur für featureKey 'termine' (der 30-Tage-Löschlauf löscht nur Terminplaner-Daten); disabledReason und disabledAt bleiben für alle Funktionen. Test: News ohne Löschdatum, Zeitfenster-Prüfung entfernt, Teamroute-Ansicht pinnt loeschungAm für den Terminplaner. Co-Authored-By: Claude Sonnet 5.5 --- server/services/siteFeatures.js | 6 ++++-- test/funktionenTermine.test.js | 24 +++++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/server/services/siteFeatures.js b/server/services/siteFeatures.js index b546f64..7724807 100644 --- a/server/services/siteFeatures.js +++ b/server/services/siteFeatures.js @@ -124,7 +124,9 @@ async function ticketWoid(ticketId) { * Ansicht einer Funktion fuer Kunde und Team. Abschaltgrund (disabledReason, ohne Grund = Altbestand => null), Zeitpunkt * (disabledAt) und Berliner Loeschtag (loeschungAm, 30 Tage danach, termine/frist.js) gibt es nur im Zustand `disabled`; * in jedem anderen Zustand sind alle drei null, auch wenn am Dokument noch Reste einer frueheren Abschaltung stehen. - * loeschungAm ist null, wenn disabledAt fehlt oder nicht lesbar ist. + * loeschungAm gibt es nur fuer den Terminplaner (R105: der 30-Tage-Loeschlauf in jobs.js loescht nur Terminplaner-Daten, + * bei News verspraeche das Datum eine Loeschung, die nie kommt) und ist null, wenn disabledAt fehlt oder nicht lesbar ist. + * disabledReason und disabledAt gelten fuer jede Funktion. */ async function featureView(doc) { const abgeschaltet = doc?.state === 'disabled' @@ -137,7 +139,7 @@ async function featureView(doc) { invoiceUrl: doc?.invoiceUrl || null, disabledReason: abgeschaltet ? doc.disabledReason || null : null, disabledAt: abgeschaltet ? doc.disabledAt || null : null, - loeschungAm: abgeschaltet ? loeschTag(doc.disabledAt) : null, + loeschungAm: abgeschaltet && doc.featureKey === 'termine' ? loeschTag(doc.disabledAt) : null, } } diff --git a/test/funktionenTermine.test.js b/test/funktionenTermine.test.js index b9c177a..d6683ab 100644 --- a/test/funktionenTermine.test.js +++ b/test/funktionenTermine.test.js @@ -174,11 +174,12 @@ test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher' const pt2 = body.projects.find((p) => p.projectId === 'pt2') assert.deepEqual([pt2.features.termine.state, pt2.features.news.state], ['off', 'off']) assert.deepEqual(Object.keys(pt2.features), ['news', 'termine']) - // Abschaltgrund und Loeschfrist (E2): News ist hier vom Team abgeschaltet, der aktive Terminplaner und das leere Projekt haben nichts + // Abschaltgrund und Loeschfrist (E2, R105): News ist hier vom Team abgeschaltet: Grund und Zeitpunkt ja, ein Loeschdatum nein + // (der 30-Tage-Loeschlauf loescht nur Terminplaner-Daten); der aktive Terminplaner und das leere Projekt haben nichts const aus = pt1.features.news assert.equal(aus.disabledReason, 'team') - assert.ok(Math.abs(Date.parse(aus.disabledAt) - Date.now()) < 60000) - assert.equal(aus.loeschungAm, loeschTag(aus.disabledAt)) + assert.match(aus.disabledAt, /^\d{4}-\d\d-\d\dT[\d:.]+Z$/) + assert.equal(aus.loeschungAm, null) for (const view of [pt1.features.termine, pt2.features.news, pt2.features.termine]) { assert.deepEqual([view.disabledReason, view.disabledAt, view.loeschungAm], [null, null, null]) } @@ -207,6 +208,23 @@ test('GET /api/funktionen: abgeschalteter Terminplaner nennt Grund, Zeitpunkt un assert.equal(termine('pe2_aboende').state, 'disabled') }) +test('Abgeschaltetes News hat Grund und Zeitpunkt, aber kein Loeschdatum; die Teamansicht nennt es fuer den Terminplaner (R105)', async () => { + const lange_her = '2026-01-15T10:00:00.000Z' + aw.seed('websiteProjects', { $id: 'pe3_p', customerId: 'ce3', projectName: 'Alt abgeschaltet' }) + aw.seed('portalFeatures', { $id: 'f_pe3_news', customerId: 'ce3', projectId: 'pe3_p', featureKey: 'news', siteKey: 'abcdefghijklmnop', state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: lange_her }) + aw.seed('portalFeatures', { $id: 'f_pe3_termine', customerId: 'ce3', projectId: 'pe3_p', featureKey: 'termine', siteKey: 'bcdefghijklmnopq', state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: lange_her }) + // Kundenansicht + const kunde = (await get('/api/funktionen', { cookie: customerCookie('ce3') })).projects.find((p) => p.projectId === 'pe3_p').features + assert.deepEqual([kunde.news.state, kunde.news.disabledReason, kunde.news.disabledAt, kunde.news.loeschungAm], ['disabled', 'team', lange_her, null]) + assert.deepEqual([kunde.termine.disabledReason, kunde.termine.disabledAt, kunde.termine.loeschungAm], ['abo_ende', lange_her, '2026-02-14']) + // Teamansicht (adminView, GET /api/team/funktionen/:customerId): dieselben Felder, die WOMS-Anzeige (E12) liest loeschungAm + const team_ansicht = (await get('/api/team/funktionen/ce3', STAFF)).projects.find((p) => p.projectId === 'pe3_p').features + assert.deepEqual([team_ansicht.termine.state, team_ansicht.termine.disabledReason, team_ansicht.termine.disabledAt], ['disabled', 'abo_ende', lange_her]) + assert.equal(team_ansicht.termine.loeschungAm, '2026-02-14', 'abgeschalteter Terminplaner traegt loeschungAm (Berliner Tag, 30 x 24 h spaeter)') + assert.equal(team_ansicht.termine.loeschungAm, loeschTag(lange_her)) + assert.deepEqual([team_ansicht.news.disabledReason, team_ansicht.news.disabledAt, team_ansicht.news.loeschungAm], ['team', lange_her, null]) +}) + // --------------------------------------------------------- Kunde loeschen --- test('Kunde loeschen schaltet seine Terminplaner ab (Notiz Kunde geloescht, Grund kunde_geloescht), nur seine', async () => { aw.seed('customers', { $id: 'kt1', name: 'Weg Salon', email: 'weg@example.invalid' })