Kontrolle: Senden eines Anzahlungs- oder Schluss-Entwurfs prueft die AB unter der Kundensperre erneut

Unter sperreRechnung(kundenId): AB noch versendet oder bestaetigt, bei Schluss keine
andere festgeschriebene Schlussrechnung, bei Anzahlung Summe aller festgeschriebenen
Rechnungen plus diese nicht ueber der AB-Summe, sonst 409 und nichts wird finalisiert.

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:22 +00:00
1 parent 57d1ab82c6
commit e3ae152f12
3 files changed
+99 -4

No files matched your search

+30 -1
View File
@@ -17,7 +17,7 @@ import {
} from './positionen.js'
import { auftraegeAktiv } from './schalter.js'
import { getAuftrag, listAuftraege, updateAuftrag } from './auftragStore.js'
import { listProjektRechnungen, updateProjektRechnung } from './projektRechnungenStore.js'
import { getProjektRechnung, listProjektRechnungen, updateProjektRechnung } from './projektRechnungenStore.js'
import {
ladeArbeitsblaetter, ladeArbeitsblaetterNachIds, abrechenbar, abgerechneteArbeitsblaetter, offeneArbeitsblaetter, zeitquelleFuerProjekt,
} from './arbeitsblaetter.js'
@@ -380,3 +380,32 @@ async function baueAbRechnung(basis, eingabe) {
}
return schliesseAb({ ...basis, projekt, auftrag, positionen })
}
const AB_UNGUELTIG = 'Die Auftragsbestätigung ist nicht mehr gültig (storniert, widerrufen, ersetzt oder schon abgerechnet). Der Entwurf wird nicht gesendet.'
/**
* Letzte Pruefung vor dem Senden eines Anzahlungs- oder Schluss-Entwurfs (Aufrufer haelt sperreRechnung des Kunden):
* die AB muss noch versendet oder bestaetigt sein, bei einer Schlussrechnung darf keine andere Schlussrechnung
* festgeschrieben sein, und bei einer Anzahlung darf die Summe aller festgeschriebenen Rechnungen zur AB plus diese die
* AB-Summe nicht uebersteigen (zwei Entwuerfe zaehlen beim Anlegen nicht mit). Sonst AuftragError 409; nichts wird
* geschrieben ausser der Selbstheilung von ladeFruehere. Die AB kommt aus dem Projektdatensatz (nach umhaengen die neue),
* ersatzweise aus den Stripe-Metadaten; ohne AB gibt es nichts zu pruefen.
*/
export async function pruefeVorSenden({ invoiceId, art, auftragIdMetadaten = '', totalCents }) {
if (!AB_RECHNUNGS_ARTEN.includes(art)) return
const auftragId = (await getProjektRechnung(invoiceId))?.auftragId || auftragIdMetadaten
if (!auftragId) return
const ab = await getAuftrag(auftragId)
if (!ab || !['versendet', 'bestaetigt'].includes(ab.status)) throw new AuftragError(409, AB_UNGUELTIG)
const andere = (await ladeFruehere(ab.id)).filter((f) => f.invoiceId !== invoiceId && FINALISIERT.includes(f.status))
if (art === 'schluss' && andere.some((f) => f.art === 'schluss')) {
throw new AuftragError(409, 'Zu dieser Auftragsbestätigung gibt es schon eine Schlussrechnung. Der Entwurf wird nicht gesendet.')
}
// Eine Schlussrechnung darf mit Nachtraegen ueber der AB-Summe liegen: die Obergrenze gilt nur fuer die Anzahlung
if (art === 'anzahlung') {
const berechnet = andere.reduce((summe, f) => summe + f.betragCents, 0)
if (berechnet + totalCents > ab.summeCents) {
throw new AuftragError(409, `Die Summe der Rechnungen würde die Auftragssumme von ${euroDe(ab.summeCents)} übersteigen (bereits berechnet ${euroDe(berechnet)}). Der Entwurf wird nicht gesendet.`)
}
}
}
+13 -1
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 } from '../auftraege/rechnungsbau.js'
import { baueRechnung, ladeFestenKunden, buche, gibProjektRechnungFrei, setzeAbAbgerechnet, pruefeVorSenden } from '../auftraege/rechnungsbau.js'
import { createProjektRechnung, updateProjektRechnung } from '../auftraege/projektRechnungenStore.js'
import { getSettings } from './settings.js'
@@ -457,6 +457,17 @@ async function createInvoice({ eingabe, daysUntilDue: angegeben, note, senden, k
*/
export async function sendInvoiceNow(invoiceId) {
const raw = await retrieveInvoice(invoiceId)
const vorab = mapInvoice(raw, { today: berlinDate() })
// Anzahlungs- und Schluss-Entwuerfe: unter der Sperre des Kunden (wie das Anlegen) senden und vorher die AB erneut pruefen
if (vorab.source === 'projekt' && vorab.status === 'entwurf' && AB_RECHNUNGS_ARTEN.includes(vorab.art) && vorab.webklarCustomerId) {
return withLocks(sperreRechnung(vorab.webklarCustomerId), () => sendeRechnung(invoiceId, null, true))
}
return sendeRechnung(invoiceId, raw, false)
}
// raw = die schon gelesene Rechnung; unter der Sperre wird frisch gelesen (ein anderer Aufruf kann sie inzwischen gesendet haben)
async function sendeRechnung(invoiceId, raw, abPruefen) {
raw ??= await retrieveInvoice(invoiceId)
const invoice = mapInvoice(raw, { today: berlinDate() })
// Ein Abo-Entwurf gehoert Stripe: Finalisieren mit auto_advance=false wuerde den Einzug des Abos anhalten
if (invoice.source !== 'projekt') throw httpError(409, ABO_MESSAGE)
@@ -464,6 +475,7 @@ export async function sendInvoiceNow(invoiceId) {
if (!isDraft && invoice.status !== 'offen' && invoice.status !== 'ueberfaellig') {
throw httpError(409, 'Diese Rechnung kann nicht mehr gesendet werden.')
}
if (isDraft && abPruefen) await pruefeVorSenden({ invoiceId: invoice.id, art: invoice.art, auftragIdMetadaten: invoice.auftragId, totalCents: invoice.totalCents })
// Nur ein Entwurf wird festgeschrieben: erst dann zaehlen Kundendaten und Anschrift. Eine offene Rechnung steht schon
// fest (Anschrift und Empfaenger sind festgeschrieben), sie wird nur erneut gesendet.
let customer = null