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') })