Files
Webklar-Kundenbereich/test/zeitenFreischalten.test.js
T
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00

423 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
// Freischalten von Oeffnungszeiten live (Aufgabe 18, Spec 3.2 Selbstheilung und 4.4): der 5-Minuten-Job (runActivation) setzt 'active',
// wenn das Einbau-Ticket geschlossen ist UND der Kunde Zeiten eingetragen hat (Haken freischaltbar: Hauptplan gespeichert, mindestens
// ein Tag nicht "zu"). Bei active: K16 und der Ersatz-Hash (Haken nachFreischaltung). Dazu heilt der Job Dokumente in setup, denen
// Ticket oder K27 fehlen (holeZeitenNach), je Dokument getrennt abgesichert.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
},
datetimes: {
...ZEITEN_DATETIMES,
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
},
defaultLimit: 25,
})
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const { zeitenDeps } = await import('../server/services/zeiten/neubau.js')
const { ladeZeiten, speichereZeiten } = await import('../server/services/zeiten/speicher.js')
const { wochenplanHash } = await import('../server/services/zeiten/ersatz.js')
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
after(() => aw.close())
after(() => setMailTransportForTests(null))
const Z = config.features.zeiten
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
const K16_BETREFF = /bereit/
const K27_BETREFF = 'Öffnungszeiten live ist eingeschaltet'
const feld = (id, name) => aw.docs('portalFeatures').get(id)[name]
const zustand = (id) => feld(id, 'state')
const tickets = () => [...aw.docs('workorders').values()]
const k16 = () => sink.sent.filter((m) => K16_BETREFF.test(m.subject))
const k27 = () => sink.sent.filter((m) => m.subject === K27_BETREFF)
const logText = () => console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
function tage(modi) {
const t = {}
for (let d = 1; d <= 7; d += 1) {
const modus = modi[d] ?? 'zu'
t[d] = { modus, fenster: modus === 'zeiten' ? [['09:00', '18:00']] : [] }
}
return t
}
const WOCHE = { 1: 'zeiten', 2: 'zeiten', 3: 'zeiten', 4: 'zeiten', 5: 'zeiten' }
const eingabe = (modi = WOCHE) => ({
land: 'RP',
plaene: [{ id: 'p_aaaaaaa1', name: 'Standard', haupt: true, tage: tage(modi) }],
feiertage: {},
ausnahmen: [],
hinweise: [],
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
})
let nr = 0
/**
* Dokument der Zeiten im Zustand setup mit Projekt, Kunde und Ticket (Standard: geschlossen), optional mit gespeicherten Zeiten
* (zeiten: Wochenplan wie in tage()). Standard: abo, einbau gesetzt und einschaltMailAt gesetzt, damit nur die Freischaltung etwas bewegt.
*/
async function einrichtung({ customerId = 'c1', ticket = 'Closed', zeiten = null, ...over } = {}) {
nr += 1
const projectId = `p${nr}`
const id = `f_${projectId}_zeiten`
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Projekt ${nr}`, subdomain: projectId, repoFullName: `JUSN/${projectId}`, previewMode: 'auto', previewDetected: 'static' })
if (ticket) aw.seed('workorders', { $id: `t_${projectId}`, woid: `WO-${nr}`, status: ticket, topic: 'x' })
aw.seed('portalFeatures', {
$id: id,
customerId,
projectId,
featureKey: 'zeiten',
state: 'setup',
enabled: false,
siteKey: 'abcdefghijklmnop',
unlockedBy: 'abo',
einbau: 'angestossen',
einschaltMailAt: JETZT.toISOString(),
...(ticket ? { ticketId: `t_${projectId}` } : {}),
...over,
})
if (zeiten) await speichereZeiten({ feature: aw.docs('portalFeatures').get(id), eingabe: eingabe(zeiten), version: 0, now: JETZT })
return id
}
let sink
let rebuilds
const originalRebuild = zeitenDeps.rebuild
beforeEach(async () => {
for (const col of ['portalFeatures', 'workorders', 'websiteProjects', 'oeffnungszeiten', 'customers']) {
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
resetZeitenCacheForTests()
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
aw.seed('customers', { $id: 'c2', appwriteUserId: 'u2', email: 'k2@example.invalid', name: 'Kunde Zwei', status: 'active', portalAccessEnabled: true })
for (const name of ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'freischaltbar', 'nachFreischaltung']) delete Z[name]
sink = mailSink()
setMailTransportForTests(sink)
rebuilds = []
zeitenDeps.rebuild = async (p) => {
rebuilds.push(p.$id)
return { queued: true }
}
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => {
mock.restoreAll()
zeitenDeps.rebuild = originalRebuild
})
// ------------------------------------------------------------------------------------------------ Freischalten
test('Ticket zu, aber keine Zeiten ⇒ bleibt setup, kein K16, kein Ersatz-Hash', async () => {
const id = await einrichtung()
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(feld(id, 'enabled'), false)
assert.equal(sink.sent.length, 0)
assert.equal(await ladeZeiten(id), null, 'ohne Zeiten entsteht auch kein Zeiten-Dokument')
// jeder weitere Lauf ändert nichts; die Wartenotiz steht genau einmal (kein Schreiben alle 5 Minuten)
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(String(feld(id, 'adminNote')).split(' | ').filter((e) => e.includes('Freischaltung wartet')).length, 1)
})
test('Ticket zu, Zeiten gespeichert, aber alle Tage „zu“ ⇒ bleibt setup', async () => {
const id = await einrichtung({ zeiten: {} })
assert.ok((await ladeZeiten(id)).daten, 'die Zeiten sind gespeichert')
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(sink.sent.length, 0)
assert.equal((await ladeZeiten(id)).ersatzHash, '')
})
test('Zeiten ohne Hauptplan (nur ein Nebenplan) oder ohne Pläne ⇒ bleibt setup', async () => {
const nebenplan = await einrichtung()
aw.seed('oeffnungszeiten', {
$id: nebenplan, customerId: 'c1', projectId: aw.docs('portalFeatures').get(nebenplan).projectId, featureId: nebenplan, version: 1, teamVersion: 0,
daten: JSON.stringify({ land: 'RP', plaene: [{ id: 'p_bbbbbbb1', name: 'Nebenplan', haupt: false, tage: tage(WOCHE) }] }),
})
const leer = await einrichtung()
aw.seed('oeffnungszeiten', {
$id: leer, customerId: 'c1', projectId: aw.docs('portalFeatures').get(leer).projectId, featureId: leer, version: 1, teamVersion: 0,
daten: JSON.stringify({ land: 'RP', plaene: [] }),
})
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.deepEqual([zustand(nebenplan), zustand(leer)], ['setup', 'setup'])
assert.equal(sink.sent.length, 0)
})
test('Ticket zu und Zeiten gespeichert ⇒ active, genau eine K16, ersatzHash gesetzt', async () => {
const id = await einrichtung({ zeiten: WOCHE })
const vorher = await ladeZeiten(id)
assert.equal(vorher.ersatzHash, '')
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(feld(id, 'enabled'), true)
assert.equal(Date.parse(feld(id, 'activatedAt')), JETZT.getTime())
assert.equal(k16().length, 1)
assert.deepEqual(k16()[0].to.map((a) => a.address), ['k@example.invalid'])
assert.equal(k27().length, 0)
const nachher = await ladeZeiten(id)
assert.match(nachher.ersatzHash, /^[0-9a-f]{16}$/)
assert.equal(nachher.ersatzHash, wochenplanHash(vorher.daten))
// der Ersatz-Hash fasst weder Daten noch Versionen an: ein offener Editor wird nicht veraltet
assert.deepEqual([nachher.version, nachher.teamVersion, nachher.daten], [vorher.version, vorher.teamVersion, vorher.daten])
// ein zweiter Lauf: nichts mehr zu tun, keine zweite Mail
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(k16().length, 1)
})
test('ein einziger Tag, der nicht „zu“ ist, reicht (auch „rund“ oder „nach Vereinbarung“)', async () => {
const rund = await einrichtung({ zeiten: { 7: 'rund' } })
const vereinbarung = await einrichtung({ zeiten: { 3: 'vereinbarung' } })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id).sort(), [rund, vereinbarung].sort())
assert.equal(k16().length, 2)
})
test('Ticket noch offen ⇒ bleibt setup, auch mit Zeiten (der Job schaltet nur nach dem Schließen frei)', async () => {
const id = await einrichtung({ ticket: 'Open', zeiten: WOCHE })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(sink.sent.length, 0)
assert.equal((await ladeZeiten(id)).ersatzHash, '')
})
test('Zeiten kommen nach dem Schließen des Tickets ⇒ der nächste Lauf schaltet frei', async () => {
const id = await einrichtung()
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
await speichereZeiten({ feature: aw.docs('portalFeatures').get(id), eingabe: eingabe(), version: 0, now: JETZT })
assert.deepEqual((await sf.runActivation({ now: JETZT })).map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(k16().length, 1)
})
test('Lesefehler im Haken ⇒ bleibt setup, kein Wurf, kein K16; der nächste Lauf schaltet frei', async () => {
const id = await einrichtung({ zeiten: WOCHE })
aw.failNext({ collection: 'oeffnungszeiten', method: 'GET', status: 500, message: 'Appwrite down' })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(sink.sent.length, 0)
assert.equal(feld(id, 'enabled'), false)
assert.deepEqual((await sf.runActivation({ now: JETZT })).map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
})
test('Haken freischaltbar der Zeiten: Hauptplan mit offenem Tag ⇒ true, sonst false, Lesefehler ⇒ false (fail-closed)', async () => {
const { freischaltbar } = reg.featureHaken('zeiten')
assert.equal(typeof freischaltbar, 'function')
const offen = await einrichtung({ zeiten: { 2: 'zeiten' } })
const zu = await einrichtung({ zeiten: {} })
const ohne = await einrichtung()
assert.equal(await freischaltbar(aw.docs('portalFeatures').get(offen)), true)
assert.equal(await freischaltbar(aw.docs('portalFeatures').get(zu)), false)
assert.equal(await freischaltbar(aw.docs('portalFeatures').get(ohne)), false)
assert.equal(await freischaltbar({ $id: 'gibt-es-nicht' }), false)
aw.failNext({ collection: 'oeffnungszeiten', method: 'GET', status: 500 })
assert.equal(await freischaltbar(aw.docs('portalFeatures').get(offen)), false)
})
// ----------------------------------------------------------------------------------------------- Ersatz-Hash
test('nachFreischaltung scheitert (Ersatz-Hash nicht schreibbar) ⇒ trotzdem active, K16 geht raus, Fehler im Log', async () => {
const id = await einrichtung({ zeiten: WOCHE })
aw.failNext({ collection: 'oeffnungszeiten', method: 'PATCH', status: 500, message: 'Schreiben kaputt' })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(k16().length, 1)
assert.equal((await ladeZeiten(id)).ersatzHash, '', 'nicht gesetzt')
assert.match(logText(), /Schreiben kaputt/)
})
test('nachFreischaltung: nur Zeiten haben den Haken (News und Terminplaner nicht)', () => {
assert.equal(typeof reg.featureHaken('zeiten').nachFreischaltung, 'function')
assert.equal(reg.featureHaken('news').nachFreischaltung, undefined)
assert.equal(reg.featureHaken('termine').nachFreischaltung, undefined)
})
test('nachFreischaltung des Registers wird je freigeschaltetem Dokument mit dem aktiven Dokument gerufen; ein Wurf stoppt die übrigen nicht', async () => {
const a = await einrichtung({ zeiten: WOCHE })
const b = await einrichtung({ zeiten: WOCHE })
const gerufen = []
Z.nachFreischaltung = async (doc) => {
gerufen.push([doc.$id, doc.state])
if (doc.$id === a) throw new Error('Haken kaputt')
}
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id).sort(), [a, b].sort())
assert.deepEqual(gerufen.sort(), [[a, 'active'], [b, 'active']].sort())
assert.equal(k16().length, 2, 'beide Kunden bekommen ihre K16')
assert.match(logText(), /Haken kaputt/)
})
// ------------------------------------------------------------------------------ Freischalten (Abnahme) durch das Team
// Merge mit TP2: freischaltenNachAbnahme (WOMS "Freischalten (Abnahme)") gilt fuer jede Funktion mit ticketFreischaltung. Die
// Voraussetzung der Zeiten ist ihr Haken freischaltbar (eingetragene Zeiten), nicht Abo und Einstellung des Terminplaners.
test('Abnahme der Zeiten: ohne eingetragene Zeiten 409 und alles bleibt setup, auch ohne laufendes Wartungs-Abo', async () => {
const id = await einrichtung({ ticket: 'Open' })
const projectId = feld(id, 'projectId')
await assert.rejects(sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', by: 'Kenso', now: JETZT }), (err) => {
assert.equal(err.status, 409)
assert.match(err.message, /Voraussetzungen/)
return true
})
assert.equal(zustand(id), 'setup')
assert.equal(sink.sent.length, 0)
})
test('Abnahme der Zeiten: mit eingetragenen Zeiten active, K16, Ersatz-Hash (nachFreischaltung) und ohne Abo-Prüfung des Terminplaners', async () => {
const id = await einrichtung({ ticket: 'Open', zeiten: WOCHE })
const projectId = feld(id, 'projectId')
const vorher = await ladeZeiten(id)
assert.equal(vorher.ersatzHash, '')
const { doc, activated } = await sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', by: 'Kenso', now: JETZT })
assert.equal(activated, true)
assert.equal(doc.state, 'active')
assert.equal(zustand(id), 'active')
assert.equal(k16().length, 1)
assert.equal((await ladeZeiten(id)).ersatzHash, wochenplanHash(vorher.daten))
// schon aktiv: Erfolg ohne zweite Mail
assert.equal((await sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', now: JETZT })).activated, false)
assert.equal(k16().length, 1)
})
// ------------------------------------------------------------------------------------------- Selbstheilung
test('Selbstheilung: setup ohne ticketId ⇒ der Job legt das Ticket an (und sendet die K27)', async () => {
const id = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(tickets().length, 1)
assert.equal(feld(id, 'ticketId'), tickets()[0].$id)
assert.match(tickets()[0].topic, /^Öffnungszeiten live einbauen – Projekt \d+$/)
assert.equal(feld(id, 'einbau'), 'angestossen')
assert.equal(rebuilds.length, 1, 'einbau war leer: ein Neubau')
assert.equal(k27().length, 1)
assert.ok(feld(id, 'einschaltMailAt'))
// kein zweites Ticket, kein zweiter Neubau, keine zweite Mail
await sf.runActivation({ now: JETZT })
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [1, 1, 1])
})
test('Selbstheilung: unlockedBy abo ohne einschaltMailAt ⇒ der Job sendet die K27 genau einmal', async () => {
const id = await einrichtung({ ticket: 'Open', einschaltMailAt: null })
await sf.runActivation({ now: JETZT })
await sf.runActivation({ now: JETZT })
await sf.runActivation({ now: JETZT })
assert.equal(k27().length, 1)
assert.deepEqual(k27()[0].to.map((a) => a.address), ['k@example.invalid'])
assert.equal(Date.parse(feld(id, 'einschaltMailAt')), JETZT.getTime())
assert.equal(tickets().length, 1, 'das Ticket war da: kein neues')
assert.deepEqual(rebuilds, [])
assert.equal(zustand(id), 'setup')
})
test('Selbstheilung: K27 nicht gesendet (Mail scheitert) ⇒ einschaltMailAt bleibt leer, der nächste Lauf sendet nach', async () => {
const id = await einrichtung({ ticket: 'Open', einschaltMailAt: null })
setMailTransportForTests({ async sendMail() { throw new Error('SMTP down') } })
await sf.runActivation({ now: JETZT })
assert.ok(!feld(id, 'einschaltMailAt'))
setMailTransportForTests(sink)
await sf.runActivation({ now: JETZT })
assert.equal(k27().length, 1)
assert.ok(feld(id, 'einschaltMailAt'))
})
test('Selbstheilung nur für setup und nicht für kauf; Team bekommt Ticket, aber keine K27', async () => {
const kauf = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, unlockedBy: 'kauf', unlockedByPurchase: true })
const aktiv = await einrichtung({ ticket: null, state: 'active', enabled: true, einschaltMailAt: null })
const aus = await einrichtung({ ticket: null, state: 'off', einschaltMailAt: null })
const team = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, unlockedBy: 'team' })
await sf.runActivation({ now: JETZT })
assert.equal(tickets().length, 1, 'nur das Team-Dokument bekam ein Ticket')
assert.equal(tickets()[0].$id, feld(team, 'ticketId'))
assert.equal(sink.sent.length, 0)
for (const id of [kauf, aktiv, aus]) assert.ok(!feld(id, 'ticketId'), id)
assert.ok(!feld(team, 'einschaltMailAt'))
})
test('Selbstheilung nur für Zeiten: ein Terminplaner in setup ohne Ticket bleibt unberührt', async () => {
aw.seed('websiteProjects', { $id: 'pt', customerId: 'c1', projectName: 'Termine' })
aw.seed('portalFeatures', { $id: 'f_pt_termine', customerId: 'c1', projectId: 'pt', featureKey: 'termine', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop' })
await sf.runActivation({ now: JETZT })
assert.equal(tickets().length, 0)
assert.equal(sink.sent.length, 0)
})
test('Selbstheilung: ein Dokument scheitert (Ticketsystem down) ⇒ die übrigen werden geheilt, der Job wirft nicht, das Log nennt keine Kundendaten', async () => {
const kaputt = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
const heil = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, customerId: 'c2' })
aw.failNext({ collection: 'workorders', method: 'POST', status: 500, message: 'Ticketsystem down' })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done, [])
assert.ok(!feld(kaputt, 'ticketId'))
assert.ok(feld(heil, 'ticketId'), 'das zweite Dokument wurde trotzdem geheilt')
assert.equal(k27().length, 1)
assert.deepEqual(k27()[0].to.map((a) => a.address), ['k2@example.invalid'])
const log = logText()
assert.match(log, /Ticketsystem down/)
assert.doesNotMatch(log, /example\.invalid|Kunde Eins|Kunde Zwei/)
// der nächste Lauf holt das gescheiterte nach
await sf.runActivation({ now: JETZT })
assert.ok(feld(kaputt, 'ticketId'))
assert.equal(k27().length, 2)
})
test('Selbstheilung: Listenfehler beim Suchen der Dokumente ⇒ kein Wurf, K16 der schon freigeschalteten bleibt', async () => {
const id = await einrichtung({ zeiten: WOCHE })
// GET auf portalFeatures: 1 Liste der Freischaltung, 2 frisches Lesen unter der Sperre, 3 Liste der Selbstheilung
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, after: 2 })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(k16().length, 1)
assert.match(logText(), /Selbstheilung Öffnungszeiten: Dokumente nicht lesbar/, 'der Fehler traf die Liste der Selbstheilung')
})
test('Projektliste eines Kunden (runActivation mit customerId) schaltet nur frei: keine Selbstheilung, kein Neubau, kein Ticket, keine Mail', async () => {
const unvollstaendig = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
const fertig = await einrichtung({ zeiten: WOCHE })
const done = await sf.runActivation({ now: JETZT, customerId: 'c1' })
assert.deepEqual(done.map((d) => d.$id), [fertig], 'Freischalten gilt weiter für den Kunden')
assert.ok(!feld(unvollstaendig, 'ticketId'))
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [1, 0, 0], 'nur das geschlossene Ticket der fertigen Funktion')
assert.equal(k16().length, 1)
// der Lauf des Jobs holt es dann nach
await sf.runActivation({ now: JETZT })
assert.ok(feld(unvollstaendig, 'ticketId'))
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [2, 1, 1])
})
test('Heilen und Freischalten im selben Lauf: aktiv gewordene Dokumente bekommen keine K27 mehr nachgeschoben', async () => {
const id = await einrichtung({ zeiten: WOCHE, einschaltMailAt: null })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(k27().length, 0)
assert.equal(k16().length, 1)
})