Terminplaner TP2 E8: Nachbesserung 1 (offener Zahlungsfall bleibt sichtbar, R108)

setzeFunktionZurueck setzt ein Funktionsdokument mit offenem Zahlungsfall
(zahlungMeldung nicht leer und nicht 'gemeldet') nicht auf off zurueck: Warnung
ohne Kennungen, +1 in lauf.fehler, kein Wurf. Das Dokument bleibt disabled mit
seinen Markern, der Abo-Abgleich holt T19 weiter nach, nach 'gemeldet' setzt der
naechste Lauf zurueck. Buchungen und Einstellung werden wie bisher geloescht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 18:08:54 +00:00
1 parent 30701ba277
commit 6bd6aed7d5
2 files changed
+114 -21

No files matched your search

+83 -9
View File
@@ -27,7 +27,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { Query, deleteDocument, listAllDocuments } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { normalisiereEinstellung } = await import('../server/services/termine/einstellungen.js')
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
@@ -1482,14 +1482,11 @@ test('loescheAbgeschalteteFunktionen: disabled länger als 30 Tage ⇒ Buchungen
assert.equal(doc.siteKey, 'abcdefghijklmnop', 'siteKey unverändert')
for (const id of andere) assert.deepEqual(aw.docs('portalFeatures').get(id), vorher[id], `${id}: Dokument unverändert`)
// zweiter Lauf: nichts mehr zu löschen, die Funktion zählt nicht noch einmal, das zurückgesetzte Dokument wird nicht mehr gelistet und nicht mehr gelesen
const leseF1 = new RegExp(`/collections/portalFeatures/documents/${F1}(\\?|$)`)
let zweiter
const zugriffeAufF1 = await zaehleAnfragen(async () => {
zweiter = await loescheAbgeschalteteFunktionen(JETZT)
}, leseF1)
assert.deepEqual(zweiter, { funktionen: 0, buchungen: 0, gedeckelt: 0 })
assert.equal(zugriffeAufF1, 0, 'keine tägliche Neuprüfung des zurückgesetzten Dokuments')
// zweiter Lauf: nichts mehr zu löschen, die Funktion zählt nicht noch einmal; die Liste der abgeschalteten Terminplaner (dieselbe Abfrage wie im Job) führt F1 nicht mehr
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 0, buchungen: 0, gedeckelt: 0 })
const abgeschalteteTermine = await listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'disabled')])
assert.ok(abgeschalteteTermine.length > 0, 'die Abfrage liefert die übrigen abgeschalteten Terminplaner (F2, f_p_projekt05_termine)')
assert.ok(!abgeschalteteTermine.some((d) => d.$id === F1), 'F1 steht nicht mehr in der Liste der abgeschalteten Terminplaner')
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
assert.equal(fehlerLog().length, 0)
})
@@ -1857,6 +1854,83 @@ for (const [name, grund] of [
})
}
/** Offener Zahlungsfall am abgeschalteten Dokument (siteFeatures.bucheSpaeteZahlung): bezahlt, Fall vermerkt, noch nicht ans Team gemeldet. */
const offenerFall = (over = {}) => eingerichtet({ disabledReason: 'abo_ende', zahlungMeldung: 'abo_ende', ...over })
const OFFENER_FALL_MARKER = ['unlockedByPurchase', 'purchaseStatus', 'ticketId', 'zahlungMeldung', 'disabledAt', 'disabledReason', 'aboEndeMailAt', 'activatedAt', 'adminNote']
test('Löschung nach 30 Tagen, R108: offener Zahlungsfall (zahlungMeldung abo_ende) ⇒ Buchungen und Einstellung weg, das Dokument bleibt disabled mit allen Markern; Warnung ohne Kennungen', async () => {
await abgeschaltetMitBuchungen(3, { funktion: offenerFall() })
const vorher = structuredClone(aw.docs('portalFeatures').get(F1))
const erg = await loescheAbgeschalteteFunktionen(JETZT)
assert.deepEqual(erg, { funktionen: 1, buchungen: 3, gedeckelt: 0 }, 'gelöscht wird wie bisher')
assert.equal(buchungen().length, 0)
assert.equal(aw.docs('termineClaims').size, 0)
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung gelöscht')
const doc = aw.docs('portalFeatures').get(F1)
assert.equal(doc.state, 'disabled', 'nicht zurückgesetzt')
assert.equal(doc.unlockedByPurchase, true)
assert.equal(doc.zahlungMeldung, 'abo_ende')
assert.equal(doc.ticketId, 't_einrichtung')
for (const feld of OFFENER_FALL_MARKER) assert.deepEqual(doc[feld], vorher[feld], `${feld} unberührt`)
assert.deepEqual({ ...doc, $updatedAt: undefined }, { ...vorher, $updatedAt: undefined }, 'das Dokument ist unverändert')
const warnungen = warnLog().filter((zeile) => /offenen Zahlungsfall/.test(zeile))
assert.equal(warnungen.length, 1, 'Warnung im Log')
assert.doesNotMatch(warnLog().join('\n'), /\bb_|f_p_|p_projekt|c_kunde|Müller|@/, 'keine Kennung, keine Personendaten im Log')
assert.equal(fehlerLog().length, 0, 'kein Wurf, kein Fehlereintrag')
// der nächste Tag: nichts mehr zu löschen, das Dokument bleibt sichtbar (weiter in der Liste), wieder nur die Warnung
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 0, buchungen: 0, gedeckelt: 0 })
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled')
assert.equal(warnLog().filter((zeile) => /offenen Zahlungsfall/.test(zeile)).length, 2)
const abgeschalteteTermine = await listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'disabled')])
assert.ok(abgeschalteteTermine.some((d) => d.$id === F1), 'F1 steht weiter in der Liste der abgeschalteten Terminplaner')
// R108: ist der Fall gemeldet, setzt der nächste Lauf wie bisher auf off zurück (ein reines Zurücksetzen zählt nicht)
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(F1), zahlungMeldung: 'gemeldet' })
log.warn.mock.resetCalls()
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 0, buchungen: 0, gedeckelt: 0 })
const zurueck = aw.docs('portalFeatures').get(F1)
assert.equal(zurueck.state, 'off')
assert.equal(zurueck.enabled, false)
assert.equal(zurueck.unlockedByPurchase, false)
assert.equal(zurueck.purchaseStatus, 'zurueckgesetzt')
assert.equal(zurueck.zahlungMeldung, '')
assert.equal(zurueck.ticketId, '')
assert.equal(zurueck.disabledAt, null)
assert.equal(zurueck.disabledReason, null)
assert.equal(zurueck.siteKey, 'abcdefghijklmnop')
assert.equal(warnLog().length, 0)
assert.equal(fehlerLog().length, 0)
const danach = await listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'disabled')])
assert.ok(!danach.some((d) => d.$id === F1), 'danach nicht mehr gelistet')
})
test('Löschung nach 30 Tagen, R108: der übersprungene Reset zählt im Tagesjob als Fehler, der nächste Lauf nach der Meldung nicht mehr', async () => {
await abgeschaltetMitBuchungen(2, { zellen: false, funktion: offenerFall() })
let erg = await taeglicherJob(JETZT)
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 2 })
assert.equal(erg.fehler, 1, 'der offene Fall zählt im Lauf')
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled')
assert.equal(aw.docs('portalFeatures').get(F1).zahlungMeldung, 'abo_ende')
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(F1), zahlungMeldung: 'gemeldet' })
erg = await taeglicherJob(plus(JETZT, TAG))
assert.equal(erg.fehler, 0)
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
})
for (const meldung of ['', 'gemeldet']) {
test(`Löschung nach 30 Tagen, R108: zahlungMeldung "${meldung}" ist kein offener Fall ⇒ zurückgesetzt wie bisher`, async () => {
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende', zahlungMeldung: meldung }) })
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 1, gedeckelt: 0 })
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
assert.equal(warnLog().length, 0)
assert.equal(fehlerLog().length, 0)
})
}
test('Löschung nach 30 Tagen: das zurückgesetzte Dokument behält siteKey sowie Bestell- und Rechnungsfelder, alles Übrige ist leer', async () => {
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
const vorher = structuredClone(aw.docs('portalFeatures').get(F1))