From f6fc291dfe6af984e369de6e619e8d1a6a65e83f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:49:18 +0000 Subject: [PATCH] =?UTF-8?q?Kundenansicht=20neu=20E-Fix=20M9:=20featureView?= =?UTF-8?q?=20nennt=20den=20L=C3=B6schtag=20auch=20f=C3=BCr=20News,=20Webs?= =?UTF-8?q?hop=20und=20Buchungskalender=20im=20Abo-Modell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit featureView lieferte loeschungAm nur für den Terminplaner und Öffnungszeiten live. Im Abo-Modell werden aber auch News (30 Tage), Webshop und Buchungskalender (90 Tage) nach dem Ende des Abos tatsächlich gelöscht (K39, Löschjobs); "Meine" und WOMS zeigten kein Datum, der Kommentar "bei News verspräche das Datum eine Löschung, die nie kommt" stimmte nicht mehr. Jetzt: abo_ende bei einem Dokument des Abo-Modells (istAboModell) von news, shop und unterkunft ergibt loeschTagNach(disabledAt, loeschFristTage(key)), derselbe Berliner Tag wie in K39. Team-Sperre, Bestand ohne Abo-Modell und der Terminplaner bleiben unverändert. Test in knLoeschfristAnzeige.test.js. (Der Text in WOMS gehört zum WOMS-Teil W2.) Co-Authored-By: Claude Sonnet 5.5 --- server/services/siteFeatures.js | 13 ++++++---- server/services/termine/frist.js | 11 +++++++++ test/knLoeschfristAnzeige.test.js | 41 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 test/knLoeschfristAnzeige.test.js diff --git a/server/services/siteFeatures.js b/server/services/siteFeatures.js index 61d275d..9b4b571 100644 --- a/server/services/siteFeatures.js +++ b/server/services/siteFeatures.js @@ -8,7 +8,8 @@ import { anleitungsLink, featureConfig, featureHaken, featureKeys, kuerze, ticke import { speichereZustimmung, zustimmungFuerSitzung } from './featureConsents.js' import { aboLage, laeuft } from './aboStatus.js' import { berlinDateTime } from './berlinTime.js' -import { loeschTag } from './termine/frist.js' +import { loeschTag, loeschTagNach } from './termine/frist.js' +import { loeschFristTage } from './aboFunktionenMails.js' import { ABRECHNUNGEN, istAboModell } from './aboModell.js' import { ohneAboBisher } from './aboAufbauphase.js' import { ABRECHNUNGS_ZEILEN, createFeatureTicket } from './ticketAdmin.js' @@ -142,9 +143,10 @@ export const unlockedByVon = (doc) => doc?.unlockedBy || (doc?.unlockedByPurchas * 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 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 fuer Oeffnungszeiten live, dort nur beim Abschaltgrund - * abo_ende (bei einer Abschaltung durch das Team gibt es keine Loeschfrist); es ist null, wenn disabledAt fehlt oder nicht lesbar ist. + * loeschungAm gibt es fuer den Terminplaner (R105: der 30-Tage-Loeschlauf in jobs.js), fuer Oeffnungszeiten live (nur beim Abschaltgrund + * abo_ende; bei einer Abschaltung durch das Team gibt es keine Loeschfrist) und, seit dem Abo-Modell (E-Fix M9), fuer News (30 Tage), Webshop und + * Buchungskalender (90 Tage), wenn das Dokument im Abo-Modell mit dem Abo endete (abo_ende): K39 und die Loeschjobs loeschen dann nach der Frist der + * Funktion (loeschFristTage), die Anzeige nennt denselben Berliner Tag. Sonst null; null auch, wenn disabledAt fehlt oder nicht lesbar ist. * disabledReason und disabledAt gelten fuer jede Funktion. * Jede Funktion traegt unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist). Funktionen mit aboInklusive * (Oeffnungszeiten live) tragen zusaetzlich aboEndeAt (Ende des Wartungs-Abos erkannt) und einbau (angestossen | manuell, Ergebnis des Neubaus). @@ -154,6 +156,7 @@ export async function featureView(doc, key = doc?.featureKey) { const abgeschaltet = doc?.state === 'disabled' const mitAbo = featureConfig(key)?.aboInklusive === true const loeschtag = abgeschaltet && (key === 'termine' || (mitAbo && doc.disabledReason === 'abo_ende')) + const aboEndeLoeschung = abgeschaltet && doc.disabledReason === 'abo_ende' && istAboModell(doc) && ['news', 'shop', 'unterkunft'].includes(key) return { state: doc?.state || 'off', siteKey: doc?.siteKey || null, @@ -163,7 +166,7 @@ export async function featureView(doc, key = doc?.featureKey) { invoiceUrl: doc?.invoiceUrl || null, disabledReason: abgeschaltet ? doc.disabledReason || null : null, disabledAt: abgeschaltet ? doc.disabledAt || null : null, - loeschungAm: loeschtag ? loeschTag(doc.disabledAt) : null, + loeschungAm: loeschtag ? loeschTag(doc.disabledAt) : aboEndeLoeschung ? loeschTagNach(doc.disabledAt, loeschFristTage(key)) : null, // unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist) gilt fuer jede Funktion (V7); aboEndeAt und einbau nur mit aboInklusive unlockedBy: doc && doc.state !== 'off' ? unlockedByVon(doc) : null, ...(mitAbo diff --git a/server/services/termine/frist.js b/server/services/termine/frist.js index dddb5d1..c515caa 100644 --- a/server/services/termine/frist.js +++ b/server/services/termine/frist.js @@ -27,3 +27,14 @@ export function loeschTag(disabledAt) { const ende = loeschzeit(disabledAt) return ende ? ausUtc(ende).tag : null } + +/** + * Wie loeschTag, mit der Frist einer anderen Funktion (E-Fix M9: News 30, Webshop und Buchungskalender 90 Tage, aboFunktionenMails.loeschFristTage): + * disabledAt plus tage x 24 Stunden, Berliner Kalendertag. Fehlendes oder ungueltiges disabledAt oder eine ungueltige Frist: null. + */ +export function loeschTagNach(disabledAt, tage) { + const ms = zeitMs(disabledAt) + if (!Number.isFinite(ms) || !Number.isInteger(tage) || tage < 0) return null + const ende = new Date(ms + tage * TAG_MS) + return Number.isNaN(ende.getTime()) ? null : ausUtc(ende).tag +} diff --git a/test/knLoeschfristAnzeige.test.js b/test/knLoeschfristAnzeige.test.js new file mode 100644 index 0000000..4d47793 --- /dev/null +++ b/test/knLoeschfristAnzeige.test.js @@ -0,0 +1,41 @@ +import { test, after } from 'node:test' +import assert from 'node:assert/strict' +import { startAppwriteMock } from './support/appwriteMock.js' +import { startFakeStripe } from './support/fakeStripe.js' +import { useTestEnv } from './support/testEnv.js' + +// E-Fix M9: featureView nennt die Löschfrist auch für News (30 Tage), Webshop und Buchungskalender (90 Tage), wenn die Funktion im Abo-Modell mit dem Abo +// endete (disabledReason abo_ende): derselbe Tag wie in K39 (disabledAt plus loeschFristTage) und in den Löschjobs. +const aw = await startAppwriteMock({ schema: {} }) +const st = await startFakeStripe() +useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) +after(() => Promise.all([aw.close(), st.close()])) +const { featureView } = await import('../server/services/siteFeatures.js') +const { loeschFristTage } = await import('../server/services/aboFunktionenMails.js') + +const BERLIN_TAG = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' }) +const tagNach = (iso, tage) => BERLIN_TAG.format(new Date(Date.parse(iso) + tage * 24 * 60 * 60 * 1000)) +const DISABLED_AT = '2026-10-04T23:30:00.000Z' // spät am Abend (UTC): der Berliner Tag liegt einen Tag später +const aus = (featureKey, over = {}) => ({ featureKey, state: 'disabled', disabledReason: 'abo_ende', disabledAt: DISABLED_AT, unlockedBy: 'abo', einrichtungAbrechnung: 'abo', ...over }) + +test('M9: im Abo-Modell mit abo_ende nennen News, Webshop und Buchungskalender den Löschtag (Frist der Funktion, Berliner Tag wie K39)', async () => { + for (const [key, tage] of [['news', 30], ['shop', 90], ['unterkunft', 90]]) { + assert.equal(loeschFristTage(key), tage, `Frist ${key}`) + const v = await featureView(aus(key)) + assert.equal(v.loeschungAm, tagNach(DISABLED_AT, tage), key) + assert.deepEqual([v.disabledReason, v.disabledAt], ['abo_ende', DISABLED_AT]) + } + // ein fehlendes oder unlesbares disabledAt ergibt kein Datum + assert.equal((await featureView(aus('shop', { disabledAt: '' }))).loeschungAm, null) + assert.equal((await featureView(aus('news', { disabledAt: 'kaputt' }))).loeschungAm, null) +}) + +test('M9: sonst bleibt es bei null (Team-Sperre, Bestand ohne Abo-Modell, ohne Sperre) und beim Terminplaner wie bisher', async () => { + for (const key of ['news', 'shop', 'unterkunft']) { + assert.equal((await featureView(aus(key, { disabledReason: 'team' }))).loeschungAm, null, `${key} Team`) + assert.equal((await featureView(aus(key, { unlockedBy: 'kauf', einrichtungAbrechnung: undefined }))).loeschungAm, null, `${key} Bestand`) + assert.equal((await featureView(aus(key, { state: 'active' }))).loeschungAm, null, `${key} aktiv`) + } + assert.equal((await featureView(aus('termine'))).loeschungAm, tagNach(DISABLED_AT, 30), 'Terminplaner wie bisher (30 Tage)') + assert.equal((await featureView(aus('termine', { disabledReason: 'team', unlockedBy: 'kauf' }))).loeschungAm, tagNach(DISABLED_AT, 30), 'Terminplaner jeder Grund') +})