Finanzen: Zuordnung zu Rechnungen erst vermerken, dann bezahlen; Vormerkungen nicht zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2bf67cb48d
commit
6e43d54fe8
4 files changed
+122
-20
No files matched your search
@@ -12,7 +12,10 @@ import { invalidateFinanceCache, listInvoices, listPayouts } from './stripeFinan
|
||||
* bezahlt markiert, "paid_out_of_band", und das ist nicht umkehrbar) oder zu einer Stripe-Auszahlung, eine
|
||||
* Belastung zu einer Fixkosten-Position, oder eine Buchung nur mit Kategorie (Rechnung ausserhalb von Stripe,
|
||||
* privat, sonstiges). Zuordnungen laufen je Buchung nacheinander (keyedLock), damit zwei Klicks nie zweimal bei
|
||||
* Stripe bezahlen. Die Meldungen sind fuer die Oberflaeche bestimmt (AssignError mit status).
|
||||
* Stripe bezahlen. Rechnung und Auszahlung nur zu gebuchten Eintraegen (nicht zu Vormerkungen). Bei einer Rechnung
|
||||
* steht erst der Vermerk (Buchungs-ID in den Metadaten) und dann das Bezahlen, damit eine Wiederholung nach einem
|
||||
* Fehler an Appwrite die Zuordnung ohne zweites Bezahlen abschliesst. Die Meldungen sind fuer die Oberflaeche
|
||||
* bestimmt (AssignError mit status).
|
||||
*/
|
||||
|
||||
/** Fehler mit HTTP-Status und einer Meldung, die die Seite anzeigen darf (nur diese gibt die Route weiter). */
|
||||
@@ -34,6 +37,8 @@ const STRIPE_INVOICE_ID = /^in_\w+$/
|
||||
const DOCUMENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
|
||||
const UNASSIGNED = { category: '', matchedInvoiceId: '', matchedInvoiceNumber: '', matchedPayoutId: '', fixedCostId: '' }
|
||||
|
||||
const PENDING_MESSAGE = 'Die Buchung ist noch vorgemerkt. Bitte zuordnen, wenn sie gebucht ist.'
|
||||
|
||||
export const STRIPE_SUGGESTIONS_ERROR = 'Stripe ist gerade nicht erreichbar. Zuordnungsvorschläge fehlen.'
|
||||
|
||||
const isIncoming = (tx) => Number.isSafeInteger(tx.amountCents) && tx.amountCents > 0
|
||||
@@ -97,28 +102,34 @@ async function stripeOrNotFound(call, message) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rechnungen und Auszahlungen nur zu gebuchten Eintraegen: eine Vormerkung kann bis zur Buchung wieder verschwinden. */
|
||||
function requireBooked(tx) {
|
||||
if (tx.status !== 'BOOK') throw new AssignError(409, PENDING_MESSAGE)
|
||||
}
|
||||
|
||||
async function resolveInvoice(tx, invoiceId) {
|
||||
if (!isIncoming(tx)) throw new AssignError(400, 'Nur Eingänge lassen sich einer Rechnung zuordnen.')
|
||||
requireBooked(tx)
|
||||
if (!STRIPE_INVOICE_ID.test(invoiceId)) throw new AssignError(404, 'Rechnung nicht gefunden.')
|
||||
const stripe = getStripe()
|
||||
try {
|
||||
const invoice = await stripeOrNotFound(() => stripe.invoices.retrieve(invoiceId), 'Rechnung nicht gefunden.')
|
||||
// Eine fruehere Zuordnung kann nach dem Bezahlen an Appwrite gescheitert sein: dann ist die Rechnung schon als
|
||||
// bezahlt mit genau dieser Buchung gekennzeichnet und es bleibt nur, die Buchung zu speichern
|
||||
const resumed =
|
||||
invoice.status === 'paid' && invoice.paid_out_of_band === true && invoice.metadata?.bankTransactionId === tx.$id
|
||||
// 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
|
||||
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.`)
|
||||
}
|
||||
await stripe.invoices.pay(invoice.id, { paid_out_of_band: true })
|
||||
// Die Rechnung ist jetzt bezahlt (nicht umkehrbar): scheitert nur der Vermerk, bleibt die Zuordnung gueltig
|
||||
try {
|
||||
// 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 || '' } })
|
||||
} catch (err) {
|
||||
console.error('[finanzen] Vermerk an der Stripe-Rechnung fehlgeschlagen:', err?.name || 'Error', err?.statusCode ?? '', err?.code ?? '')
|
||||
}
|
||||
await stripe.invoices.pay(invoice.id, { paid_out_of_band: true })
|
||||
}
|
||||
return { category: 'rechnung', matchedInvoiceId: invoice.id, matchedInvoiceNumber: invoice.number || '' }
|
||||
} finally {
|
||||
@@ -128,6 +139,7 @@ async function resolveInvoice(tx, invoiceId) {
|
||||
|
||||
async function resolvePayout(tx, payoutId) {
|
||||
if (!isIncoming(tx)) throw new AssignError(400, 'Nur Eingänge lassen sich einer Auszahlung zuordnen.')
|
||||
requireBooked(tx)
|
||||
// Frisch von Stripe (nicht aus dem 60-s-Zwischenspeicher): die Zuordnung darf nicht auf einem alten Stand beruhen
|
||||
const payout = (await listPayouts({ force: true })).find((item) => item.id === payoutId)
|
||||
if (!payout) throw new AssignError(404, 'Auszahlung nicht gefunden.')
|
||||
|
||||
Reference in new issue
Block a user