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' // Nachbau von Appwrite mit dem Schema und den Datumsfeldern der neuen Collections und dem Standard-Limit 25 (wie in den Tests von L0) 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 { baueRechnung } = await import('../server/services/auftraege/rechnungsbau.js') const { getProjektRechnung, updateProjektRechnung } = await import('../server/services/auftraege/projektRechnungenStore.js') const { getAuftrag } = await import('../server/services/auftraege/auftragStore.js') after(() => Promise.all([aw.close(), st.close()])) seedRechnungsDaten(aw) config.auftraege.mode = 'on' beforeEach(() => leereRechnungsDaten(aw, st)) const NBSP = String.fromCharCode(160) // geschütztes Leerzeichen U+00A0, wie euroDe es vor € setzt const SCHALTER_AUS = 'Auftragsbestätigungen sind für diesen Kunden noch nicht freigeschaltet.' const POSTEN = [{ description: 'Domain', quantity: 1, unitAmountCents: 1200 }] const wirft = (promise, status, message) => assert.rejects(promise, (err) => { assert.equal(err.status, status, `Status ${err.status}: ${err.message}`) assert.equal(err.message, message) return true }) test('baueRechnung frei: freie Positionen, nur Kopf-Metadaten, keine Zusatzfelder, kein Projektdatensatz', async () => { const e = await baueRechnung({ customerId: 'k1', items: POSTEN, createdBy: 'kenso@webklar.com' }) assert.equal(e.art, 'frei') assert.equal(e.kunde.$id, 'k1') assert.equal(e.projekt, null) assert.equal(e.auftrag, null) assert.deepEqual(e.positionen, [{ schluessel: 'pos-1', beschreibung: 'Domain', menge: 1, einzelpreisCents: 1200, betragCents: 1200 }]) assert.equal(e.summeCents, 1200) assert.equal(e.leistungszeitraum, '') assert.deepEqual(e.customFields, []) assert.deepEqual(e.metadata, { webklarCustomerId: 'k1', quelle: 'woms', erstelltVon: 'kenso@webklar.com', art: 'frei' }) assert.equal(e.projektRechnung, null) assert.equal((await baueRechnung({ customerId: 'k1', items: POSTEN })).metadata.erstelltVon, '') }) test('baueRechnung frei: Projekt, Auftrag, Arbeitsblätter und Anzahlung der Eingabe bleiben ohne Wirkung', async () => { const e = await baueRechnung({ customerId: 'k1', art: 'frei', items: POSTEN, projektId: 'p1', auftragId: 'a1', arbeitsblattIds: ['ws1'], anzahlung: { prozent: 10 } }) assert.equal(e.projekt, null) assert.equal(e.auftrag, null) assert.equal(e.projektRechnung, null) assert.deepEqual(e.customFields, []) assert.deepEqual(Object.keys(e.metadata).sort(), ['art', 'erstelltVon', 'quelle', 'webklarCustomerId']) }) test('baueRechnung: Leistungszeitraum ist optional, wird getrimmt und hat höchstens 140 Zeichen', async () => { const e = await baueRechnung({ customerId: 'k1', items: POSTEN, leistungszeitraum: ' Oktober 2026 ' }) assert.equal(e.leistungszeitraum, 'Oktober 2026') assert.deepEqual(e.customFields, [{ name: 'Leistungszeitraum', value: 'Oktober 2026' }]) assert.equal((await baueRechnung({ customerId: 'k1', items: POSTEN, leistungszeitraum: 'z'.repeat(140) })).leistungszeitraum.length, 140) for (const leer of [undefined, null, '', ' ']) assert.deepEqual((await baueRechnung({ customerId: 'k1', items: POSTEN, leistungszeitraum: leer })).customFields, []) const MELDUNG = 'Der Leistungszeitraum muss ein Text mit höchstens 140 Zeichen sein.' for (const leistungszeitraum of ['z'.repeat(141), 5, {}]) await wirft(baueRechnung({ customerId: 'k1', items: POSTEN, leistungszeitraum }), 400, MELDUNG) }) test('baueRechnung: unbekannte Art, unbekannter oder nicht fester Kunde, ungültige Positionen -> 400 ohne Stripe-Aufruf', async () => { for (const art of ['quatsch', 'FREI', 5, {}]) await wirft(baueRechnung({ customerId: 'k1', art, items: POSTEN }), 400, 'Unbekannte Rechnungsart.') for (const customerId of [undefined, '', 'gibtEsNicht', '../x', 5]) await wirft(baueRechnung({ customerId, items: POSTEN }), 400, 'Kunde nicht gefunden.') await wirft(baueRechnung({ customerId: 'lead1', items: POSTEN }), 400, 'Rechnungen nur für feste Kunden.') await wirft(baueRechnung({ customerId: 'k1' }), 400, 'Eine Rechnung braucht 1 bis 20 Positionen.') assert.equal(st.calls.length, 0) }) test('baueRechnung festbetrag: Projekt, Zusatzfeld Projekt, Metadaten und Projektdatensatz', async () => { const items = [{ description: 'Erstellung der Website anelia-wolf.com, Pauschalpreis laut Absprache', quantity: 1, unitAmountCents: 50000 }] const e = await baueRechnung({ customerId: 'k1', art: 'festbetrag', projektId: 'p1', items, createdBy: 'kenso@webklar.com' }) assert.deepEqual(e.projekt, { id: 'p1', name: 'Website anelia-wolf.com', ticketId: 't1' }) assert.equal(e.auftrag, null) assert.equal(e.summeCents, 50000) assert.deepEqual(e.customFields, [{ name: 'Projekt', value: 'Website anelia-wolf.com' }]) assert.deepEqual(e.metadata, { webklarCustomerId: 'k1', quelle: 'woms', erstelltVon: 'kenso@webklar.com', art: 'festbetrag', projektId: 'p1', projektName: 'Website anelia-wolf.com' }) const { erstelltAm, ...rest } = e.projektRechnung assert.match(erstelltAm, /^\d{4}-\d{2}-\d{2}T/) assert.deepEqual(rest, { kundenId: 'k1', projektId: 'p1', auftragId: '', art: 'festbetrag', betragCents: 50000, arbeitsblattIds: [], rechnungsnummer: '', aktiv: true, erstelltVon: 'kenso@webklar.com' }) // eine genannte auftragId bleibt bei dieser Art ohne Wirkung assert.equal((await baueRechnung({ customerId: 'k1', art: 'festbetrag', projektId: 'p1', auftragId: 'a1', items })).auftrag, null) }) test('baueRechnung festbetrag: lange Projektnamen werden auf 140 Zeichen gekürzt, leere ersetzt', async () => { aw.seed('websiteProjects', { $id: 'plang', customerId: 'k1', projectName: 'P'.repeat(200), ticketId: '' }) aw.seed('websiteProjects', { $id: 'psub', customerId: 'k1', projectName: '', subdomain: 'wolf-test', ticketId: '' }) aw.seed('websiteProjects', { $id: 'pleer', customerId: 'k1', ticketId: '' }) const e = await baueRechnung({ customerId: 'k1', art: 'festbetrag', projektId: 'plang', items: POSTEN }) assert.equal(e.customFields[0].value.length, 140) assert.ok(e.customFields[0].value.endsWith('P…')) assert.equal(e.metadata.projektName, e.customFields[0].value) const name = async (projektId) => (await baueRechnung({ customerId: 'k1', art: 'festbetrag', projektId, items: POSTEN })).customFields[0].value assert.equal(await name('psub'), 'wolf-test') assert.equal(await name('pleer'), 'Projekt') }) test('baueRechnung festbetrag: Projekt ist Pflicht, fremde und unbekannte Projekte sind 404 (kein Unterschied), ungültige Positionen 400', async () => { const mit = (projektId, items = POSTEN) => baueRechnung({ customerId: 'k1', art: 'festbetrag', projektId, items }) for (const projektId of [undefined, '', null, 5]) await wirft(mit(projektId), 400, 'Für diese Rechnungsart ist ein Projekt nötig.') for (const projektId of ['gibtEsNicht', 'p2', '../x']) await wirft(mit(projektId), 404, 'Projekt nicht gefunden.') await wirft(mit('p1', []), 400, 'Eine Rechnung braucht 1 bis 20 Positionen.') assert.equal(st.calls.length, 0) })