Files
Webklar-Kundenbereich/test/auftraegeKontrolleRechnung.test.js
T
ClaudeandClaude Sonnet 5.5 e3ae152f12 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>
2026-10-07 15:18:22 +00:00

107 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Schlusskontrolle (Z3.4): Korrekturen an Projektrechnungen zu Auftragsbestaetigungen
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js'
const aw = await startAppwriteMock({
schema: { auftraege: schemaKeys('auftraege'), projektRechnungen: schemaKeys('projektRechnungen') },
datetimes: AUFTRAEGE_DATETIMES,
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { seedRechnungsDaten, legeAuftragAn, legeRechnungAn, leereRechnungsDaten } = await import('./auftraegeRechnungenHilfe.js')
const { createProjectInvoice, sendInvoiceNow, voidInvoice, deleteDraft, invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import('../server/services/finance/stripeFinance.js')
const { getAuftrag, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js')
const { umhaengen } = await import('../server/services/auftraege/projektRechnungenStore.js')
after(() => Promise.all([aw.close(), st.close()]))
seedRechnungsDaten(aw)
config.auftraege.mode = 'on'
beforeEach(async () => {
await leereRechnungsDaten(aw, st)
invalidateFinanceCache()
resetInvoiceCreateGuardForTests()
})
const letzterEntwurf = () => st.calls.filter((c) => c.method === 'POST' && c.path === '/v1/invoices').at(-1)
let zaehler = 0
const anzahlung = (ab, prozent, extra = {}) =>
createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, projektId: 'p1', anzahlung: { prozent, bezeichnung: `Anzahlung ${++zaehler}` }, ...extra })
const schluss = (ab, extra = {}) => createProjectInvoice({ customerId: 'k1', art: 'schluss', auftragId: ab.id, projektId: 'p1', ...extra })
test('Zahlungsziel: Anzahlung und Schluss ohne eigene Angabe nehmen das Zahlungsziel der AB, eine Angabe gilt vor der AB', async () => {
const ab = await legeAuftragAn({ zahlungszielTage: 30 })
await anzahlung(ab, 50, { senden: true })
assert.equal(letzterEntwurf().body.days_until_due, '30')
await schluss(ab)
assert.equal(letzterEntwurf().body.days_until_due, '30')
const ab2 = await legeAuftragAn({ zahlungszielTage: 30, projektId: 'p3' }, { nummer: 'AB-2026-0003', laufnummer: 3 })
await createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab2.id, projektId: 'p3', anzahlung: { prozent: 50 }, daysUntilDue: 7 })
assert.equal(letzterEntwurf().body.days_until_due, '7')
})
test('Zahlungsziel: ohne AB-Bezug (frei, festbetrag) bleibt es bei 14 Tagen', async () => {
await legeAuftragAn({ zahlungszielTage: 30 })
await createProjectInvoice({ customerId: 'k1', items: [{ description: 'Domain', quantity: 1, unitAmountCents: 1200 }] })
assert.equal(letzterEntwurf().body.days_until_due, '14')
})
const ENTWURF_ID = /^in_/
const finalisiert = (id) => st.calls.some((c) => c.method === 'POST' && c.path === `/v1/invoices/${id}/finalize`)
test('Senden: zwei Anzahlungs-Entwürfe à 60 % einer 800-€-AB – nur der erste geht, der zweite 409 und nichts finalisiert', async () => {
const ab = await legeAuftragAn()
const a = await anzahlung(ab, 60)
const b = await anzahlung(ab, 60)
assert.match(a.id, ENTWURF_ID)
await sendInvoiceNow(a.id)
st.calls.length = 0
await assert.rejects(sendInvoiceNow(b.id), (err) => err.status === 409 && /übersteig|Auftragssumme/.test(err.message))
assert.equal(finalisiert(b.id), false)
assert.equal(st.invoices.get(b.id).status, 'draft')
})
test('Senden: parallel gesendet geht von zwei 60-%-Entwürfen genau einer', async () => {
const ab = await legeAuftragAn()
const a = await anzahlung(ab, 60)
const b = await anzahlung(ab, 60)
const ergebnisse = await Promise.allSettled([sendInvoiceNow(a.id), sendInvoiceNow(b.id)])
assert.deepEqual(ergebnisse.map((e) => e.status).sort(), ['fulfilled', 'rejected'])
assert.equal([a.id, b.id].filter((id) => st.invoices.get(id).status !== 'draft').length, 1)
})
test('Senden: ein Anzahlungs-Entwurf zu einer stornierten, widerrufenen oder ersetzten AB wird nicht gesendet', async () => {
for (const status of ['storniert', 'widerrufen', 'ersetzt']) {
const ab = await legeAuftragAn({}, { nummer: `AB-2026-${status}` })
const entwurf = await anzahlung(ab, 30, { projektId: 'p1', anzahlung: { prozent: 30, bezeichnung: `A ${status}` } })
await updateAuftrag(ab.id, { status })
await assert.rejects(sendInvoiceNow(entwurf.id), (err) => err.status === 409 && /nicht mehr gültig/.test(err.message), status)
assert.equal(finalisiert(entwurf.id), false, status)
await deleteDraft(entwurf.id)
await updateAuftrag(ab.id, { status: 'storniert' })
}
})
test('Senden: ein Schluss-Entwurf wird nicht gesendet, wenn zur AB schon eine andere Schlussrechnung festgeschrieben ist', async () => {
const ab = await legeAuftragAn()
const s = await schluss(ab)
await legeRechnungAn(st, 'in_fremd1', { status: 'open', total: 80000, art: 'schluss', auftragId: ab.id })
await assert.rejects(sendInvoiceNow(s.id), (err) => err.status === 409)
assert.equal(finalisiert(s.id), false)
})
test('Senden: ein gültiger Anzahlungs- oder Schluss-Entwurf geht wie bisher', async () => {
const ab = await legeAuftragAn()
const a = await anzahlung(ab, 50)
assert.equal((await sendInvoiceNow(a.id)).status, 'offen')
const s = await schluss(ab)
assert.equal((await sendInvoiceNow(s.id)).status, 'offen')
assert.equal((await getAuftrag(ab.id)).status, 'abgerechnet')
})