Finanzen: Zuordnung zu Rechnungen robuster (Speicherfehler nach pay, Doppelzuordnung, Protokoll)
M1: scheitert Appwrite nach dem Bezahlen, kommt 502 mit klarer Meldung statt eines blanken 500. M2: Wiederaufnahme nur bei passendem bezahltem Betrag; eine Buchung, die schon an einer anderen bezahlten Rechnung haengt, wird nicht ein zweites Mal vermerkt und bezahlt (409). M3: Protokollzeile je erfolgreicher, nicht umkehrbarer Zuordnung (Buchung, Rechnung, Nutzer). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
daa451094a
commit
83ee9d5a25
3 files changed
+118
-11
No files matched your search
@@ -324,14 +324,21 @@ test('bezahlte Rechnung mit dem Vermerk einer anderen Buchung -> 409 nicht mehr
|
||||
assert.equal(stored('b2').category, '')
|
||||
})
|
||||
|
||||
test('Appwrite scheitert nach pay: 500, die Wiederholung schliesst ab ohne zweites pay (Rechnung ohne paid_out_of_band wie bei Stripe)', async () => {
|
||||
test('Appwrite scheitert nach pay: 502 mit klarer Meldung, die Wiederholung schliesst ab ohne zweites pay (Rechnung ohne paid_out_of_band wie bei Stripe) (M1)', async () => {
|
||||
seedTx('b1')
|
||||
st.invoices.set('in_ewv', rawInvoice())
|
||||
const error = mock.method(console, 'error', () => {})
|
||||
aw.failNext({ collection: TX(), method: 'PATCH', status: 500, message: 'Appwrite kaputt' })
|
||||
const failed = await zuordnen('b1', { invoiceId: 'in_ewv' })
|
||||
assert.equal(failed.status, 500)
|
||||
assert.equal(failed.body.error, 'Interner Fehler.')
|
||||
assert.equal(failed.status, 502)
|
||||
assert.equal(failed.body.error, 'In Stripe als bezahlt markiert, aber die Buchung wurde nicht gespeichert. Bitte erneut zuordnen.')
|
||||
assert.deepEqual(Object.keys(failed.body), ['error'])
|
||||
assert.ok(!JSON.stringify(failed.body).includes('Appwrite kaputt'))
|
||||
// die Ursache steht im Protokoll (Art und Status), nie ihre Meldung
|
||||
assert.equal(error.mock.callCount(), 1)
|
||||
assert.equal(error.mock.calls[0].arguments[0], '[finanzen] Buchung zuordnen fehlgeschlagen:')
|
||||
assert.ok(!JSON.stringify(error.mock.calls).includes('Appwrite kaputt'))
|
||||
mock.restoreAll()
|
||||
assert.equal(stored('b1').category, '')
|
||||
assert.equal(st.invoices.get('in_ewv').status, 'paid')
|
||||
// wie die echte API: an der bezahlten Rechnung steht nichts von paid_out_of_band
|
||||
@@ -346,6 +353,86 @@ test('Appwrite scheitert nach pay: 500, die Wiederholung schliesst ab ohne zweit
|
||||
assert.equal(callsTo('POST', '/v1/invoices/in_ewv').length, 1)
|
||||
})
|
||||
|
||||
test('Appwrite scheitert beim Zuordnen ohne Rechnung (Kategorie): weiter ein blanker 500 (M1)', async () => {
|
||||
seedTx('b1')
|
||||
mock.method(console, 'error', () => {})
|
||||
aw.failNext({ collection: TX(), method: 'PATCH', status: 500, message: 'Appwrite kaputt' })
|
||||
const failed = await zuordnen('b1', { category: 'privat' })
|
||||
assert.equal(failed.status, 500)
|
||||
assert.equal(failed.body.error, 'Interner Fehler.')
|
||||
})
|
||||
|
||||
test('bezahlt und gekennzeichnet, aber in anderer Hoehe bezahlt als die Buchung -> keine Wiederaufnahme, 409 nicht mehr offen (M2)', async () => {
|
||||
seedTx('b1', { amountCents: 10000 })
|
||||
st.invoices.set('in_ewv', rawInvoice({ status: 'paid', amount_remaining: 0, amount_paid: 16879, metadata: { bankTransactionId: 'b1' } }))
|
||||
const res = await zuordnen('b1', { invoiceId: 'in_ewv' })
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, 'Diese Rechnung ist nicht mehr offen.')
|
||||
assert.equal(stored('b1').category, '')
|
||||
assert.equal(payCalls().length, 0)
|
||||
})
|
||||
|
||||
test('Buchung gehoert schon zu einer anderen bezahlten Rechnung -> 409, kein Vermerk, kein pay (M2)', async () => {
|
||||
seedTx('b1')
|
||||
// in_alt wurde fuer b1 bezahlt, aber das Speichern in Appwrite scheiterte: b1 steht noch ohne Kategorie da
|
||||
st.invoices.set('in_alt', rawInvoice({ id: 'in_alt', number: 'WK-0004', status: 'paid', amount_remaining: 0, amount_paid: 16879, metadata: { bankTransactionId: 'b1' } }))
|
||||
st.invoices.set('in_ewv', rawInvoice())
|
||||
const res = await zuordnen('b1', { invoiceId: 'in_ewv' })
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, 'Diese Buchung ist bereits einer anderen Rechnung zugeordnet.')
|
||||
assert.equal(payCalls().length, 0)
|
||||
assert.equal(callsTo('POST', '/v1/invoices/in_ewv').length, 0, 'kein Vermerk')
|
||||
assert.equal(st.invoices.get('in_ewv').status, 'open')
|
||||
assert.equal(stored('b1').category, '')
|
||||
// die Pruefung liest frisch von Stripe
|
||||
assert.ok(callsTo('GET', '/v1/invoices').length >= 1)
|
||||
|
||||
// dieselbe Rechnung noch einmal zuordnen (Wiederaufnahme) bleibt moeglich
|
||||
const resume = await zuordnen('b1', { invoiceId: 'in_alt' })
|
||||
assert.equal(resume.status, 200, JSON.stringify(resume.body))
|
||||
assert.equal(stored('b1').matchedInvoiceId, 'in_alt')
|
||||
assert.equal(payCalls().length, 0)
|
||||
})
|
||||
|
||||
test('Buchung ist nur an einer offenen oder fremden Rechnung vermerkt -> kein Hindernis (M2)', async () => {
|
||||
seedTx('b1')
|
||||
seedTx('b2')
|
||||
// offene Rechnung mit Vermerk von b1 (pay scheiterte frueher) und eine bezahlte mit dem Vermerk von b2
|
||||
st.invoices.set('in_offen', rawInvoice({ id: 'in_offen', number: 'WK-0003', amount_remaining: 5000, total: 5000, metadata: { bankTransactionId: 'b1' } }))
|
||||
st.invoices.set('in_b2', rawInvoice({ id: 'in_b2', number: 'WK-0002', status: 'paid', amount_remaining: 0, amount_paid: 16879, metadata: { bankTransactionId: 'b2' } }))
|
||||
st.invoices.set('in_ewv', rawInvoice())
|
||||
assert.equal((await zuordnen('b1', { invoiceId: 'in_ewv' })).status, 200)
|
||||
assert.equal(payCalls().length, 1)
|
||||
})
|
||||
|
||||
test('Zuordnung zu einer Rechnung schreibt eine Protokollzeile mit Buchung, Rechnungsnummer und Nutzer, ohne Betrag (M3)', async () => {
|
||||
seedTx('b1')
|
||||
st.invoices.set('in_ewv', rawInvoice())
|
||||
const log = mock.method(console, 'log', () => {})
|
||||
assert.equal((await zuordnen('b1', { invoiceId: 'in_ewv' })).status, 200)
|
||||
assert.deepEqual(log.mock.calls.map((c) => c.arguments), [['[finanzen] Zuordnung b1 -> Rechnung WK-0005 durch kenso@webklar.com']])
|
||||
assert.ok(!log.mock.calls[0].arguments[0].includes('168'))
|
||||
})
|
||||
|
||||
test('Wiederaufnahme schreibt die Protokollzeile einmal beim Abschluss, nicht beim Scheitern (M3)', async () => {
|
||||
seedTx('b1')
|
||||
st.invoices.set('in_ewv', rawInvoice())
|
||||
mock.method(console, 'error', () => {})
|
||||
const log = mock.method(console, 'log', () => {})
|
||||
aw.failNext({ collection: TX(), method: 'PATCH', status: 500 })
|
||||
assert.equal((await zuordnen('b1', { invoiceId: 'in_ewv' })).status, 502)
|
||||
assert.equal(log.mock.callCount(), 0)
|
||||
assert.equal((await zuordnen('b1', { invoiceId: 'in_ewv' })).status, 200)
|
||||
assert.deepEqual(log.mock.calls.map((c) => c.arguments[0]), ['[finanzen] Zuordnung b1 -> Rechnung WK-0005 durch kenso@webklar.com'])
|
||||
})
|
||||
|
||||
test('Auszahlung, Fixkosten und Kategorie schreiben keine Zuordnungs-Protokollzeile (umkehrbar) (M3)', async () => {
|
||||
seedTx('b1')
|
||||
const log = mock.method(console, 'log', () => {})
|
||||
assert.equal((await zuordnen('b1', { category: 'privat' })).status, 200)
|
||||
assert.equal(log.mock.callCount(), 0)
|
||||
})
|
||||
|
||||
// ---------- Vorgemerkte Buchungen ----------
|
||||
|
||||
test('vorgemerkte Buchung: Rechnung und Auszahlung -> 409 vor jedem Stripe-Aufruf', async () => {
|
||||
|
||||
Reference in new issue
Block a user