Zahlungseingang: markPaid meldet den Fall und merkt ihn im Dokument

markPaid liefert jetzt { doc, created, fall }. Trifft eine Zahlung ein Dokument, das nicht
mehr auf die Bezahlung wartet (active, disabled, setup mit Ticket), wird sie einmalig
verbucht (purchaseStatus paid, unlockedByPurchase, Rechnung, Notiz) und der noch zu
meldende Fall im eigenen Feld zahlungMeldung vermerkt (bereits_aktiv | abgeschaltet |
abo_ende), in einem einzigen Schreibzugriff. Ein Folgeereignis bucht nichts doppelt und
liefert den Fall erneut, bis zahlungMeldung auf gemeldet steht. ordered und fortsetzbares
setup laufen wie bisher, off bleibt unveraendert (Uebernahme ueber adoptPaidSession).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 13:13:42 +00:00
1 parent 992d7358fa
commit 468df49812
2 files changed
+202 -4

No files matched your search

+148
View File
@@ -437,3 +437,151 @@ test('disableCustomerFeatures reicht den Grund an alle Dokumente durch, ein scho
await sf.disableCustomerFeatures('c_a11b', 'termine', {})
assert.equal(lesen('f_a11_k2').disabledReason, 'team')
})
// ------------------------------- markPaid meldet den Fall und merkt ihn im Dokument (B10a) ---
const HINWEIS = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z System: Zahlung eingegangen \(Sitzung cs_[\w]+\), nichts eingerichtet$/
const bezahlDoc = (id, over = {}) =>
termineDoc(id, { projectId: 'p1', stripeSessionId: `cs_${id}`, purchaseStatus: 'ordered', unlockedByPurchase: false, ...over })
const bezahle = (id) => sf.markPaid({ sessionId: `cs_${id}`, invoiceId: `in_${id}`, invoiceUrl: `https://x/inv/${id}` })
const FAELLE = [
['active', { state: 'active', enabled: true }, 'bereits_aktiv'],
['setup_mit_ticket', { state: 'setup', enabled: false, ticketId: 't_vorhanden' }, 'bereits_aktiv'],
['disabled_team', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_abo_ende', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_geloescht', { state: 'disabled', enabled: false, disabledReason: 'kunde_geloescht', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_altbestand', { state: 'disabled', enabled: false, disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_vor_einrichtung', { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abo_ende'],
]
for (const [name, zustand, fall] of FAELLE) {
test(`markPaid ${name}: Zahlung wird einmalig verbucht, der Fall ${fall} im Dokument vermerkt, nichts eingerichtet`, async () => {
const id = `f_b10a_${name}`
aw.seed('portalFeatures', bezahlDoc(id, zustand))
const ticketsVorher = aw.docs('workorders').size
const davor = lesen(id)
const { doc, created, fall: gemeldet } = await bezahle(id)
assert.equal(created, false)
assert.equal(gemeldet, fall)
assert.equal(doc.$id, id)
const nachher = lesen(id)
assert.equal(nachher.purchaseStatus, 'paid')
assert.equal(nachher.unlockedByPurchase, true)
assert.equal(nachher.stripeInvoiceId, `in_${id}`)
assert.equal(nachher.invoiceUrl, `https://x/inv/${id}`)
assert.equal(nachher.zahlungMeldung, fall)
assert.match(nachher.adminNote, HINWEIS)
assert.ok(nachher.adminNote.includes(`(Sitzung cs_${id})`))
assert.ok(nachher.adminNote.length <= 300)
assert.deepEqual([nachher.state, nachher.enabled, nachher.ticketId, nachher.disabledReason, nachher.disabledAt], [davor.state, davor.enabled, davor.ticketId, davor.disabledReason, davor.disabledAt], 'Zustand, Ticket und Abschaltgrund bleiben')
assert.equal(aw.docs('workorders').size, ticketsVorher, 'kein Ticket')
assert.deepEqual(doc, nachher, 'liefert das gespeicherte Dokument')
})
}
test('markPaid: ein Folgeereignis bucht nichts doppelt, liefert den Fall aber erneut, bis die Meldung als gemeldet vermerkt ist', async () => {
const id = 'f_b10a_wiederholung'
aw.seed('portalFeatures', bezahlDoc(id, { adminNote: 'Notiz vom Team' }))
const erst = await bezahle(id)
assert.equal(erst.fall, 'bereits_aktiv')
const gebucht = lesen(id)
assert.equal(gebucht.adminNote.endsWith(' | Notiz vom Team'), true, 'aeltere Notiz bleibt dahinter erhalten')
assert.match(gebucht.adminNote.split(' | ')[0], HINWEIS)
// Stripe wiederholt, SEPA schickt completed und async_payment_succeeded: nichts wird geschrieben, der Fall bleibt offen
for (let i = 0; i < 2; i += 1) {
const { ergebnis, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0, 'kein Schreibzugriff beim Folgeereignis')
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, 'bereits_aktiv'])
assert.deepEqual(lesen(id), gebucht, 'adminNote, purchaseStatus und Rechnung unveraendert')
}
// die Meldung ist raus (B10b): der Marker steht auf gemeldet, danach ist der Fall nicht mehr offen
aw.seed('portalFeatures', { ...lesen(id), zahlungMeldung: 'gemeldet' })
const { ergebnis: danach, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0)
assert.deepEqual([danach.created, danach.fall], [false, null])
assert.equal(lesen(id).purchaseStatus, 'paid', 'purchaseStatus wird nie mit Meldezustaenden ueberladen')
assert.equal(lesen(id).zahlungMeldung, 'gemeldet')
// ein unbekannter Wert im Marker gilt nicht als offener Fall
aw.seed('portalFeatures', { ...lesen(id), zahlungMeldung: 'unsinn' })
assert.equal((await bezahle(id)).fall, null)
})
test('markPaid: die Notiz haelt 300 Zeichen mit dem neuesten Eintrag vorn', async () => {
const id = 'f_b10a_notiz'
aw.seed('portalFeatures', bezahlDoc(id, { adminNote: 'x'.repeat(300) }))
await bezahle(id)
const note = lesen(id).adminNote
assert.equal(note.length, 300)
assert.match(note, /^\d{4}-\d{2}-\d{2}T[\d:.]+Z System: Zahlung eingegangen \(Sitzung cs_f_b10a_notiz\), nichts eingerichtet \| x+$/)
})
test('markPaid: eine normal bezahlte Bestellung (ordered, setup, active) hat zahlungMeldung leer, ihr Folgeereignis liefert keinen Fall', async () => {
aw.seed('websiteProjects', { $id: 'p_b10a_normal', customerId: 'c1', projectName: 'Normal bezahlt' })
await sf.markOrdered({ customerId: 'c1', projectId: 'p_b10a_normal', key: 'termine', sessionId: 'cs_b10a_normal', consent: {} })
const erst = await sf.markPaid({ sessionId: 'cs_b10a_normal', invoiceId: 'in_n', invoiceUrl: 'https://x/n' })
assert.deepEqual([erst.created, erst.fall], [true, null])
assert.equal(erst.doc.state, 'setup')
assert.equal(erst.doc.purchaseStatus, 'paid')
assert.equal(erst.doc.zahlungMeldung, '')
const notiz = erst.doc.adminNote
// Ticket fertig, Funktion aktiv: ein Folgeereignis ist kein Zahlungsfall
const aktiv = await sf.activate(erst.doc.$id, {})
assert.equal(aktiv.state, 'active')
const { ergebnis, schreibend } = await schreibzugriffe(() => sf.markPaid({ sessionId: 'cs_b10a_normal', invoiceId: 'in_n', invoiceUrl: 'https://x/n' }))
assert.equal(schreibend, 0)
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, null])
assert.equal(lesen(erst.doc.$id).zahlungMeldung, '')
assert.equal(lesen(erst.doc.$id).adminNote, notiz)
assert.equal(lesen(erst.doc.$id).purchaseStatus, 'paid')
// auch nach dem Abschalten bleibt es ohne Fall
await sf.disable(erst.doc.$id, { reason: 'abo_ende' })
const spaeter = await sf.markPaid({ sessionId: 'cs_b10a_normal' })
assert.deepEqual([spaeter.created, spaeter.fall], [false, null])
})
test('markPaid: ordered und fortsetzbares setup (ohne Ticket) laufen wie bisher mit created true und ohne Fall', async () => {
const ordered = 'f_b10a_ordered'
aw.seed('portalFeatures', bezahlDoc(ordered, { state: 'ordered', enabled: false }))
const a = await bezahle(ordered)
assert.deepEqual([a.created, a.fall], [true, null])
assert.equal(a.doc.state, 'setup')
assert.ok(a.doc.ticketId)
const fortsetzbar = 'f_b10a_fortsetzbar'
aw.seed('portalFeatures', bezahlDoc(fortsetzbar, { state: 'setup', enabled: false, purchaseStatus: 'paid', unlockedByPurchase: true }))
const b = await bezahle(fortsetzbar)
assert.deepEqual([b.created, b.fall], [true, null])
assert.ok(b.doc.ticketId)
assert.ok(!lesen(fortsetzbar).adminNote, 'nichts verbucht, die Zahlung stand schon')
})
test('markPaid: ein Dokument im Zustand off bleibt unveraendert, ohne Fall (die Uebernahme ist Sache von adoptPaidSession)', async () => {
const id = 'f_b10a_off'
aw.seed('portalFeatures', bezahlDoc(id, { state: 'off', enabled: false, purchaseStatus: 'failed' }))
const davor = lesen(id)
const { ergebnis, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0)
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, null])
assert.equal(ergebnis.doc.state, 'off')
assert.deepEqual(lesen(id), davor)
})
test('markPaid: ohne Sitzung oder ohne Dokument gibt es kein Dokument, kein created und keinen Fall', async () => {
assert.deepEqual(await sf.markPaid({}), { doc: null, created: false, fall: null })
assert.deepEqual(await sf.markPaid({ sessionId: 'cs_gibts_nicht' }), { doc: null, created: false, fall: null })
})
test('markPaid: gleichzeitige Aufrufe buchen die Zahlung einmal und liefern alle denselben Fall', async () => {
const id = 'f_b10a_parallel'
aw.seed('portalFeatures', bezahlDoc(id, { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-01T10:00:00.000Z' }))
const { ergebnis, schreibend } = await schreibzugriffe(() => Promise.all([1, 2, 3, 4].map(() => bezahle(id))))
assert.equal(schreibend, 1, 'genau ein Schreibzugriff')
assert.deepEqual(ergebnis.map((r) => [r.created, r.fall]), Array(4).fill([false, 'abo_ende']))
assert.equal(lesen(id).adminNote.split('System: Zahlung eingegangen').length - 1, 1, 'ein Eintrag in der Notiz')
assert.equal(lesen(id).zahlungMeldung, 'abo_ende')
})