Funktionen: Team-Route fuer die Terminplaner-Einbauanleitung
GET /api/team/funktionen/:customerId/termine/anleitung?projectId= liefert die volle Einbauanleitung (ohne kurz) fuer das WOMS-Panel: stand, warnungen mit Drift-Warnung gegen die Version im Ticket, abschnitte, text, state, ticketWoid, ticketVersion und hindernis (nur in setup). Nur lesend, in jedem Zustand und Modus, ohne customerViewLog. Die Teamansicht GET /:customerId traegt hindernis je Terminplaner in setup, mit einem Abo-Lauf je Anfrage und ohne portalFeatures erneut zu lesen. Die Einbau-Doku nennt die Anleitung je Kunde im WOMS-Panel und die Kurzfassung im Ticket. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f830698dc3
commit
938b7099dd
3 files changed
+399
-7
No files matched your search
@@ -23,6 +23,9 @@ const { deleteCustomerWithPortalAccess } = await import('../server/services/cust
|
||||
const { aboMeldung } = await import('../server/services/featureOrder.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { mailSink } = await import('./support/mailSink.js')
|
||||
const { ausVorlage } = await import('../server/services/termine/vorlagen.js')
|
||||
const { neuerFeedToken } = await import('../server/services/termine/einstellungen.js')
|
||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
|
||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/team': teamRoutes })
|
||||
// Mails (K16 der Abnahme, K15/T16 des nachgelegten Tickets) landen in einer Senke
|
||||
@@ -1039,3 +1042,321 @@ test('ticket bei News (a): derselbe Weg des Webhooks, K15 und T16 der News-Pinnw
|
||||
assert.equal(neu.filter((m) => m.subject === 'Neue Funktion bestellt: News-Pinnwand').length, 1)
|
||||
assert.equal(neu.filter((m) => m.to.some((to) => to.address === k.email)).length, 1, 'K15 an den Kunden')
|
||||
})
|
||||
|
||||
// ------------------------------------------- Team-Route Einbauanleitung (D8) ---
|
||||
// GET /api/team/funktionen/:customerId/termine/anleitung?projectId=: die volle Anleitung (ohne `kurz`) fuer das WOMS-Panel, nur lesend,
|
||||
// in jedem Zustand und bei jedem Modus. Dazu hindernis je setup-Dokument in der Kundenansicht des Teams (GET /:customerId).
|
||||
const ANLEITUNG_IDS = [
|
||||
'modell', 'leistungen', 'ressourcen', 'regeln', 'gastfelder', 'benachrichtigungen',
|
||||
'einbau', 'snippet', 'css', 'datenschutz_sie', 'datenschutz_du', 'checkliste', 'abrechnung',
|
||||
]
|
||||
const ANLEITUNG_KEYS = ['abschnitte', 'hindernis', 'stand', 'state', 'text', 'ticketVersion', 'ticketWoid', 'warnungen'] // sortiert
|
||||
const D8_BETRIEB = {
|
||||
name: 'Salon Muster', email: 'inhaber@salon-muster.example', telefon: '0631 123456', adresse: 'Hauptstraße 1, 67655 Kaiserslautern',
|
||||
kleinunternehmer: false, impressumUrl: 'https://salon-muster.example/impressum', datenschutzUrl: 'https://salon-muster.example/datenschutz', bundesland: 'RP',
|
||||
}
|
||||
/** Salon-Vorlage mit einer zweiten Person; beide Personen tragen einen feedToken (nie in die Antwort). */
|
||||
function d8Einstellung() {
|
||||
const e = ausVorlage('salon', D8_BETRIEB, new Date('2026-10-05T10:00:00Z'))
|
||||
e.ressourcen.push({ id: 'r_ben00001', typ: 'person', name: 'Ben', aktiv: true, plan: { 2: [['08:00', '16:00']] }, feedToken: neuerFeedToken() })
|
||||
return e
|
||||
}
|
||||
const TICKET_MIT_STAND = (version) =>
|
||||
`Funktion: Terminplaner\nProjekt: Salon\nDomain: salon.example\nKunde: Salon\nsiteKey: abcdefghijklmnop\nKonfigurationsstand: Version ${version}, 03.10.2026\n\nSchritte:\n1. WOMS`
|
||||
|
||||
/**
|
||||
* Funktionsdokument wie legeEinrichtung (Abo, Ticket), aber mit einer vollen Einstellung in termineConfig.
|
||||
* einstellung: Objekt | false (kein termineConfig-Dokument) | string (roh gespeichert); ticketDetails ersetzt die Details eines vorhandenen Tickets.
|
||||
*/
|
||||
function legeAnleitung({ einstellung = d8Einstellung(), version = 1, ticketDetails, ...rest } = {}) {
|
||||
const k = legeEinrichtung({ ...rest, einstellung: false })
|
||||
if (einstellung !== false) {
|
||||
aw.seed('termineConfig', {
|
||||
$id: k.docId, customerId: k.customerId, projectId: k.projectId,
|
||||
einstellung: typeof einstellung === 'string' ? einstellung : JSON.stringify(einstellung), version, updatedAt: '2026-10-03T08:15:00.000Z', updatedBy: 'test',
|
||||
})
|
||||
}
|
||||
const ticket = ticketDoc(k.ticketId)
|
||||
if (ticketDetails !== undefined && ticket) {
|
||||
aw.seed('workorders', { $id: k.ticketId, woid: ticket.woid, status: ticket.status, topic: ticket.topic, details: ticketDetails })
|
||||
}
|
||||
return k
|
||||
}
|
||||
const anleitungPfad = (k, query = `projectId=${k.projectId}`) => `/api/team/funktionen/${k.customerId}/termine/anleitung${query ? `?${query}` : ''}`
|
||||
const anleitung = (k, opts = STAFF, query) => rawGet(anleitungPfad(k, query), opts)
|
||||
const warnungDrift = (ticketVersion, jetzt) => `Einstellungen seit der Bestellung geändert (Ticket Version ${ticketVersion}, jetzt ${jetzt}).`
|
||||
|
||||
/** Fuehrt fn aus und zaehlt die schreibenden Anfragen (alles ausser GET) an den Appwrite-Nachbau. */
|
||||
async function zaehleSchreibzugriffe(fn) {
|
||||
const echt = globalThis.fetch
|
||||
const schreibend = []
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
const methode = optionen?.method ?? 'GET'
|
||||
if (methode !== 'GET' && String(url).startsWith(aw.url)) schreibend.push(`${methode} ${String(url).replace(/\?.*/, '')}`)
|
||||
return echt(url, optionen)
|
||||
}
|
||||
try {
|
||||
return { ergebnis: await fn(), schreibend }
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
}
|
||||
}
|
||||
const modusMerken = (t) => {
|
||||
const vorher = { mode: config.termine.mode, pilotEmails: [...config.termine.pilotEmails] }
|
||||
t.after(() => termineMit(vorher.mode, vorher.pilotEmails))
|
||||
}
|
||||
|
||||
test('Anleitung: ohne JWT 401, mit Kundensitzung statt JWT 401/403, die Route gibt es im Kundenrouter nicht (404)', async () => {
|
||||
const k = legeAnleitung()
|
||||
const ohne = await anleitung(k, {})
|
||||
assert.equal(ohne.status, 401)
|
||||
const alsKunde = await anleitung(k, { cookie: customerCookie(k.customerId) })
|
||||
assert.ok(alsKunde.status === 401 || alsKunde.status === 403, `Status ${alsKunde.status}`)
|
||||
assert.ok(!JSON.stringify(alsKunde.body).includes('abschnitte'), 'keine Anleitung fuer den Kunden')
|
||||
// kein Pfad unter /api/funktionen (Kundenrouter), auch nicht mit gueltiger Kundensitzung
|
||||
const kundenpfad = await rawGet(`/api/funktionen/termine/anleitung?projectId=${k.projectId}`, { cookie: customerCookie(k.customerId) })
|
||||
assert.equal(kundenpfad.status, 404)
|
||||
assert.equal((await rawGet('/api/funktionen/termine/anleitung', { cookie: customerCookie(k.customerId) })).status, 404)
|
||||
})
|
||||
|
||||
test('Anleitung: projectId fehlt oder ist leer oder doppelt => 400; fremdes oder unbekanntes Projekt => 403; ohne Dokument => 409 ohne Dokument anzulegen', async () => {
|
||||
const k = legeAnleitung()
|
||||
for (const query of ['', 'projectId=', 'projectId=a&projectId=b', 'projekt=x']) {
|
||||
const res = await anleitung(k, STAFF, query)
|
||||
assert.equal(res.status, 400, query)
|
||||
assert.equal(typeof res.body.error, 'string', query)
|
||||
}
|
||||
// pt3 gehoert c2, nicht dem Kunden in der URL
|
||||
assert.equal((await anleitung(k, STAFF, 'projectId=pt3')).status, 403)
|
||||
assert.equal((await rawGet(`/api/team/funktionen/c1/termine/anleitung?projectId=pt3`, STAFF)).status, 403, 'Projekt von c2 fuer c1')
|
||||
assert.equal((await anleitung(k, STAFF, 'projectId=gibtsnicht')).status, 403)
|
||||
const docs = aw.docs('portalFeatures').size
|
||||
aw.seed('websiteProjects', { $id: 'pt_d8_leer', customerId: 'c1', projectName: 'Ohne Dokument D8' })
|
||||
const ohneDoc = await rawGet('/api/team/funktionen/c1/termine/anleitung?projectId=pt_d8_leer', STAFF)
|
||||
assert.equal(ohneDoc.status, 409)
|
||||
assert.equal(ohneDoc.body.error, 'Für dieses Projekt gibt es noch keinen Terminplaner')
|
||||
assert.equal(ohneDoc.body.error, `Für dieses Projekt gibt es noch ${sf.FEATURE_LABELS.termine.keine}`)
|
||||
assert.equal(aw.docs('portalFeatures').size, docs, 'kein Dokument entstanden')
|
||||
// ein Dokument der News-Pinnwand ist kein Terminplaner
|
||||
const news = legeEinrichtung({ key: 'news', einstellung: false })
|
||||
assert.equal((await rawGet(`/api/team/funktionen/${news.customerId}/termine/anleitung?projectId=${news.projectId}`, STAFF)).status, 409)
|
||||
})
|
||||
|
||||
test('Anleitung: 200 in jedem Zustand und bei Modus off, nur lesend, ohne Kundenansicht-Protokoll, mit state und ohne kurz', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('off')
|
||||
const faelle = {
|
||||
off: { state: 'off', ticket: null, over: { unlockedByPurchase: false, purchaseStatus: '' } },
|
||||
ordered: { state: 'ordered', ticket: null, over: { unlockedByPurchase: false, purchaseStatus: 'ordered' } },
|
||||
setup: { state: 'setup' },
|
||||
active: { state: 'active' },
|
||||
disabled: { state: 'disabled', over: { disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z' } },
|
||||
}
|
||||
const protokoll = aw.docs('customerViewLog').size
|
||||
for (const [state, optionen] of Object.entries(faelle)) {
|
||||
const k = legeAnleitung(optionen)
|
||||
const { ergebnis: res, schreibend } = await zaehleSchreibzugriffe(() => anleitung(k))
|
||||
assert.equal(res.status, 200, `${state}: ${JSON.stringify(res.body)}`)
|
||||
assert.equal(res.body.state, state)
|
||||
assert.deepEqual(Object.keys(res.body).sort(), ANLEITUNG_KEYS, state)
|
||||
assert.ok(!('kurz' in res.body), `${state}: kurz bleibt im Ticket`)
|
||||
assert.deepEqual(schreibend, [], `${state}: die Route schreibt nichts`)
|
||||
}
|
||||
assert.equal(aw.docs('customerViewLog').size, protokoll, 'kein Eintrag im Protokoll der Kundenansicht')
|
||||
})
|
||||
|
||||
test('Anleitung: die Antwort enthaelt keinen feedToken der Einstellung', async () => {
|
||||
const einstellung = d8Einstellung()
|
||||
const tokens = einstellung.ressourcen.map((r) => r.feedToken)
|
||||
assert.equal(tokens.length, 2)
|
||||
assert.ok(tokens.every((tk) => typeof tk === 'string' && tk.length >= 20), 'Ausgangslage: echte Token')
|
||||
const k = legeAnleitung({ einstellung })
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200)
|
||||
const json = JSON.stringify(res.body)
|
||||
for (const tk of tokens) assert.ok(!json.includes(tk), 'feedToken in der Antwort')
|
||||
assert.ok(!json.includes('feedToken'))
|
||||
assert.ok(!('kurz' in res.body))
|
||||
assert.ok(!json.includes('Konfigurationsstand:'), 'die Kurzfassung des Tickets (mit ihrer Standzeile) gehoert nicht in die Antwort')
|
||||
})
|
||||
|
||||
test('Anleitung mit Einstellung: 13 Abschnitte wie der Generator, stand.version 1, Volltext, Warnungen als Liste', async () => {
|
||||
const k = legeAnleitung()
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(res.body.abschnitte.map((a) => a.id), ANLEITUNG_IDS)
|
||||
for (const a of res.body.abschnitte) assert.deepEqual(Object.keys(a).sort(), ['id', 'kopierbar', 'text', 'titel'], a.id)
|
||||
assert.deepEqual(res.body.abschnitte.filter((a) => a.kopierbar).map((a) => a.id), ['snippet', 'css', 'datenschutz_sie', 'datenschutz_du'])
|
||||
assert.deepEqual(res.body.stand, { version: 1, updatedAt: '2026-10-03T08:15:00.000Z' })
|
||||
assert.equal(typeof res.body.text, 'string')
|
||||
assert.ok(res.body.text.includes(res.body.abschnitte[0].titel))
|
||||
assert.ok(Array.isArray(res.body.warnungen) && res.body.warnungen.every((w) => typeof w === 'string'))
|
||||
const snippet = res.body.abschnitte.find((a) => a.id === 'snippet').text
|
||||
assert.ok(snippet.includes('data-webklar-termine="abcdefghijklmnop"'), 'siteKey des Dokuments im Einbau-Code')
|
||||
// eine hoehere Version der Einstellung kommt an
|
||||
const v4 = legeAnleitung({ version: 4 })
|
||||
assert.equal((await anleitung(v4)).body.stand.version, 4)
|
||||
})
|
||||
|
||||
test('Anleitung ohne gespeicherte Einstellung: nur der Einbau-Code und die Warnung, stand.version 0; unlesbare Einstellung ebenso mit der Version des Dokuments', async (t) => {
|
||||
t.mock.method(console, 'error', () => {})
|
||||
const k = legeAnleitung({ einstellung: false })
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(res.body.abschnitte.map((a) => a.id), ['snippet'])
|
||||
assert.ok(res.body.warnungen.includes('Keine Einstellungen gespeichert'), res.body.warnungen.join(' | '))
|
||||
assert.deepEqual(res.body.stand, { version: 0, updatedAt: null })
|
||||
assert.ok(res.body.abschnitte[0].text.includes('abcdefghijklmnop'))
|
||||
// kaputtes JSON im Dokument: wie "keine Einstellung", die Version bleibt sichtbar
|
||||
const kaputt = legeAnleitung({ einstellung: '{kaputt', version: 4 })
|
||||
const r2 = await anleitung(kaputt)
|
||||
assert.equal(r2.status, 200, JSON.stringify(r2.body))
|
||||
assert.deepEqual(r2.body.abschnitte.map((a) => a.id), ['snippet'])
|
||||
assert.ok(r2.body.warnungen.includes('Keine Einstellungen gespeichert'))
|
||||
assert.equal(r2.body.stand.version, 4)
|
||||
})
|
||||
|
||||
test('Anleitung, Drift: Ticket auf Version 1, Einstellung jetzt Version 3 => Warnung am Ende von warnungen, ticketVersion und ticketWoid', async () => {
|
||||
const k = legeAnleitung({ version: 3, ticketDetails: TICKET_MIT_STAND(1) })
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(warnungDrift(1, 3), 'Einstellungen seit der Bestellung geändert (Ticket Version 1, jetzt 3).')
|
||||
assert.equal(res.body.warnungen.at(-1), warnungDrift(1, 3))
|
||||
assert.ok(res.body.warnungen.length > 1, 'sie steht hinter den uebrigen Warnungen')
|
||||
assert.equal(res.body.warnungen.filter((w) => w.startsWith('Einstellungen seit der Bestellung')).length, 1)
|
||||
assert.equal(res.body.ticketVersion, 1)
|
||||
assert.equal(res.body.ticketWoid, ticketDoc(k.ticketId).woid)
|
||||
assert.deepEqual(res.body.stand.version, 3)
|
||||
})
|
||||
|
||||
test('Anleitung, Drift: gleiche oder neuere Version im Ticket, Ticket ohne Standzeile, Ticket geloescht, Funktion ohne ticketId => keine Warnung und kein Fehler', async () => {
|
||||
const faelle = {
|
||||
'gleiche Version': { opts: { version: 3, ticketDetails: TICKET_MIT_STAND(3) }, ticketVersion: 3, hatTicket: true },
|
||||
'Ticket neuer als die Einstellung': { opts: { version: 3, ticketDetails: TICKET_MIT_STAND(5) }, ticketVersion: 5, hatTicket: true },
|
||||
'Ticket ohne Standzeile': { opts: { version: 3 }, ticketVersion: null, hatTicket: true },
|
||||
'Ticket geloescht': { opts: { version: 3, ticket: 'geloescht' }, ticketVersion: null, hatTicket: false },
|
||||
'ohne ticketId': { opts: { version: 3, ticket: null }, ticketVersion: null, hatTicket: false },
|
||||
}
|
||||
for (const [name, { opts, ticketVersion, hatTicket }] of Object.entries(faelle)) {
|
||||
const k = legeAnleitung(opts)
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200, `${name}: ${JSON.stringify(res.body)}`)
|
||||
assert.ok(!res.body.warnungen.some((w) => /seit der Bestellung/.test(w)), `${name}: ${res.body.warnungen.join(' | ')}`)
|
||||
assert.equal(res.body.ticketVersion, ticketVersion, name)
|
||||
assert.equal(res.body.ticketWoid, hatTicket ? ticketDoc(k.ticketId).woid : null, name)
|
||||
}
|
||||
// Ticket aus der Zeit ohne Einstellung (Version 0), inzwischen gespeichert (Version 1)
|
||||
const nullStand = legeAnleitung({ version: 1, ticketDetails: 'Funktion: Terminplaner\nKonfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)' })
|
||||
const r0 = await anleitung(nullStand)
|
||||
assert.equal(r0.body.ticketVersion, 0)
|
||||
assert.equal(r0.body.warnungen.at(-1), warnungDrift(0, 1))
|
||||
})
|
||||
|
||||
test('Anleitung, Drift: ein nicht lesbares Ticket (5xx) ist ein Fehler, keine stille Luecke; ein Aufruf spaeter geht', async (t) => {
|
||||
t.mock.method(console, 'error', () => {})
|
||||
const k = legeAnleitung({ version: 3, ticketDetails: TICKET_MIT_STAND(1) })
|
||||
aw.failNext({ collection: 'workorders', method: 'GET', status: 500, message: 'Nachbau: Ticket kaputt' })
|
||||
const aus = await anleitung(k)
|
||||
assert.ok(aus.status >= 500 && aus.status < 600, `Status ${aus.status}`)
|
||||
assert.equal(typeof aus.body.error, 'string')
|
||||
assert.equal((await anleitung(k)).status, 200)
|
||||
})
|
||||
|
||||
test('Anleitung, hindernis: nur in setup - ohne Abo kein_abo, mit Abo und Einstellung null, ohne Einstellung keine_einstellung; sonst null und das Abo bleibt ungelesen', async () => {
|
||||
const setupOhneAbo = legeAnleitung({ abo: null })
|
||||
assert.equal((await anleitung(setupOhneAbo)).body.hindernis, 'kein_abo')
|
||||
assert.equal((await anleitung(legeAnleitung({ abo: 'hosting' }))).body.hindernis, 'kein_abo', 'Hosting ist kein Wartungs-Abo')
|
||||
const beendet = legeAnleitung({ abo: 'wartung' })
|
||||
aw.seed('subscriptions', subscriptionDoc(`sub_cd${d7Nr}`, { ...wartungDoc, customerId: beendet.customerId, status: 'canceled', endedAt: '2026-10-01T10:00:00.000Z' }))
|
||||
assert.equal((await anleitung(beendet)).body.hindernis, 'kein_abo', 'beendetes Abo')
|
||||
const frei = await anleitung(legeAnleitung())
|
||||
assert.equal(frei.body.hindernis, null)
|
||||
assert.equal(frei.body.state, 'setup')
|
||||
assert.equal((await anleitung(legeAnleitung({ einstellung: false }))).body.hindernis, 'keine_einstellung')
|
||||
// eine vorgemerkte Kuendigung und past_due laufen noch
|
||||
const gekuendigt = legeAnleitung()
|
||||
aw.seed('subscriptions', subscriptionDoc(`sub_cd${d7Nr}`, { ...wartungDoc, customerId: gekuendigt.customerId, cancelAtPeriodEnd: true }))
|
||||
assert.equal((await anleitung(gekuendigt)).body.hindernis, null)
|
||||
|
||||
// active, disabled, ordered und off: null, auch ohne Abo, und das Abo wird gar nicht erst gelesen
|
||||
const vorher = aw.listCalls('subscriptions').length
|
||||
for (const [state, optionen] of Object.entries({
|
||||
active: { state: 'active', abo: null },
|
||||
disabled: { state: 'disabled', abo: null, over: { disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z' } },
|
||||
ordered: { state: 'ordered', abo: null, ticket: null, over: { unlockedByPurchase: false, purchaseStatus: 'ordered' } },
|
||||
off: { state: 'off', abo: null, ticket: null, over: { unlockedByPurchase: false, purchaseStatus: '' } },
|
||||
})) {
|
||||
const res = await anleitung(legeAnleitung(optionen))
|
||||
assert.equal(res.status, 200, state)
|
||||
assert.equal(res.body.hindernis, null, state)
|
||||
}
|
||||
assert.equal(aw.listCalls('subscriptions').length, vorher, 'ausserhalb von setup keine Abo-Abfrage')
|
||||
})
|
||||
|
||||
test('Anleitung, hindernis: Abo nicht lesbar => 200 mit abo_unbekannt (geloggt), nie ein stilles null', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const k = legeAnleitung()
|
||||
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500, message: 'Nachbau: Abo kaputt' })
|
||||
const res = await anleitung(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(res.body.hindernis, 'abo_unbekannt')
|
||||
assert.ok(log.mock.calls.length >= 1, 'der Fehler steht im Log')
|
||||
assert.equal((await anleitung(k)).body.hindernis, null, 'der naechste Aufruf liest wieder')
|
||||
})
|
||||
|
||||
test('Anleitung, Abrechnung: der Rechnungs-Link erscheint, sobald invoiceUrl im Dokument steht (auch bei altem Ticket); Preis aus dem Register', async () => {
|
||||
const k = legeAnleitung({ ticketDetails: TICKET_MIT_STAND(1) })
|
||||
const abrechnung = async () => (await anleitung(k)).body.abschnitte.find((a) => a.id === 'abrechnung').text
|
||||
const davor = await abrechnung()
|
||||
assert.ok(davor.includes('Rechnung folgt im Portal'), davor)
|
||||
assert.ok(davor.includes(config.features.termine.priceLabel), davor)
|
||||
const ticketVorher = { ...ticketDoc(k.ticketId) }
|
||||
// backfillInvoiceLinks traegt den Link spaeter nach: die Anleitung zeigt ihn sofort, das Ticket bleibt, wie es war
|
||||
await updateDocument(config.collections.portalFeatures, k.docId, { invoiceUrl: 'https://invoice.stripe.test/in_d8' })
|
||||
const danach = await abrechnung()
|
||||
assert.ok(danach.includes('https://invoice.stripe.test/in_d8'), danach)
|
||||
assert.ok(!danach.includes('folgt im Portal'), danach)
|
||||
assert.deepEqual(ticketDoc(k.ticketId), ticketVorher, 'das Ticket wird nicht angefasst')
|
||||
})
|
||||
|
||||
// ---- Teamansicht GET /:customerId: hindernis je setup-Dokument
|
||||
|
||||
test('Teamansicht: hindernis je Terminplaner in setup, null in jedem anderen Zustand und bei News; die Funktionsdokumente werden einmal gelesen', async (t) => {
|
||||
const kunde = legeAnleitung() // Abo und Einstellung vorhanden
|
||||
// zweites Projekt desselben Kunden in setup, ohne Einstellung
|
||||
aw.seed('websiteProjects', { $id: 'cdp_zwei', customerId: kunde.customerId, projectName: 'Zweites Projekt' })
|
||||
aw.seed('portalFeatures', { $id: 'f_cdp_zwei_termine', customerId: kunde.customerId, projectId: 'cdp_zwei', featureKey: 'termine', siteKey: 'bcdefghijklmnopq', state: 'setup', enabled: false, unlockedByPurchase: true, purchaseStatus: 'paid' })
|
||||
// drittes Projekt: aktiv, viertes: News in setup
|
||||
aw.seed('websiteProjects', { $id: 'cdp_drei', customerId: kunde.customerId, projectName: 'Drittes Projekt' })
|
||||
aw.seed('portalFeatures', { $id: 'f_cdp_drei_termine', customerId: kunde.customerId, projectId: 'cdp_drei', featureKey: 'termine', siteKey: 'cdefghijklmnopqr', state: 'active', enabled: true })
|
||||
aw.seed('portalFeatures', { $id: 'f_cdp_drei_news', customerId: kunde.customerId, projectId: 'cdp_drei', featureKey: 'news', siteKey: 'defghijklmnopqrs', state: 'setup', enabled: false, unlockedByPurchase: true, purchaseStatus: 'paid' })
|
||||
|
||||
const echt = globalThis.fetch
|
||||
const abfragen = []
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (/\/collections\/portalFeatures\/documents/.test(String(url))) abfragen.push(`${optionen?.method ?? 'GET'} ${String(url).replace(/\?.*/, '')}`)
|
||||
return echt(url, optionen)
|
||||
}
|
||||
t.after(() => (globalThis.fetch = echt))
|
||||
const abosVorher = aw.listCalls('subscriptions').length
|
||||
const res = await get(`/api/team/funktionen/${kunde.customerId}`, STAFF)
|
||||
globalThis.fetch = echt
|
||||
|
||||
assert.equal(abfragen.length, 1, abfragen.join(' | '))
|
||||
const features = (projectId) => res.projects.find((p) => p.projectId === projectId).features
|
||||
assert.equal(features(kunde.projectId).termine.hindernis, null, 'setup mit Abo und Einstellung')
|
||||
assert.equal(features('cdp_zwei').termine.hindernis, 'keine_einstellung', 'setup ohne Einstellung')
|
||||
assert.equal(features('cdp_drei').termine.hindernis, null, 'active')
|
||||
assert.equal(features('cdp_drei').news.hindernis, null, 'News in setup hat keine Freischalt-Hindernisse')
|
||||
assert.equal(features(kunde.projectId).news.hindernis, null, 'kein Dokument')
|
||||
for (const p of res.projects) for (const view of Object.values(p.features)) assert.ok('hindernis' in view, `${p.projectId}: Feld hindernis fehlt`)
|
||||
assert.equal(aw.listCalls('subscriptions').length - abosVorher, 1, 'zwei setup-Dokumente, ein Abo-Lauf (aboMemo je Anfrage)')
|
||||
|
||||
// ohne Abo: kein_abo, und die Ansicht der uebrigen Felder bleibt wie bisher
|
||||
const ohneAbo = legeAnleitung({ abo: null })
|
||||
const r2 = await get(`/api/team/funktionen/${ohneAbo.customerId}`, STAFF)
|
||||
const view = r2.projects.find((p) => p.projectId === ohneAbo.projectId).features.termine
|
||||
assert.equal(view.hindernis, 'kein_abo')
|
||||
assert.equal(view.state, 'setup')
|
||||
assert.equal(view.ticketMissing, false)
|
||||
})
|
||||
Reference in new issue
Block a user