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
@@ -168,7 +168,7 @@ function sendFinanceError(res, what, err) {
|
||||
router.post('/buchungen/:id/zuordnen', async (req, res) => {
|
||||
const { invoiceId, payoutId, fixedCostId, category, note } = req.body || {}
|
||||
try {
|
||||
const buchung = await assignTransaction(req.params.id, { invoiceId, payoutId, fixedCostId, category, note })
|
||||
const buchung = await assignTransaction(req.params.id, { invoiceId, payoutId, fixedCostId, category, note }, { by: req.appwriteUser?.email })
|
||||
return res.json({ buchung: publicDoc(buchung) })
|
||||
} catch (err) {
|
||||
return sendFinanceError(res, 'Buchung zuordnen', err)
|
||||
|
||||
@@ -116,14 +116,18 @@ async function resolveInvoice(tx, invoiceId) {
|
||||
// Der Vermerk steht vor dem Bezahlen an der Rechnung: er verraet einer Wiederholung, dass die Rechnung zu dieser
|
||||
// Buchung gehoert (Stripe liefert paid_out_of_band nicht an der Rechnung zurueck)
|
||||
const linked = invoice.metadata?.bankTransactionId === tx.$id
|
||||
// Schon bezahlt und mit dieser Buchung gekennzeichnet: ein frueherer Versuch scheiterte erst danach (Appwrite),
|
||||
// es bleibt nur, die Buchung zu speichern
|
||||
const resumed = invoice.status === 'paid' && linked
|
||||
// Schon bezahlt und mit dieser Buchung gekennzeichnet (und in genau dieser Hoehe bezahlt): ein frueherer Versuch
|
||||
// scheiterte erst danach (Appwrite), es bleibt nur, die Buchung zu speichern
|
||||
const resumed = invoice.status === 'paid' && linked && invoice.amount_paid === tx.amountCents
|
||||
if (!resumed) {
|
||||
if (invoice.status !== 'open') throw new AssignError(409, 'Diese Rechnung ist nicht mehr offen.')
|
||||
if (invoice.amount_remaining !== tx.amountCents) {
|
||||
throw new AssignError(409, `Betrag weicht vom offenen Betrag ab (${euroDe(invoice.amount_remaining)}). Teilzahlungen bitte direkt in Stripe erfassen.`)
|
||||
}
|
||||
// Gehoert die Buchung schon zu einer anderen, bezahlten Rechnung (deren Speichern in Appwrite scheiterte), wuerde sie
|
||||
// sonst ein zweites Mal bezahlt verbucht. Frisch von Stripe, nicht aus dem Zwischenspeicher.
|
||||
const claimed = (await listInvoices({ force: true })).some((other) => other.id !== invoice.id && other.bankTransactionId === tx.$id && other.status === 'bezahlt')
|
||||
if (claimed) throw new AssignError(409, 'Diese Buchung ist bereits einer anderen Rechnung zugeordnet.')
|
||||
// Erst der Vermerk (umkehrbar), dann das Bezahlen (nicht umkehrbar): scheitert der Vermerk, bleibt die Rechnung offen
|
||||
if (!linked) {
|
||||
await stripe.invoices.update(invoice.id, { metadata: { bankTransactionId: tx.$id, bankBookingDate: tx.bookingDate || '' } })
|
||||
@@ -171,10 +175,12 @@ function resolveCategory(tx, category) {
|
||||
* Buchung zuordnen. Genau eines von invoiceId, payoutId, fixedCostId oder category (rechnung-extern, privat,
|
||||
* sonstiges); note ist optional (hoechstens 500 Zeichen). Liefert die gespeicherte Buchung. Fehler (AssignError):
|
||||
* 400 bei falscher Eingabe oder falscher Richtung, 404 bei unbekannter Buchung, Rechnung oder Auszahlung, 409 wenn
|
||||
* die Buchung schon zugeordnet ist, die Rechnung nicht (mehr) offen ist oder der Betrag nicht passt. Stripe-Fehler
|
||||
* fliegen unveraendert weiter.
|
||||
* die Buchung schon zugeordnet ist, die Rechnung nicht (mehr) offen ist, der Betrag nicht passt oder die Buchung schon
|
||||
* zu einer anderen bezahlten Rechnung gehoert; 502, wenn die Rechnung in Stripe bezahlt wurde, die Buchung aber nicht
|
||||
* gespeichert werden konnte (erneut zuordnen schliesst ab). Stripe-Fehler fliegen unveraendert weiter. by = Mail des
|
||||
* Nutzers fuer die Protokollzeile bei einer Rechnung.
|
||||
*/
|
||||
export async function assignTransaction(txId, input = {}) {
|
||||
export async function assignTransaction(txId, input = {}, { by = '' } = {}) {
|
||||
const request = parseInput(input)
|
||||
const id = documentId(txId)
|
||||
return withLocks(lockKeys(id, request), async () => {
|
||||
@@ -186,7 +192,21 @@ export async function assignTransaction(txId, input = {}) {
|
||||
else if (request.kind === 'payout') fields = await resolvePayout(tx, request.value)
|
||||
else if (request.kind === 'fixedCost') fields = await resolveFixedCost(tx, request.value)
|
||||
else fields = resolveCategory(tx, request.value)
|
||||
return updateTransaction(id, request.note === undefined ? fields : { ...fields, note: request.note })
|
||||
let saved
|
||||
try {
|
||||
saved = await updateTransaction(id, request.note === undefined ? fields : { ...fields, note: request.note })
|
||||
} catch (cause) {
|
||||
// Bei einer Rechnung ist sie in Stripe schon bezahlt (nicht umkehrbar): die Wiederholung schliesst die Zuordnung ab
|
||||
if (request.kind !== 'invoice') throw cause
|
||||
const err = new AssignError(502, 'In Stripe als bezahlt markiert, aber die Buchung wurde nicht gespeichert. Bitte erneut zuordnen.')
|
||||
err.cause = cause
|
||||
throw err
|
||||
}
|
||||
// Nicht umkehrbar (Stripe hat die Rechnung bezahlt gebucht): wer, welche Buchung, welche Rechnung; ohne Betraege
|
||||
if (request.kind === 'invoice') {
|
||||
console.log(`[finanzen] Zuordnung ${id} -> Rechnung ${fields.matchedInvoiceNumber || fields.matchedInvoiceId} durch ${by || 'unbekannt'}`)
|
||||
}
|
||||
return saved
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user