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:
1 parent
d66dd60a76
commit
36b36bcc4d
7 files changed
+268
-26
No files matched your search
+97
-11
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user