Kontrolle: Storno einer Anzahlung bei bestehendem Schluss-Entwurf der AB abgewiesen

409 'Erst den Entwurf der Schlussrechnung löschen.'; das Storno einer Anzahlung laeuft
unter der Kundensperre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 15:18:46 +00:00
1 parent e3ae152f12
commit 1bb172ad91
3 files changed
+38 -7

No files matched your search

@@ -409,3 +409,10 @@ export async function pruefeVorSenden({ invoiceId, art, auftragIdMetadaten = '',
}
}
}
/** Gibt es zur AB (aus dem Projektdatensatz der Rechnung, ersatzweise aus den Stripe-Metadaten) einen Entwurf der Schlussrechnung? */
export async function hatSchlussEntwurf({ invoiceId, auftragIdMetadaten = '' }) {
const auftragId = (await getProjektRechnung(invoiceId))?.auftragId || auftragIdMetadaten
if (!auftragId) return false
return (await ladeFruehere(auftragId)).some((f) => f.art === 'schluss' && f.status === 'draft')
}
+15 -7
View File
@@ -4,7 +4,7 @@ import { getStripe, getOrCreateStripeCustomer, stripeCustomerFields, subscriptio
import { addDays, berlinDate, daysBetween } from './dates.js'
import { RECHNUNGS_ARTEN, AB_RECHNUNGS_ARTEN, sperreRechnung } from '../auftraege/konstanten.js'
import { withLocks } from '../keyedLock.js'
import { baueRechnung, ladeFestenKunden, buche, gibProjektRechnungFrei, setzeAbAbgerechnet, pruefeVorSenden } from '../auftraege/rechnungsbau.js'
import { baueRechnung, ladeFestenKunden, buche, gibProjektRechnungFrei, setzeAbAbgerechnet, pruefeVorSenden, hatSchlussEntwurf } from '../auftraege/rechnungsbau.js'
import { createProjektRechnung, updateProjektRechnung } from '../auftraege/projektRechnungenStore.js'
import { getSettings } from './settings.js'
@@ -507,13 +507,21 @@ export async function voidInvoice(invoiceId) {
const invoice = await readInvoice(invoiceId)
if (invoice.source !== 'projekt') throw httpError(409, ABO_MESSAGE)
if (invoice.status !== 'offen' && invoice.status !== 'ueberfaellig') throw httpError(409, 'Nur offene Rechnungen können storniert werden.')
try {
await getStripe().invoices.voidInvoice(invoice.id)
await bucheStorniert(invoice.id, invoice.art)
return await readInvoice(invoice.id)
} finally {
invalidateFinanceCache()
const stornieren = async () => {
// Der Schluss-Entwurf enthaelt die Minuszeile dieser Anzahlung: wuerde sie storniert, ginge er zu niedrig hinaus
if (invoice.art === 'anzahlung' && (await hatSchlussEntwurf({ invoiceId: invoice.id, auftragIdMetadaten: invoice.auftragId }))) {
throw httpError(409, 'Erst den Entwurf der Schlussrechnung löschen.')
}
try {
await getStripe().invoices.voidInvoice(invoice.id)
await bucheStorniert(invoice.id, invoice.art)
return await readInvoice(invoice.id)
} finally {
invalidateFinanceCache()
}
}
// Eine Anzahlung unter der Sperre des Kunden: das Anlegen und Senden einer Schlussrechnung laeuft nie zwischen Pruefung und Storno
return invoice.art === 'anzahlung' && invoice.webklarCustomerId ? withLocks(sperreRechnung(invoice.webklarCustomerId), stornieren) : stornieren()
}
/** Projekt-Entwurf bei Stripe loeschen, sonst status 409 (nur Entwuerfe lassen sich loeschen). */
+16
View File
@@ -104,3 +104,19 @@ test('Senden: ein gültiger Anzahlungs- oder Schluss-Entwurf geht wie bisher', a
assert.equal((await sendInvoiceNow(s.id)).status, 'offen')
assert.equal((await getAuftrag(ab.id)).status, 'abgerechnet')
})
test('Storno: eine Anzahlung wird nicht storniert, solange zur AB ein Schluss-Entwurf besteht (409); nach dem Löschen des Entwurfs geht es', async () => {
const ab = await legeAuftragAn()
const a = await anzahlung(ab, 50, { senden: true })
const s = await schluss(ab)
await assert.rejects(voidInvoice(a.id), (err) => err.status === 409 && err.message === 'Erst den Entwurf der Schlussrechnung löschen.')
assert.equal(st.invoices.get(a.id).status, 'open')
await deleteDraft(s.id)
assert.equal((await voidInvoice(a.id)).status, 'storniert')
})
test('Storno: eine Anzahlung ohne Schluss-Entwurf lässt sich stornieren', async () => {
const ab = await legeAuftragAn()
const a = await anzahlung(ab, 50, { senden: true })
assert.equal((await voidInvoice(a.id)).status, 'storniert')
})