Terminplaner-Banner und Meldung nach Abschaltgrund mit Löschdatum

GET /api/termine/einstellungen liefert bei abgeschalteter Funktion
abgeschaltet { grund, seit, loeschungAm }. Die 409-Meldung bei
Schreibversuchen (Code abgeschaltet) und der Banner im Portal
unterscheiden Wartungs-Abo beendet, Abo vor der Einrichtung beendet und
die übrigen Gründe und nennen das Löschdatum, wenn es eines gibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 18:41:03 +00:00
1 parent d66dd60a76
commit 36b36bcc4d
7 files changed
+268 -26

No files matched your search

+97 -11
View File
@@ -1102,10 +1102,29 @@ test('exportSeite: ohne Funktion, mit ungueltigem Cursor oder ungueltiger Grenze
// ---------------------------------------------------------------- Abgeschaltet (disabled)
test('abgeschaltete Funktion: Lesen und Export gehen, jedes Schreiben ist 409', async () => {
const disabled = featureDoc({ $id: 'f_p4_termine', projectId: 'p4', state: 'disabled', enabled: false, siteKey: 'cdefghijklmnopqr' })
const MELDUNG_ALT = 'Der Terminplaner ist für diese Website abgeschaltet. Du kannst Termine nur noch ansehen und exportieren.'
/** Jede Schreib-Route des Portals, wie sie bei einer abgeschalteten Funktion von Projekt `projectId` aufgerufen wird. */
const schreibVersuche = (projectId, einstellung) => [
['PATCH', '/api/termine/einstellungen', { projectId, version: 1, einstellung }],
['POST', '/api/termine/einstellungen/vorlage', { projectId, vorlage: 'salon' }],
['POST', '/api/termine/buchungen', { ...manuellBody(), projectId }],
['POST', '/api/termine/buchungen/b_alt/aktion', { projectId, aktion: 'absagen', grund: 'x' }],
['POST', `/api/termine/ressourcen/${R1}/feed`, { projectId }],
['DELETE', `/api/termine/buchungen/b_alt?projectId=${projectId}`],
]
/** Funktion von p4 abgeschaltet mit Einstellung und einer Buchung; `over` setzt Grund und Zeitpunkt. */
async function seedAbgeschaltet(over = {}) {
const disabled = featureDoc({ $id: 'f_p4_termine', projectId: 'p4', state: 'disabled', enabled: false, siteKey: 'cdefghijklmnopqr', ...over })
aw.seed('portalFeatures', disabled)
await speichere(einstellung(), disabled)
aw.seed('termineBookings', buchungsDoc('b_alt', { featureId: 'f_p4_termine', projectId: 'p4' }))
return disabled
}
test('abgeschaltete Funktion: Lesen und Export gehen, jedes Schreiben ist 409', async () => {
await seedAbgeschaltet()
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.equal(lesen.status, 200)
assert.equal(lesen.body.state, 'disabled')
@@ -1117,15 +1136,7 @@ test('abgeschaltete Funktion: Lesen und Export gehen, jedes Schreiben ist 409',
assert.ok(export_.text.includes('Max Mustermann'))
assert.equal((await send('GET', `/api/termine/zeiten?projectId=p4&leistung=${S_SOFORT}&tag=${TAG}`, { cookie })).status, 200)
const schreiben = [
['PATCH', '/api/termine/einstellungen', { projectId: 'p4', version: 1, einstellung: lesen.body.einstellung }],
['POST', '/api/termine/einstellungen/vorlage', { projectId: 'p4', vorlage: 'salon' }],
['POST', '/api/termine/buchungen', { ...manuellBody(), projectId: 'p4' }],
['POST', '/api/termine/buchungen/b_alt/aktion', { projectId: 'p4', aktion: 'absagen', grund: 'x' }],
['POST', `/api/termine/ressourcen/${R1}/feed`, { projectId: 'p4' }],
['DELETE', '/api/termine/buchungen/b_alt?projectId=p4'],
]
for (const [method, pfad, body] of schreiben) {
for (const [method, pfad, body] of schreibVersuche('p4', lesen.body.einstellung)) {
const res = await send(method, pfad, { cookie, body })
assert.equal(res.status, 409, `${method} ${pfad}`)
assert.equal(res.body.code, 'abgeschaltet')
@@ -1134,6 +1145,81 @@ test('abgeschaltete Funktion: Lesen und Export gehen, jedes Schreiben ist 409',
assert.equal(aw.docs('termineConfig').get('f_p4_termine').version, 1)
})
test('abgeschaltet ohne Grund und Zeitpunkt (Altbestand): abgeschaltet leer, die Meldung bleibt der bisherige Satz', async () => {
await seedAbgeschaltet()
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.deepEqual(lesen.body.abgeschaltet, { grund: null, seit: null, loeschungAm: null })
for (const [method, pfad, body] of schreibVersuche('p4', lesen.body.einstellung)) {
const res = await send(method, pfad, { cookie, body })
assert.equal(res.status, 409, `${method} ${pfad}`)
assert.equal(res.body.code, 'abgeschaltet')
assert.equal(res.body.error, MELDUNG_ALT, `${method} ${pfad}`)
}
})
test('abgeschaltet wegen Abo-Ende: Grund, Löschtag und Meldung mit Wartungs-Abo und Datum; Lesen und Export gehen weiter', async () => {
await seedAbgeschaltet({ disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' })
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.equal(lesen.status, 200)
// 23:30 UTC am 4.10. + 30 Tage = 3.11. 23:30 UTC = 4.11. 00:30 Berliner Zeit (Winterzeit)
assert.deepEqual(lesen.body.abgeschaltet, { grund: 'abo_ende', seit: '2026-10-04T23:30:00.000Z', loeschungAm: '2026-11-04' })
assert.equal((await send('GET', `/api/termine/buchungen?projectId=p4&von=${TAG}&bis=${TAG}`, { cookie })).status, 200)
assert.equal((await rohExport('p4')).res.status, 200)
for (const [method, pfad, body] of schreibVersuche('p4', lesen.body.einstellung)) {
const res = await send(method, pfad, { cookie, body })
assert.equal(res.status, 409, `${method} ${pfad}`)
assert.equal(res.body.code, 'abgeschaltet')
assert.ok(res.body.error.includes('Wartungs-Abo') && res.body.error.includes('04.11.2026'), `${method} ${pfad}: ${res.body.error}`)
assert.equal(res.body.error, 'Der Terminplaner ist abgeschaltet, weil dein Wartungs-Abo beendet ist. Du kannst Termine nur noch ansehen und exportieren. Ab dem 04.11.2026 löschen wir die Daten.')
}
assert.equal(aw.docs('termineBookings').get('b_alt').status, 'bestaetigt')
})
test('abgeschaltet, weil das Abo vor der Einrichtung endete: Meldung nennt "eingerichtet" und das Löschdatum', async () => {
await seedAbgeschaltet({ disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-04T10:00:00.000Z' })
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.deepEqual(lesen.body.abgeschaltet, { grund: 'abo_ende_vor_einrichtung', seit: '2026-10-04T10:00:00.000Z', loeschungAm: '2026-11-03' })
for (const [method, pfad, body] of schreibVersuche('p4', lesen.body.einstellung)) {
const res = await send(method, pfad, { cookie, body })
assert.equal(res.status, 409, `${method} ${pfad}`)
assert.equal(res.body.code, 'abgeschaltet')
assert.ok(res.body.error.includes('eingerichtet') && res.body.error.includes('Wartungs-Abo endete'), `${method} ${pfad}: ${res.body.error}`)
assert.ok(res.body.error.endsWith('Ab dem 03.11.2026 löschen wir die Daten.'), res.body.error)
assert.ok(!/exportieren/.test(res.body.error), 'es gibt nichts zu exportieren')
}
})
test('abgeschaltet durch das Team oder wegen gelöschtem Konto: bisheriger Satz, mit Zeitpunkt zusätzlich das Löschdatum', async () => {
for (const grund of ['team', 'kunde_geloescht']) {
await seedAbgeschaltet({ disabledReason: grund, disabledAt: '2026-10-04T23:30:00.000Z' })
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.deepEqual(lesen.body.abgeschaltet, { grund, seit: '2026-10-04T23:30:00.000Z', loeschungAm: '2026-11-04' })
const res = await send('PATCH', '/api/termine/einstellungen', { cookie, body: { projectId: 'p4', version: 1, einstellung: lesen.body.einstellung } })
assert.equal(res.status, 409, grund)
assert.equal(res.body.code, 'abgeschaltet')
assert.equal(res.body.error, `${MELDUNG_ALT} Ab dem 04.11.2026 löschen wir die Daten.`, grund)
}
})
test('abgeschaltet mit unlesbarem Zeitpunkt: Grund bleibt, Löschtag fehlt und die Meldung nennt kein Datum', async () => {
await seedAbgeschaltet({ disabledReason: 'abo_ende', disabledAt: 'kein Datum' })
const lesen = await send('GET', einstellungenUrl('p4'), { cookie })
assert.deepEqual(lesen.body.abgeschaltet, { grund: 'abo_ende', seit: 'kein Datum', loeschungAm: null })
const res = await send('PATCH', '/api/termine/einstellungen', { cookie, body: { projectId: 'p4', version: 1, einstellung: lesen.body.einstellung } })
assert.equal(res.status, 409)
assert.equal(res.body.error, 'Der Terminplaner ist abgeschaltet, weil dein Wartungs-Abo beendet ist. Du kannst Termine nur noch ansehen und exportieren.')
})
test('aktive Funktion: abgeschaltet ist null, auch wenn am Dokument noch Reste einer früheren Abschaltung stehen', async () => {
await speichere()
assert.equal((await lade()).abgeschaltet, null)
aw.seed('portalFeatures', featureDoc({ disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' }))
resetKontextForTests()
const body = await lade()
assert.equal(body.state, 'active')
assert.equal(body.abgeschaltet, null)
})
// ---------------------------------------------------------------- Kundenansicht
test('Kundenansicht darf PATCH, POST und DELETE (mit Header) und es wird protokolliert; ohne Gastdaten im Protokoll', async () => {