Abschaltgrund und Loeschfrist in der Funktionsansicht (featureView, frist.js)

featureView liefert zusaetzlich disabledReason, disabledAt und loeschungAm (Berliner
Tag, 30 Tage nach disabledAt); alle drei nur im Zustand disabled, sonst null.
Neu termine/frist.js mit LOESCHFRIST_TAGE, loeschzeit und loeschTag (nur zeit.js/util.js).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 13:58:06 +00:00
1 parent 144b053851
commit 35c9800856
4 files changed
+183

No files matched your search

+113
View File
@@ -11,6 +11,7 @@ const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const sf = await import('../server/services/siteFeatures.js')
const { LOESCHFRIST_TAGE, loeschzeit, loeschTag } = await import('../server/services/termine/frist.js')
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin' })
aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei' })
@@ -607,3 +608,115 @@ test('markPaid: gleichzeitige Aufrufe buchen die Zahlung einmal und liefern alle
assert.equal(lesen(id).adminNote.split('System: Zahlung eingegangen').length - 1, 1, 'ein Eintrag in der Notiz')
assert.equal(lesen(id).zahlungMeldung, 'abo_ende')
})
// ------------------------------------------------ E2: Abschaltgrund und Loeschfrist in der Ansicht ---
const TAG_MS = 24 * 60 * 60 * 1000
test('frist: LOESCHFRIST_TAGE sind 30, loeschzeit ist disabledAt plus genau 30 x 24 h', () => {
assert.equal(LOESCHFRIST_TAGE, 30)
const bis = loeschzeit('2026-10-04T23:30:00.000Z')
assert.ok(bis instanceof Date)
assert.equal(bis.toISOString(), '2026-11-03T23:30:00.000Z')
assert.equal(bis.getTime() - Date.parse('2026-10-04T23:30:00.000Z'), 30 * TAG_MS)
// 24-Stunden-Rechnung, keine Wanduhr: ueber die Zeitumstellung am 25.10. hinweg bleiben es 720 Stunden
const ueberUmstellung = loeschzeit('2026-10-10T12:00:00.000Z')
assert.equal(ueberUmstellung.toISOString(), '2026-11-09T12:00:00.000Z')
assert.equal(ueberUmstellung.getTime() - Date.parse('2026-10-10T12:00:00.000Z'), 720 * 60 * 60 * 1000)
// Appwrite liefert +00:00; ein Date wird nicht veraendert
assert.equal(loeschzeit('2026-10-04T23:30:00.000+00:00').toISOString(), '2026-11-03T23:30:00.000Z')
const eingabe = new Date('2026-10-04T23:30:00.000Z')
assert.equal(loeschzeit(eingabe).toISOString(), '2026-11-03T23:30:00.000Z')
assert.equal(eingabe.toISOString(), '2026-10-04T23:30:00.000Z', 'die Eingabe bleibt unveraendert')
})
test('frist: loeschTag ist der Berliner Kalendertag, nicht der UTC-Tag', () => {
assert.equal(loeschTag('2026-10-04T23:30:00.000Z'), '2026-11-04', 'Berlin 00:30 am 04.11., UTC waere der 03.11.')
assert.equal(loeschTag('2026-10-04T10:00:00.000Z'), '2026-11-03')
// Sommerzeit: 31.07. 22:30 UTC + 30 Tage = 30.08. 22:30 UTC = 31.08. 00:30 Berlin
assert.equal(loeschTag('2026-07-31T22:30:00.000Z'), '2026-08-31')
assert.equal(loeschTag(new Date('2026-10-04T23:30:00.000Z')), '2026-11-04')
})
test('frist: ungueltig, leer oder fehlend ergibt null (kein Wurf)', () => {
for (const wert of [undefined, null, '', 'kein Datum', '2026-13-45T00:00:00Z', new Date('x'), NaN, Infinity, {}, []]) {
assert.equal(loeschzeit(wert), null, `loeschzeit(${String(wert)})`)
assert.equal(loeschTag(wert), null, `loeschTag(${String(wert)})`)
}
})
const E2_ZUSTAENDE = [
// [Name, Dokumentfelder, erwartet { disabledReason, disabledAt, loeschungAm }]
['disabled abo_ende', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z', loeschungAm: '2026-11-04' }],
['disabled team', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' },
{ disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z', loeschungAm: '2026-10-31' }],
['disabled abo_ende_vor_einrichtung', { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-02T08:00:00.000Z' },
{ disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-02T08:00:00.000Z', loeschungAm: '2026-11-01' }],
['disabled kunde_geloescht', { state: 'disabled', enabled: false, disabledReason: 'kunde_geloescht', disabledAt: '2026-10-03T08:00:00.000Z' },
{ disabledReason: 'kunde_geloescht', disabledAt: '2026-10-03T08:00:00.000Z', loeschungAm: '2026-11-02' }],
['disabled Altbestand ohne Grund', { state: 'disabled', enabled: false, disabledAt: '2026-10-01T10:00:00.000Z' },
{ disabledReason: null, disabledAt: '2026-10-01T10:00:00.000Z', loeschungAm: '2026-10-31' }],
['disabled ohne disabledAt (Altbestand)', { state: 'disabled', enabled: false, disabledReason: 'team' },
{ disabledReason: 'team', disabledAt: null, loeschungAm: null }],
['disabled mit leerem disabledAt', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '' },
{ disabledReason: 'team', disabledAt: null, loeschungAm: null }],
['disabled mit unlesbarem disabledAt', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: 'kein Datum' },
{ disabledReason: 'team', disabledAt: 'kein Datum', loeschungAm: null }],
// Reste aus frueheren Abschaltungen zaehlen in anderen Zustaenden nie
['active mit Resten', { state: 'active', enabled: true, disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['setup mit Resten', { state: 'setup', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['ordered mit Resten', { state: 'ordered', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['off mit Resten', { state: 'off', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['active ohne Reste', { state: 'active', enabled: true }, { disabledReason: null, disabledAt: null, loeschungAm: null }],
['active nach activate() (null-Felder)', { state: 'active', enabled: true, disabledReason: null, disabledAt: null },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
]
test('featureView: Abschaltgrund, Zeitpunkt und Loeschtag nur im Zustand disabled, sonst alle drei null', async () => {
aw.seed('customers', { $id: 'c_e2', name: 'Kunde E2' })
E2_ZUSTAENDE.forEach(([name, felder], i) => {
aw.seed('websiteProjects', { $id: `p_e2_${i}`, customerId: 'c_e2', projectName: `E2 ${name}` })
aw.seed('portalFeatures', { $id: `f_e2_${i}`, customerId: 'c_e2', projectId: `p_e2_${i}`, featureKey: 'termine', siteKey: 'abcdefghijklmnop', ...felder })
})
aw.seed('websiteProjects', { $id: 'p_e2_ohne', customerId: 'c_e2', projectName: 'E2 ohne Dokument' })
const liste = await sf.listCustomerFeatures('c_e2')
assert.equal(liste.length, E2_ZUSTAENDE.length + 1)
E2_ZUSTAENDE.forEach(([name, , erwartet], i) => {
const view = liste.find((p) => p.projectId === `p_e2_${i}`).features.termine
assert.deepEqual(
{ disabledReason: view.disabledReason, disabledAt: view.disabledAt, loeschungAm: view.loeschungAm },
erwartet,
name
)
})
// kein Dokument = off: alle drei Felder sind da und null; die Felder der anderen Funktion bleiben davon unberuehrt
const ohne = liste.find((p) => p.projectId === 'p_e2_ohne').features
for (const key of Object.keys(ohne)) {
assert.equal(ohne[key].state, 'off', key)
assert.deepEqual([ohne[key].disabledReason, ohne[key].disabledAt, ohne[key].loeschungAm], [null, null, null], key)
}
const erster = liste.find((p) => p.projectId === 'p_e2_0').features
assert.equal(erster.termine.state, 'disabled')
assert.equal(erster.news.state, 'off')
assert.equal(erster.news.loeschungAm, null)
})
test('featureView: nach disable() steht der Grund in der Ansicht und die Frist ist 30 Tage nach disabledAt', async () => {
aw.seed('websiteProjects', { $id: 'p_e2_live', customerId: 'c_e2', projectName: 'E2 live' })
aw.seed('portalFeatures', { $id: 'f_e2_live', customerId: 'c_e2', projectId: 'p_e2_live', featureKey: 'termine', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop' })
const vorher = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([vorher.disabledReason, vorher.disabledAt, vorher.loeschungAm], [null, null, null])
const aus = await sf.disable('f_e2_live', { reason: 'abo_ende' })
const view = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([view.state, view.disabledReason, view.disabledAt], ['disabled', 'abo_ende', aus.disabledAt])
assert.equal(view.loeschungAm, loeschTag(aus.disabledAt))
assert.match(view.loeschungAm, /^\d{4}-\d\d-\d\d$/)
const wieder = await sf.activate('f_e2_live', {})
assert.equal(wieder.state, 'active')
const danach = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([danach.disabledReason, danach.disabledAt, danach.loeschungAm], [null, null, null], 'Wiedereinschalten leert die Anzeige')
})