TP2 Abschlussprüfung: Löschung (IP/Browser auf portalFeatures, Erstattungsspur, K22-Datum)
- markOrdered speichert IP und Browser der Bestellung nicht mehr am Funktionsdokument (nur im Nachweis featureConsents, dort mit Löschfrist); Altwerte werden bei einer neuen Bestellung geleert - Aufbewahrungslauf (retention): consentIp, consentUserAgent, consentText, consentAt werden an allen Funktionsdokumenten eines fälligen Kunden geleert, auch an abgeschalteten; Fehlschlag hält den Kunden wie die übrigen abhängigen Daten - Einmal-Skript scripts/feature-consent-felder-leeren.mjs (Standard Probelauf, --apply leert nur, wo featureConsents die Angabe trägt) - 30-Tage-Reset (jobs.js): auch eine bezahlte Einrichtung mit Grund abo_ende_vor_einrichtung und noch nicht gemeldetem T19 wird nicht zurückgesetzt, solange zahlungMeldung nicht gemeldet ist (die Spur der Erstattung bleibt) - K22: enddatum ist das Abo-Ende (lage.endedAt), sonst der Tag des Abschaltens; loeschdatum bleibt 30 Tage nach dem Abschalten Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b974b3a3ec
commit
2fc48df915
13 files changed
+425
-22
No files matched your search
@@ -0,0 +1,100 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Einmal-Skript scripts/feature-consent-felder-leeren.mjs (C1): Probelauf als Standard, --apply leert IP und Browser nur dort, wo
|
||||
// featureConsents sie traegt. Nur Nachbauten, nie Produktion.
|
||||
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), featureConsents: schemaKeys('featureConsents') } })
|
||||
after(() => aw.close())
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
const { run, parseArgs } = await import('../scripts/feature-consent-felder-leeren.mjs')
|
||||
|
||||
const feature = (id, over = {}) =>
|
||||
aw.seed('portalFeatures', {
|
||||
$id: id, customerId: 'c1', projectId: `p_${id}`, featureKey: 'termine', state: 'active', enabled: true, siteKey: `k_${id}`,
|
||||
stripeSessionId: `cs_${id}`, consentText: 'Text', consentAt: '2026-10-01T10:00:00.000Z', consentIp: '203.0.113.7', consentUserAgent: 'UA', ...over,
|
||||
})
|
||||
const nachweis = (id, over = {}) =>
|
||||
aw.seed('featureConsents', {
|
||||
$id: `n_${id}`, customerId: 'c1', projectId: `p_${id}`, featureKey: 'termine', stripeSessionId: `cs_${id}`, acceptedAt: '2026-10-01T10:00:00.000Z',
|
||||
fassungen: '{}', texte: '[]', textHash: 'x', ip: '203.0.113.7', userAgent: 'UA', konto: 'k@example.invalid', ...over,
|
||||
})
|
||||
const angaben = (id) => {
|
||||
const d = aw.docs('portalFeatures').get(id)
|
||||
return [d.consentIp, d.consentUserAgent, d.consentText, d.consentAt, d.siteKey]
|
||||
}
|
||||
const stumm = () => {
|
||||
const zeilen = []
|
||||
return { log: (z) => zeilen.push(z), zeilen }
|
||||
}
|
||||
|
||||
feature('f_ok1'); nachweis('f_ok1') // IP und Browser im Nachweis
|
||||
feature('f_ok2', { consentUserAgent: '' }); nachweis('f_ok2') // nur IP am Dokument, im Nachweis vorhanden
|
||||
feature('f_alt'); // Altbestand: kein Nachweis
|
||||
feature('f_ohne_ip_im_nachweis'); nachweis('f_ohne_ip_im_nachweis', { ip: '' }) // Nachweis ohne IP: traegt sie nicht
|
||||
feature('f_keine_sitzung', { stripeSessionId: '' }) // ohne Sitzung kein Nachweis
|
||||
feature('f_leer', { consentIp: '', consentUserAgent: '' }); nachweis('f_leer') // nichts zu leeren
|
||||
aw.seed('portalFeatures', { $id: 'f_n', customerId: 'c2', projectId: 'p_n', featureKey: 'news', state: 'off', enabled: false }) // Feld nie gesetzt
|
||||
|
||||
test('parseArgs: ohne Argument und mit --dry-run ist es der Probelauf, nur --apply schreibt, Unbekanntes und beides zusammen ist ein Fehler', () => {
|
||||
assert.deepEqual(parseArgs([]), { apply: false })
|
||||
assert.deepEqual(parseArgs(['--dry-run']), { apply: false })
|
||||
assert.deepEqual(parseArgs(['--apply']), { apply: true })
|
||||
assert.throws(() => parseArgs(['--apply', '--dry-run']), /schliessen sich aus/)
|
||||
assert.throws(() => parseArgs(['--force']), /Unbekanntes Argument: --force/)
|
||||
})
|
||||
|
||||
test('Probelauf (Standard) zaehlt und listet, schreibt aber nichts', async () => {
|
||||
const vorher = JSON.stringify([...aw.docs('portalFeatures')])
|
||||
const out = stumm()
|
||||
const erg = await run({ log: out.log })
|
||||
assert.equal(erg.applied, false)
|
||||
assert.equal(erg.total, 7)
|
||||
assert.equal(erg.withData, 5, 'f_ok1, f_ok2, f_alt, f_ohne_ip_im_nachweis, f_keine_sitzung')
|
||||
assert.deepEqual(erg.clearable.sort(), ['f_ok1', 'f_ok2'])
|
||||
assert.deepEqual(erg.kept.sort(), ['f_alt', 'f_keine_sitzung', 'f_ohne_ip_im_nachweis'])
|
||||
assert.equal(erg.cleared, 0)
|
||||
assert.equal(JSON.stringify([...aw.docs('portalFeatures')]), vorher, 'nichts geschrieben')
|
||||
assert.match(out.zeilen.join('\n'), /Probelauf \(--dry-run\): es wird nichts geschrieben/)
|
||||
assert.match(out.zeilen.join('\n'), /zu leeren .*: 2/)
|
||||
})
|
||||
|
||||
test('--apply leert consentIp und consentUserAgent nur dort, wo der Nachweis sie traegt; Text, Zeit und alles andere bleiben', async () => {
|
||||
const out = stumm()
|
||||
const erg = await run({ apply: true, log: out.log })
|
||||
assert.equal(erg.applied, true)
|
||||
assert.equal(erg.cleared, 2)
|
||||
assert.deepEqual(erg.failed, [])
|
||||
assert.deepEqual(angaben('f_ok1').filter((_, i) => i !== 3), ['', '', 'Text', 'k_f_ok1'])
|
||||
assert.ok(aw.docs('portalFeatures').get('f_ok1').consentAt, 'consentAt bleibt')
|
||||
assert.deepEqual(angaben('f_ok2').slice(0, 2), ['', ''])
|
||||
for (const id of ['f_alt', 'f_ohne_ip_im_nachweis', 'f_keine_sitzung']) assert.equal(aw.docs('portalFeatures').get(id).consentIp, '203.0.113.7', `${id} bleibt`)
|
||||
// ein zweiter Lauf findet nichts mehr zu leeren
|
||||
const nochmal = await run({ apply: true, log: () => {} })
|
||||
assert.deepEqual(nochmal.clearable, [])
|
||||
assert.equal(nochmal.cleared, 0)
|
||||
})
|
||||
|
||||
test('--apply: ein Schreibfehler steht im Ergebnis, die uebrigen Dokumente werden trotzdem geleert; ein nicht lesbarer Nachweis bleibt unberuehrt', async () => {
|
||||
feature('f_x1'); nachweis('f_x1')
|
||||
feature('f_x2'); nachweis('f_x2')
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500, message: 'kaputt' })
|
||||
const erg = await run({ apply: true, log: () => {} })
|
||||
assert.equal(erg.failed.length, 1)
|
||||
assert.match(erg.failed[0].reason, /kaputt/)
|
||||
assert.equal(erg.cleared, 1)
|
||||
const offen = ['f_x1', 'f_x2'].filter((id) => aw.docs('portalFeatures').get(id).consentIp)
|
||||
assert.deepEqual(offen, [erg.failed[0].id])
|
||||
|
||||
assert.equal((await run({ apply: true, log: () => {} })).cleared, 1, 'der naechste Lauf holt das fehlgeschlagene Dokument nach')
|
||||
|
||||
feature('f_a_y'); nachweis('f_a_y')
|
||||
aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500, message: 'Nachweis kaputt' })
|
||||
const out = stumm()
|
||||
const lesefehler = await run({ apply: true, log: out.log })
|
||||
assert.deepEqual(lesefehler.unreadable, ['f_a_y'])
|
||||
assert.equal(lesefehler.cleared, 0, 'nicht lesbar heisst nicht leeren')
|
||||
assert.equal(aw.docs('portalFeatures').get('f_a_y').consentIp, '203.0.113.7')
|
||||
})
|
||||
@@ -656,8 +656,9 @@ test('Nachweis steht vor der Weiterleitung: Serverfassungen und -texte in Reihen
|
||||
assert.ok(vor <= n.acceptedAt && n.acceptedAt <= nach, `${vor} <= ${n.acceptedAt} <= ${nach}`)
|
||||
assert.match(n.acceptedAt, /Z$/, 'UTC-Zeit mit Z')
|
||||
assert.equal(doc.consentText, hakenTexte(T).slice(0, 1000), 'consentText aus den Haken-Texten')
|
||||
assert.equal(doc.consentIp, n.ip)
|
||||
assert.equal(doc.consentUserAgent, n.userAgent)
|
||||
// C1: IP und Browser stehen nur im Nachweis (featureConsents), nicht mehr am Funktionsdokument
|
||||
assert.equal(doc.consentIp, '')
|
||||
assert.equal(doc.consentUserAgent, '')
|
||||
})
|
||||
|
||||
test('Belehrung im Nachweis: Eintrag belehrung (haken false) direkt vor dem Knopf, News hat keinen', async () => {
|
||||
|
||||
@@ -188,7 +188,8 @@ test('checkout_legt_session_an', async () => {
|
||||
assert.equal(doc.stripeSessionId, session.id)
|
||||
assert.equal(doc.consentText, config.funktionen.consentText)
|
||||
assert.ok(doc.consentAt)
|
||||
assert.ok(doc.consentUserAgent)
|
||||
assert.equal(doc.consentUserAgent, '', 'C1: der Browser steht nur im Nachweis, nicht am Funktionsdokument')
|
||||
assert.equal(doc.consentIp, '')
|
||||
// zweite Bestellung waehrend die erste offen ist: dieselbe Zahlungsseite, keine neue Sitzung
|
||||
const again = await checkout('p1')
|
||||
assert.equal(again.status, 200)
|
||||
|
||||
@@ -400,6 +400,72 @@ test('retention_loescht_featureConsents: Ein Fehler beim Löschen eines Nachweis
|
||||
assert.deepEqual(alt.failed, [])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------- C1: IP, Browser, Text und Zeit der Bestellung an portalFeatures
|
||||
|
||||
const KLICK = { consentIp: '203.0.113.7', consentUserAgent: 'Mozilla/5.0 Test', consentText: 'Ich stimme zu.', consentAt: '2022-02-01T10:00:00.000Z' }
|
||||
const LEER = { consentIp: '', consentUserAgent: '', consentText: '', consentAt: null }
|
||||
const feld = (id) => {
|
||||
const d = aw.docs('portalFeatures').get(id)
|
||||
return { consentIp: d.consentIp, consentUserAgent: d.consentUserAgent, consentText: d.consentText, consentAt: d.consentAt }
|
||||
}
|
||||
|
||||
test('C1 retention: IP, Browser, Text und Zeit werden auf ALLEN Funktionsdokumenten eines fälligen Kunden geleert, auch auf abgeschalteten; andere Kunden und Felder bleiben', async () => {
|
||||
sub('cf_sub', 'cf', { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
stripeSub('cf_sub', 'cf')
|
||||
aw.seed('portalFeatures', { $id: 'cf_aktiv', customerId: 'cf', projectId: 'cfp1', featureKey: 'termine', state: 'active', enabled: true, siteKey: 'k1', ...KLICK })
|
||||
aw.seed('portalFeatures', { $id: 'cf_aus', customerId: 'cf', projectId: 'cfp2', featureKey: 'news', state: 'disabled', enabled: false, siteKey: 'k2', disabledAt: '2022-03-15T10:00:00.000Z', ...KLICK })
|
||||
aw.seed('portalFeatures', { $id: 'cf_leer', customerId: 'cf', projectId: 'cfp3', featureKey: 'news', state: 'disabled', enabled: false, siteKey: 'k3', consentIp: '', consentUserAgent: '', consentText: '', consentAt: null })
|
||||
sub('cf2_sub', 'cf2', { status: 'active' })
|
||||
aw.seed('portalFeatures', { $id: 'cf2_aktiv', customerId: 'cf2', projectId: 'cf2p', featureKey: 'termine', state: 'active', enabled: true, siteKey: 'k4', ...KLICK })
|
||||
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
const eintrag = plan.news.find((n) => n.customerId === 'cf')
|
||||
assert.ok(eintrag, 'ein fälliger Kunde mit gespeicherten Bestellangaben steht im Plan')
|
||||
assert.deepEqual(eintrag.featureIds, ['cf_aktiv'], 'abschalten: nur das noch nicht abgeschaltete')
|
||||
assert.deepEqual([...eintrag.featureFieldIds].sort(), ['cf_aktiv', 'cf_aus'], 'leeren: alle mit Angaben, auch das abgeschaltete; das leere Dokument nicht')
|
||||
assert.ok(!plan.news.some((n) => n.customerId === 'cf2'))
|
||||
assert.match(formatReport(plan), /Kunde cf: .*2 Funktionsdokument\(e\) mit Zustimmungsangaben zu leeren/)
|
||||
|
||||
// Probelauf ändert nichts
|
||||
await run({ mode: '', now: JAN_2026, log: () => {} })
|
||||
assert.deepEqual(feld('cf_aus'), { ...KLICK })
|
||||
|
||||
const result = await applyDeletions(planVon(plan, 'cf'))
|
||||
assert.deepEqual(result.failed, [])
|
||||
assert.equal(result.deleted.featureFields, 2)
|
||||
assert.deepEqual(feld('cf_aktiv'), LEER)
|
||||
assert.deepEqual(feld('cf_aus'), LEER)
|
||||
assert.equal(aw.docs('portalFeatures').get('cf_aktiv').state, 'disabled', 'abgeschaltet wie bisher')
|
||||
assert.equal(aw.docs('portalFeatures').get('cf_aus').siteKey, 'k2', 'übrige Felder unberührt')
|
||||
assert.deepEqual(feld('cf2_aktiv'), { ...KLICK }, 'laufender Kunde bleibt')
|
||||
assert.match(formatReport(plan, result), /Zustimmungsangaben an Funktionsdokumenten \(IP, Browser, Text, Zeit\): 2 geleert\./)
|
||||
// ein zweiter Lauf findet nichts mehr für diesen Kunden
|
||||
assert.ok(!(await findDueDeletions(JAN_2026)).news.some((n) => n.customerId === 'cf'))
|
||||
})
|
||||
|
||||
test('C1 retention: scheitert das Leeren, hält es den Kunden (Abo bleibt), der nächste Lauf plant es erneut; ältere Pläne ohne featureFieldIds laufen weiter', async () => {
|
||||
sub('cg_sub', 'cg', { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
stripeSub('cg_sub', 'cg')
|
||||
aw.seed('portalFeatures', { $id: 'cg_aus', customerId: 'cg', projectId: 'cgp', featureKey: 'news', state: 'disabled', enabled: false, siteKey: 'k5', ...KLICK })
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500, message: 'kaputt' })
|
||||
const result = await applyDeletions(planVon(plan, 'cg'))
|
||||
assert.deepEqual(result.failed.map((f) => [f.what, f.id]), [['portalFeatures', 'cg_aus']])
|
||||
assert.deepEqual(result.held, ['cg'])
|
||||
assert.ok(aw.docs('subscriptions').has('cg_sub'), 'das Abo-Dokument bleibt, sonst fände der nächste Lauf den Kunden nicht')
|
||||
assert.deepEqual(feld('cg_aus'), { ...KLICK })
|
||||
const nochmal = await findDueDeletions(JAN_2026)
|
||||
assert.deepEqual(nochmal.news.find((n) => n.customerId === 'cg').featureFieldIds, ['cg_aus'])
|
||||
const zweiter = await applyDeletions(planVon(nochmal, 'cg'))
|
||||
assert.deepEqual(zweiter.failed, [])
|
||||
assert.deepEqual(feld('cg_aus'), LEER)
|
||||
assert.ok(!aw.docs('subscriptions').has('cg_sub'))
|
||||
|
||||
const alt = await applyDeletions({ customers: [], takeover: [], news: [{ customerId: 'cgx', posts: [], featureIds: [], consentIds: [] }] })
|
||||
assert.equal(alt.deleted.featureFields, 0)
|
||||
assert.deepEqual(alt.failed, [])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Reihenfolge: abhängige Daten vor dem Abo (Fix Runde 1, R119)
|
||||
|
||||
/** Kunde mit beendetem Abo (2022), einem Vertragsereignis und einem Zustimmungsnachweis, ohne Beiträge und Funktionen. */
|
||||
|
||||
@@ -30,11 +30,26 @@ test('ohne_dokument_ist_off', async () => {
|
||||
assert.equal(list.find((p) => p.projectId === 'p1').liveUrl, 'https://baeckerei.de')
|
||||
})
|
||||
|
||||
test('C1: eine neue Bestellung leert IP und Browser aus einer frueheren Bestellung desselben Dokuments', async () => {
|
||||
const first = await sf.markOrdered({ customerId: 'c2', projectId: 'p3', key: 'news', sessionId: 'cs_c1_a', consent })
|
||||
// Altbestand: eine frueher gespeicherte IP am Dokument (vor dieser Aenderung wurde sie geschrieben)
|
||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
await updateDocument('portalFeatures', first.$id, { consentIp: '9.9.9.9', consentUserAgent: 'alt', state: 'off', enabled: false })
|
||||
const again = await sf.markOrdered({ customerId: 'c2', projectId: 'p3', key: 'news', sessionId: 'cs_c1_b', consent })
|
||||
assert.deepEqual([again.consentIp, again.consentUserAgent], ['', ''])
|
||||
assert.equal(aw.docs('portalFeatures').get(first.$id).consentIp, '')
|
||||
aw.docs('portalFeatures').delete(first.$id) // die uebrigen Faelle dieser Datei zaehlen Dokumente
|
||||
})
|
||||
|
||||
test('bezahlt_legt_ticket_an', async () => {
|
||||
const ordered = await sf.markOrdered({ customerId: 'c1', projectId: 'p1', key: 'news', sessionId: 'cs_1', consent })
|
||||
assert.equal(ordered.state, 'ordered')
|
||||
assert.equal(ordered.enabled, false)
|
||||
assert.equal(ordered.consentIp, '1.2.3.4')
|
||||
// C1: IP und Browser der Bestellung stehen nur im Nachweis (featureConsents), nie am Funktionsdokument, auch wenn der Aufrufer sie mitgibt
|
||||
assert.equal(ordered.consentIp, '')
|
||||
assert.equal(ordered.consentUserAgent, '')
|
||||
assert.equal(ordered.consentText, 'Ich stimme zu')
|
||||
assert.equal(ordered.consentAt, '2026-09-30T10:00:00.000Z')
|
||||
assert.match(ordered.siteKey, /^[a-z2-7]{16}$/)
|
||||
const { doc, created } = await sf.markPaid({ sessionId: 'cs_1', invoiceId: 'in_1', invoiceUrl: 'https://x/inv' })
|
||||
assert.equal(created, true)
|
||||
|
||||
@@ -602,6 +602,44 @@ test('meldeAboEnde: unbekannter Projektname faellt auf "Dein Projekt" zurueck, e
|
||||
assert.equal(mails().length, 1)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- C3: das Datum in K22 ist das Abo-Ende, nicht der Tag des Abschaltens
|
||||
|
||||
const AM_ABO_ENDE = new Date('2026-10-02T09:30:00.000Z') // drei Tage vor dem Lauf (JETZT = 05.10.2026)
|
||||
|
||||
test('C3: K22 nennt das Abo-Ende (endedAt des Abos), wenn es frueher war als das Abschalten; das Loeschdatum bleibt disabledAt + 30 Tage', async () => {
|
||||
legeFunktion(1)
|
||||
wartung('canceled', { endedAt: AM_ABO_ENDE.toISOString(), canceledAt: '2026-09-20T08:00:00.000Z' })
|
||||
|
||||
assert.deepEqual(await laufe(JETZT), ergebnis({ geprueft: 1, abgeschaltet: 1, mails: 1 }))
|
||||
assert.equal(ms(funktion().disabledAt), JETZT.getTime())
|
||||
const mail = mails()[0]
|
||||
assert.match(mail.text, /weil Dein Wartungs-Abo am 02\.10\.2026 beendet wurde/, 'Enddatum = Abo-Ende, nicht der 05.10.')
|
||||
assert.match(mail.text, /Beendet am: 02\.10\.2026/, 'auch die Zeile "Beendet am"')
|
||||
assert.match(mail.text, /Bis 04\.11\.2026 kannst Du Deine Termine/, 'Loeschdatum = Abschalten (05.10.) + 30 Tage')
|
||||
})
|
||||
|
||||
test('C3: ohne endedAt gilt canceledAt, ohne beides der Tag des Abschaltens (disabledAt)', async () => {
|
||||
legeFunktion(1)
|
||||
wartung('canceled', { endedAt: null, canceledAt: AM_ABO_ENDE.toISOString() })
|
||||
await laufe(JETZT)
|
||||
assert.match(mails()[0].text, /weil Dein Wartungs-Abo am 02\.10\.2026 beendet wurde/, 'canceledAt als Abo-Ende')
|
||||
|
||||
const id = legeFunktion(2, { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: JETZT.toISOString() }, { projekt: 'Zweites' })
|
||||
assert.equal(await meldeAboEnde(funktion(id), { lage: { laeuft: false, art: 'beendet', endedAt: null }, now: JETZT }), true)
|
||||
assert.match(mails().at(-1).text, /weil Dein Wartungs-Abo am 05\.10\.2026 beendet wurde/, 'ohne Abo-Ende: disabledAt')
|
||||
const id3 = legeFunktion(3, { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: JETZT.toISOString() }, { projekt: 'Drittes' })
|
||||
assert.equal(await meldeAboEnde(funktion(id3), { lage: { laeuft: false, art: 'beendet', endedAt: 'kein Datum' }, now: JETZT }), true)
|
||||
assert.match(mails().at(-1).text, /weil Dein Wartungs-Abo am 05\.10\.2026 beendet wurde/, 'unlesbares endedAt: disabledAt')
|
||||
})
|
||||
|
||||
test('C3: K22 vor_einrichtung nennt ebenfalls das Abo-Ende', async () => {
|
||||
legeAufbau(1)
|
||||
wartung('canceled', { endedAt: AM_ABO_ENDE.toISOString() })
|
||||
await laufe(JETZT)
|
||||
assert.equal(funktion().disabledReason, 'abo_ende_vor_einrichtung')
|
||||
assert.match(k22()[0].text, /Dein Wartungs-Abo wurde am 02\.10\.2026 beendet, noch vor der Einrichtung/)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Schritt 1b: Abo endet vor der Einrichtung (E5)
|
||||
|
||||
test('Bezahlt im Aufbau (setup, Ticket) ohne Abo: abgeschaltet, ein T19 an das Team, K22 vor_einrichtung, Ticket bleibt offen, am Folgetag nichts', async () => {
|
||||
|
||||
@@ -1921,6 +1921,54 @@ test('Löschung nach 30 Tagen, R108: der übersprungene Reset zählt im Tagesjob
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
})
|
||||
|
||||
// C2: bezahlte Einrichtung, abgeschaltet wegen des Abo-Endes vor der Einrichtung, T19 (Erstattung) noch nicht beim Team: der Reset nach
|
||||
// 30 Tagen wuerde zahlungMeldung, unlockedByPurchase und ticketId loeschen und damit die Spur der geschuldeten Erstattung.
|
||||
const ERSTATTUNG_OFFEN = ['', 'abo_ende', 'bereits_aktiv']
|
||||
for (const meldung of ERSTATTUNG_OFFEN) {
|
||||
test(`Löschung nach 30 Tagen, C2: bezahlt + abo_ende_vor_einrichtung + zahlungMeldung "${meldung}" (T19 offen) ⇒ Daten weg, Dokument bleibt disabled mit allen Markern; Warnung ohne Kennungen`, async () => {
|
||||
await abgeschaltetMitBuchungen(2, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende_vor_einrichtung', zahlungMeldung: meldung, aboEndeMailAt: null }) })
|
||||
const vorher = structuredClone(aw.docs('portalFeatures').get(F1))
|
||||
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 2, gedeckelt: 0 }, 'gelöscht wird wie bisher')
|
||||
assert.equal(buchungen().length, 0)
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung gelöscht')
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.deepEqual({ ...doc, $updatedAt: undefined }, { ...vorher, $updatedAt: undefined }, 'das Dokument ist unverändert (nicht zurückgesetzt)')
|
||||
assert.equal(warnLog().filter((zeile) => /offenen Zahlungsfall/.test(zeile)).length, 1)
|
||||
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')
|
||||
})
|
||||
}
|
||||
|
||||
test('Löschung nach 30 Tagen, C2: unbezahlt (unlockedByPurchase false) oder T19 gemeldet ⇒ zurückgesetzt wie bisher', async () => {
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende_vor_einrichtung', zahlungMeldung: 'gemeldet' }) })
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 1, gedeckelt: 0 })
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off', 'T19 gemeldet')
|
||||
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, praefix: 'b_un', funktion: eingerichtet({ disabledReason: 'abo_ende_vor_einrichtung', zahlungMeldung: '', unlockedByPurchase: false, purchaseStatus: 'ordered', aboEndeMailAt: null }) })
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 1, gedeckelt: 0 })
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off', 'unbezahlt: es gibt nichts zu erstatten')
|
||||
assert.equal(warnLog().length, 0)
|
||||
})
|
||||
|
||||
test('Tagesjob, C2: T19 scheitert 31 Tage lang ⇒ der Reset wird nicht ausgeführt (Fehler gezählt); sobald T19 beim Team ist, setzt ein Lauf zurück und T19 ging genau einmal', async () => {
|
||||
config.chatNotify.emails = ['team@example.com']
|
||||
await abgeschaltetMitBuchungen(2, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende_vor_einrichtung', zahlungMeldung: '', aboEndeMailAt: null }) })
|
||||
setMailTransportForTests({ sendMail: async () => { throw new Error('SMTP nicht erreichbar') } })
|
||||
|
||||
let erg = await taeglicherJob(JETZT)
|
||||
assert.ok(erg.fehler >= 1, `Fehler gezählt: ${erg.fehler}`)
|
||||
let doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.deepEqual([doc.state, doc.unlockedByPurchase, doc.zahlungMeldung, doc.ticketId, doc.disabledReason], ['disabled', true, '', 't_einrichtung', 'abo_ende_vor_einrichtung'], 'die Spur der Erstattung bleibt')
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'die Daten sind trotzdem gelöscht')
|
||||
|
||||
setMailTransportForTests(sink)
|
||||
for (const tage of [1, 2]) await taeglicherJob(plus(JETZT, tage * TAG))
|
||||
doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.deepEqual([doc.state, doc.purchaseStatus, doc.unlockedByPurchase, doc.zahlungMeldung], ['off', 'zurueckgesetzt', false, ''], 'nach der Meldung wird zurückgesetzt')
|
||||
assert.equal(sink.sent.filter((m) => /Erstattung fällig/.test(m.subject)).length, 1, 'T19 genau einmal')
|
||||
})
|
||||
|
||||
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 }) })
|
||||
|
||||
Reference in new issue
Block a user