Rechnungen: Sperre je Kunde gegen doppelte Abrechnung (Aufwand, Anzahlung, Schluss)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d264f2af5a
commit
ab3e6938da
2 files changed
+46
-2
No files matched your search
@@ -2,7 +2,8 @@ import { createHash } from 'node:crypto'
|
|||||||
import { config } from '../../config.js'
|
import { config } from '../../config.js'
|
||||||
import { getStripe, getOrCreateStripeCustomer, stripeCustomerFields, subscriptionIdOfInvoice } from '../stripeBilling.js'
|
import { getStripe, getOrCreateStripeCustomer, stripeCustomerFields, subscriptionIdOfInvoice } from '../stripeBilling.js'
|
||||||
import { addDays, berlinDate, daysBetween } from './dates.js'
|
import { addDays, berlinDate, daysBetween } from './dates.js'
|
||||||
import { RECHNUNGS_ARTEN } from '../auftraege/konstanten.js'
|
import { RECHNUNGS_ARTEN, sperreRechnung } from '../auftraege/konstanten.js'
|
||||||
|
import { withLocks } from '../keyedLock.js'
|
||||||
import { baueRechnung, ladeFestenKunden } from '../auftraege/rechnungsbau.js'
|
import { baueRechnung, ladeFestenKunden } from '../auftraege/rechnungsbau.js'
|
||||||
import { createProjektRechnung } from '../auftraege/projektRechnungenStore.js'
|
import { createProjektRechnung } from '../auftraege/projektRechnungenStore.js'
|
||||||
import { getSettings } from './settings.js'
|
import { getSettings } from './settings.js'
|
||||||
@@ -330,7 +331,10 @@ export async function createProjectInvoice({
|
|||||||
claimCreate(fingerprint)
|
claimCreate(fingerprint)
|
||||||
try {
|
try {
|
||||||
const eingabe = { customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, createdBy }
|
const eingabe = { customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, createdBy }
|
||||||
const invoice = await createInvoice({ eingabe, daysUntilDue, note, senden: senden === true, key })
|
const anlegen = () => createInvoice({ eingabe, daysUntilDue, note, senden: senden === true, key })
|
||||||
|
// Aufwand, Anzahlung und Schluss: Pruefung, Entwurf, Posten, Projektdatensatz und Senden je Kunde nacheinander,
|
||||||
|
// damit zwei Admins nie dieselben Arbeitsblaetter oder dieselbe AB gleichzeitig abrechnen
|
||||||
|
const invoice = ['aufwand', 'anzahlung', 'schluss'].includes(art) ? await withLocks(sperreRechnung(customerId), anlegen) : await anlegen()
|
||||||
releaseCreate(fingerprint, true)
|
releaseCreate(fingerprint, true)
|
||||||
return invoice
|
return invoice
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -163,3 +163,43 @@ test('rechnungsVorschau: dieselben Prüfungen wie das Anlegen; Anzahlung ohne An
|
|||||||
assert.deepEqual([v.summeCents, v.positionen[0].schluessel], [40000, 'anzahlung'])
|
assert.deepEqual([v.summeCents, v.positionen[0].schluessel], [40000, 'anzahlung'])
|
||||||
assert.equal(aw.docs(config.collections.projektRechnungen).size, 1, 'nur der Entwurf von oben')
|
assert.equal(aw.docs(config.collections.projektRechnungen).size, 1, 'nur der Entwurf von oben')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Aufwand: zwei gleichzeitige Rechnungen mit denselben Blättern -> genau eine entsteht, die andere bekommt 409', async () => {
|
||||||
|
st.delayNext({ method: 'POST', path: '/v1/invoices', ms: 150 })
|
||||||
|
const eingabe = (memo) => ({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1', 'ws2'], memo, createdBy: 'k' })
|
||||||
|
const ergebnisse = await Promise.allSettled([createProjectInvoice(eingabe('Admin A')), createProjectInvoice(eingabe('Admin B'))])
|
||||||
|
assert.deepEqual(ergebnisse.map((e) => e.status), ['fulfilled', 'rejected'])
|
||||||
|
assert.equal(ergebnisse[1].reason.status, 409)
|
||||||
|
assert.equal(ergebnisse[1].reason.message, 'Arbeitsblatt bereits abgerechnet (Entwurf).')
|
||||||
|
assert.equal(st.invoices.size, 1)
|
||||||
|
assert.equal(callsTo('POST', '/v1/invoices').length, 1)
|
||||||
|
assert.equal(aw.docs(config.collections.projektRechnungen).size, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Aufwand: eine nur teilweise überschneidende Auswahl wird abgewiesen, die übrigen Blätter bleiben frei', async () => {
|
||||||
|
const eingabe = (ids, memo) => ({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ids, memo })
|
||||||
|
await createProjectInvoice(eingabe(['ws1', 'ws2'], 'A'))
|
||||||
|
await wirft(createProjectInvoice(eingabe(['ws2', 'ws3'], 'B')), 409, 'Arbeitsblatt bereits abgerechnet (Entwurf).')
|
||||||
|
const rest = await createProjectInvoice(eingabe(['ws3'], 'C'))
|
||||||
|
assert.equal(rest.totalCents, 10000)
|
||||||
|
assert.equal(st.invoices.size, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Doppelklick (gleicher Inhalt gleichzeitig): der Schutz vor doppeltem Anlegen greift, es entsteht eine Rechnung', async () => {
|
||||||
|
const eingabe = { customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1'] }
|
||||||
|
const ergebnisse = await Promise.allSettled([createProjectInvoice(eingabe), createProjectInvoice(eingabe)])
|
||||||
|
assert.deepEqual(ergebnisse.map((e) => e.status), ['fulfilled', 'rejected'])
|
||||||
|
assert.equal(ergebnisse[1].reason.message, 'Diese Rechnung wird gerade schon erstellt.')
|
||||||
|
assert.equal(st.invoices.size, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Schluss: zwei gleichzeitige Schlussrechnungen zur selben AB -> eine entsteht, die andere stößt auf den Entwurf (409)', async () => {
|
||||||
|
const ab = await legeAuftragAn()
|
||||||
|
st.delayNext({ method: 'POST', path: '/v1/invoices', ms: 150 })
|
||||||
|
const eingabe = (memo) => ({ customerId: 'k1', art: 'schluss', auftragId: ab.id, memo })
|
||||||
|
const ergebnisse = await Promise.allSettled([createProjectInvoice(eingabe('A')), createProjectInvoice(eingabe('B'))])
|
||||||
|
assert.deepEqual(ergebnisse.map((e) => e.status), ['fulfilled', 'rejected'])
|
||||||
|
assert.equal(ergebnisse[1].reason.status, 409)
|
||||||
|
assert.equal(ergebnisse[1].reason.message, `Erst den Entwurf über 800,00${NBSP}€ senden oder löschen.`)
|
||||||
|
assert.equal(st.invoices.size, 1)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user