Terminplaner TP2: Kachel mit Bestellweg, Abo-Felder in der Funktionen-Liste

GET /api/funktionen liefert bei termine.verfuegbar: true zusaetzlich bestellbar
(true | false | null bei Lesefehler im Abo), grund (kein_abo | gekuendigt |
nicht_aktiv | null), preis und einrichtung aus dem Register; nicht verfuegbar
bleibt exakt { verfuegbar: false }. Die Kachel zeigt je Zustand Pille und Knopf:
Aus mit Abo "Terminplaner einrichten · 149 €" bzw. "Fragebogen fortsetzen",
Aus ohne Abo keinen Knopf, sondern den Hinweis zum Abo, Zahlung offen und In
Einrichtung "Status ansehen", Aktiv "Termine oeffnen", Abgeschaltet "Termine
ansehen". Zielprojekt: aktiv, ordered/setup, abgeschaltet, Entwurf, erstes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 22:34:50 +00:00
1 parent 8d628ef906
commit 6ae0f67457
5 files changed
+364 -43

No files matched your search

+81 -8
View File
@@ -148,20 +148,93 @@ const termineMit = (mode, pilotEmails = []) => {
config.termine.pilotEmails = pilotEmails
}
after(() => termineMit(MODUS.mode, MODUS.pilotEmails))
const verfuegbar = async (c = cookie) => (await get('/api/funktionen', { cookie: c })).termine
const termineAntwort = async (c = cookie) => (await get('/api/funktionen', { cookie: c })).termine
const verfuegbar = async (c = cookie) => (await termineAntwort(c)).verfuegbar
test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail', async () => {
test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail; false bleibt exakt { verfuegbar: false }', async () => {
termineMit('off')
assert.deepEqual(await verfuegbar(), { verfuegbar: false })
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
termineMit('pilot', ['k@example.invalid'])
assert.deepEqual(await verfuegbar(), { verfuegbar: true })
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: false }, 'Kunde nicht in der Pilotliste')
assert.equal(await verfuegbar(), true)
assert.deepEqual(await termineAntwort(customerCookie('c2')), { verfuegbar: false }, 'Kunde nicht in der Pilotliste')
termineMit('pilot', [])
assert.deepEqual(await verfuegbar(), { verfuegbar: false })
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
termineMit('pilot', ['*'])
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true })
assert.equal(await verfuegbar(customerCookie('c2')), true)
termineMit('on')
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true })
assert.equal(await verfuegbar(customerCookie('c2')), true)
})
// ---- Abo-Felder der Kachel (C10a): bestellbar, grund, preis, einrichtung nur bei verfuegbar: true
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
const angebot = (bestellbar, grund) => ({
verfuegbar: true,
bestellbar,
grund,
preis: config.features.termine.priceLabel,
einrichtung: config.features.termine.setupEstimate,
})
test('GET /api/funktionen: Kunde mit Wartungs-Abo => bestellbar true, grund null, Preis und Einrichtung aus dem Register', async (t) => {
t.after(() => aw.seed('subscriptions', hostingDoc))
termineMit('pilot', ['*'])
wartung()
const termine = await termineAntwort()
assert.deepEqual(termine, angebot(true, null))
assert.ok(termine.preis && termine.einrichtung, 'das Register liefert beide Werte')
assert.deepEqual(Object.keys(termine), ['verfuegbar', 'bestellbar', 'grund', 'preis', 'einrichtung'])
})
test('GET /api/funktionen: ohne Wartungs-Abo (nur Hosting, gar kein Abo, beendet) => bestellbar false, grund kein_abo', async (t) => {
t.after(() => aw.seed('subscriptions', hostingDoc))
termineMit('pilot', ['*'])
assert.deepEqual(await termineAntwort(), angebot(false, 'kein_abo'), 'c1 hat seit seedFixtures nur Hosting')
assert.deepEqual(await termineAntwort(customerCookie('c3')), angebot(false, 'kein_abo'), 'c3 hat gar kein Abo')
wartung({ status: 'canceled', endedAt: '2026-09-01T00:00:00.000Z' })
assert.deepEqual(await termineAntwort(), angebot(false, 'kein_abo'), 'beendetes Wartungs-Abo')
})
test('GET /api/funktionen: Kuendigung vorgemerkt => grund gekuendigt; past_due und trialing => nicht_aktiv', async (t) => {
t.after(() => aw.seed('subscriptions', hostingDoc))
termineMit('pilot', ['*'])
wartung({ cancelAtPeriodEnd: true })
assert.deepEqual(await termineAntwort(), angebot(false, 'gekuendigt'))
wartung({ cancelAt: '2026-11-30T00:00:00.000Z' })
assert.deepEqual(await termineAntwort(), angebot(false, 'gekuendigt'))
wartung({ status: 'past_due' })
assert.deepEqual(await termineAntwort(), angebot(false, 'nicht_aktiv'))
wartung({ status: 'trialing' })
assert.deepEqual(await termineAntwort(), angebot(false, 'nicht_aktiv'))
})
test('GET /api/funktionen: Lesefehler im Abo => bestellbar null, grund null, die Projekte kommen trotzdem; nichts Technisches nach aussen', async (t) => {
t.after(() => aw.seed('subscriptions', hostingDoc))
const fehler = t.mock.method(console, 'error', () => {})
termineMit('pilot', ['*'])
wartung()
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
const res = await get('/api/funktionen', { cookie })
assert.deepEqual(res.termine, angebot(null, null))
for (const id of ['pt1', 'pt2']) assert.ok(res.projects.some((p) => p.projectId === id), `Projekt ${id} ist in der Liste`)
assert.equal(featuresOf({ body: res }, 'pt1').termine.state, 'active', 'die Zustaende der Projekte bleiben')
assert.ok(!JSON.stringify(res).includes('geheimer'))
assert.ok(fehler.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log')
assert.ok(fehler.mock.calls.length >= 1, 'der Fehler steht im Log')
// der naechste Aufruf liest wieder, nichts wird gemerkt
assert.deepEqual(await termineAntwort(), angebot(true, null))
})
test('GET /api/funktionen: ist der Terminplaner nicht verfuegbar, wird das Abo nicht gelesen', async (t) => {
const echt = globalThis.fetch
const abfragen = []
globalThis.fetch = async (url, optionen) => {
if (/\/collections\/subscriptions\/documents/.test(String(url))) abfragen.push(String(url))
return echt(url, optionen)
}
t.after(() => (globalThis.fetch = echt))
termineMit('off')
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
assert.equal(abfragen.length, 0, abfragen.join(' | '))
})
test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher', async () => {