Files
Webklar-Kundenbereich/test/auftraegeRoutesRechnungen.test.js
ClaudeandClaude Sonnet 5.5 57d1ab82c6 Kontrolle: Anzahlung und Schlussrechnung nehmen das Zahlungsziel der AB
Ohne daysUntilDue gilt bei Anzahlung und Schluss auftrag.zahlungszielTage (Route
und createProjectInvoice), sonst weiterhin 14 Tage; eine Angabe gilt vor der AB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:17:06 +00:00

139 lines
8.9 KiB
JavaScript
Raw Permalink 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.
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, leereRechnungsDaten } = await import('./auftraegeRechnungenHilfe.js')
const { invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import('../server/services/finance/stripeFinance.js')
const { startTestApp, post, rawGet } = await import('./support/testApp.js')
const { default: financeRoutes } = await import('../server/routes/admin/finanzen.js')
const app = await startTestApp({ '/api/admin/finanzen': financeRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
seedRechnungsDaten(aw)
config.auftraege.mode = 'on'
beforeEach(async () => {
await leereRechnungsDaten(aw, st)
invalidateFinanceCache()
resetInvoiceCreateGuardForTests()
})
const admin = { headers: { Authorization: 'Bearer jwt-admin' } }
const staff = { headers: { Authorization: 'Bearer jwt-staff' } }
const base = '/api/admin/finanzen'
const POSTEN = [{ description: 'Domain', quantity: 1, unitAmountCents: 1200 }]
const SCHALTER_AUS = 'Auftragsbestätigungen sind für diesen Kunden noch nicht freigeschaltet.'
const schreibaufrufe = () => st.calls.filter((c) => c.method !== 'GET')
const erwarte = async (pfad, body, status, error) => {
const res = await post(`${base}${pfad}`, body, admin)
assert.equal(res.status, status, JSON.stringify(res.body))
assert.deepEqual(res.body, { error })
}
test('POST /rechnungen: der bisherige Körper (ohne art) legt eine freie A4-Rechnung an', async () => {
const res = await post(`${base}/rechnungen`, { customerId: 'k1', items: POSTEN, daysUntilDue: 14, memo: '', senden: false, idempotencyKey: 'form-1' }, admin)
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual([res.body.rechnung.art, res.body.rechnung.status, res.body.rechnung.totalCents], ['frei', 'entwurf', 1200])
assert.equal(st.calls.find((c) => c.method === 'POST' && c.path === '/v1/invoices').body['rendering[pdf][page_size]'], 'a4')
})
test('POST /rechnungen: Festbetrag, Aufwand, Anzahlung und Schluss tragen Projekt, Auftrag und Art in der Antwort', async () => {
const ab = await legeAuftragAn()
const antwort = async (body) => {
const res = await post(`${base}/rechnungen`, body, admin)
assert.equal(res.status, 200, JSON.stringify(res.body))
return res.body.rechnung
}
const fest = await antwort({ customerId: 'k1', art: 'festbetrag', projektId: 'p1', items: POSTEN })
assert.deepEqual([fest.art, fest.projektId, fest.projektName, fest.auftragId], ['festbetrag', 'p1', 'Website anelia-wolf.com', ''])
const aufwand = await antwort({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1', 'ws2'] })
assert.deepEqual([aufwand.art, aufwand.totalCents], ['aufwand', 22500])
const anzahlung = await antwort({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung: { prozent: 25 }, senden: 'true' })
assert.deepEqual([anzahlung.art, anzahlung.status, anzahlung.totalCents, anzahlung.auftragNummer], ['anzahlung', 'offen', 20000, 'AB-2026-0002'])
const schluss = await antwort({ customerId: 'k1', art: 'schluss', auftragId: ab.id })
assert.deepEqual([schluss.art, schluss.totalCents], ['schluss', 60000])
})
test('POST /rechnungen/vorschau: Positionen, Summe, Zeitraum und Zusatzfelder, ohne Stripe-Schreibzugriff und ohne Datensatz', async () => {
const res = await post(`${base}/rechnungen/vorschau`, { customerId: 'k1', art: 'aufwand', projektId: 'p1' }, admin)
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(Object.keys(res.body).sort(), ['customFields', 'leistungszeitraum', 'positionen', 'summeCents'])
assert.equal(res.body.summeCents, 32500)
assert.deepEqual(res.body.positionen.map((p) => p.schluessel), ['2026-10-06', '2026-10-07'])
assert.equal(res.body.leistungszeitraum, '06.10.–07.10.2026')
assert.deepEqual(res.body.customFields.map((f) => f.name), ['Projekt', 'Leistungszeitraum'])
assert.equal(schreibaufrufe().length, 0)
assert.equal(aw.docs(config.collections.projektRechnungen).size, 0)
})
test('Fehler kommen mit Status und nur der Meldung an (400, 404, 409), Schalter aus 400; nichts bei Stripe', async () => {
const ab = await legeAuftragAn()
for (const pfad of ['/rechnungen', '/rechnungen/vorschau']) {
await erwarte(pfad, { customerId: 'k1', art: 'quatsch', items: POSTEN }, 400, 'Unbekannte Rechnungsart.')
await erwarte(pfad, { customerId: 'gibtEsNicht', items: POSTEN }, 400, 'Kunde nicht gefunden.')
await erwarte(pfad, { customerId: 'k1', art: 'festbetrag', projektId: 'p2', items: POSTEN }, 404, 'Projekt nicht gefunden.')
await erwarte(pfad, { customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['wsFremd'] }, 400, 'Arbeitsblatt nicht gefunden.')
await erwarte(pfad, { customerId: 'k1', art: 'schluss', auftragId: 'gibtEsNicht' }, 404, 'Auftrag nicht gefunden.')
config.auftraege.mode = 'off'
try {
await erwarte(pfad, { customerId: 'k1', art: 'anzahlung', auftragId: ab.id }, 400, SCHALTER_AUS)
} finally {
config.auftraege.mode = 'on'
}
}
assert.equal(schreibaufrufe().length, 0)
})
test('Abgerechnete Arbeitsblätter: Anlegen und Vorschau antworten 409 mit der Meldung', async () => {
const res = await post(`${base}/rechnungen`, { customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1'] }, admin)
assert.equal(res.status, 200, JSON.stringify(res.body))
for (const pfad of ['/rechnungen', '/rechnungen/vorschau']) {
await erwarte(pfad, { customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1', 'ws3'], memo: 'zweite' }, 409, 'Arbeitsblatt bereits abgerechnet (Entwurf).')
}
})
test('Falsche Feldtypen im Körper -> 400 mit Meldung, nichts bei Stripe', async () => {
const ab = await legeAuftragAn()
await erwarte('/rechnungen', { customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: 'ws1' }, 400, 'Die Arbeitsblätter müssen als Liste von Kennungen übergeben werden.')
await erwarte('/rechnungen', { customerId: 'k1', art: 'aufwand', projektId: 'p1', zeilentexte: ['x'] }, 400, 'Die Zeilentexte müssen ein Objekt mit Texten sein.')
await erwarte('/rechnungen', { customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung: 'viel' }, 400, 'Die Angaben zur Anzahlung müssen ein Objekt sein.')
await erwarte('/rechnungen', { customerId: 'k1', items: 'x' }, 400, 'Eine Rechnung braucht 1 bis 20 Positionen.')
await erwarte('/rechnungen', { customerId: 'k1', art: {}, items: POSTEN }, 400, 'Unbekannte Rechnungsart.')
assert.equal(schreibaufrufe().length, 0)
})
test('Rechte: ohne Anmeldung 401, Mitarbeiter ohne Admin-Label 403; GET /rechnungen trägt die neuen Felder', async () => {
for (const pfad of ['/rechnungen', '/rechnungen/vorschau']) {
assert.equal((await post(`${base}${pfad}`, { customerId: 'k1', items: POSTEN })).status, 401)
assert.equal((await post(`${base}${pfad}`, { customerId: 'k1', items: POSTEN }, staff)).status, 403)
}
assert.equal(st.invoices.size, 0)
await post(`${base}/rechnungen`, { customerId: 'k1', items: POSTEN, memo: 'frei' }, admin)
await post(`${base}/rechnungen`, { customerId: 'k1', art: 'festbetrag', projektId: 'p1', items: POSTEN }, admin)
const liste = (await rawGet(`${base}/rechnungen`, admin)).body.rechnungen
const felder = (r) => [r.art, r.projektId, r.projektName, r.auftragId, r.auftragNummer]
assert.deepEqual(liste.map(felder).sort(), [['festbetrag', 'p1', 'Website anelia-wolf.com', '', ''], ['frei', '', '', '', '']])
})
test('POST /rechnungen: Anzahlung ohne daysUntilDue nimmt das Zahlungsziel der AB, mit Angabe diese; freie Rechnung bleibt bei 14', async () => {
const ab = await legeAuftragAn({ zahlungszielTage: 30 })
const tage = () => st.calls.filter((c) => c.method === 'POST' && c.path === '/v1/invoices').at(-1).body.days_until_due
assert.equal((await post(`${base}/rechnungen`, { customerId: 'k1', art: 'anzahlung', auftragId: ab.id, projektId: 'p1', anzahlung: { prozent: 20 } }, admin)).status, 200)
assert.equal(tage(), '30')
assert.equal((await post(`${base}/rechnungen`, { customerId: 'k1', art: 'anzahlung', auftragId: ab.id, projektId: 'p1', anzahlung: { prozent: 10 }, daysUntilDue: 7 }, admin)).status, 200)
assert.equal(tage(), '7')
assert.equal((await post(`${base}/rechnungen`, { customerId: 'k1', items: POSTEN }, admin)).status, 200)
assert.equal(tage(), '14')
})