import { test } from 'node:test' import assert from 'node:assert/strict' import { useTestEnv } from './support/testEnv.js' import { festpreisEingabe, aufwandEingabe, wirftFehler, MELDUNG } from './helpers/auftraegeAb.js' // Rein: kein Appwrite-Zugriff. Die Umgebung zeigt trotzdem auf tote lokale Adressen, damit nie ein echtes System erreicht wird. useTestEnv({ appwriteUrl: 'http://127.0.0.1:1/v1', stripeUrl: 'http://127.0.0.1:1' }) const { pruefeEingabe } = await import('../server/services/auftraege/auftraege.js') const { AuftragError } = await import('../server/services/auftraege/fehler.js') const pos = (patch = {}) => ({ beschreibung: 'Webdesign', menge: 1, einzelpreisCents: 100, ...patch }) const neunPositionen = () => Array.from({ length: 9 }, () => pos({ einzelpreisCents: 10_000_000 })) const E = { art: 'Art: Bitte Festpreis oder Nach Aufwand wählen.', beschreibung: (n = 1) => `Position ${n}: Die Beschreibung braucht 1 bis 200 Zeichen.`, menge: 'Position 1: Die Menge muss eine ganze Zahl von 1 bis 999 sein.', preis: (n = 1) => `Position ${n}: Der Einzelpreis muss zwischen 0,01 € und 100.000,00 € liegen.`, summe: 'Die Auftragssumme ist zu hoch (höchstens 999.999,99 €).', plan: 'Zahlungsplan: Bitte eine Zahlungsart wählen.', prozent: 'Zahlungsplan: Die Anzahlung muss eine ganze Zahl von 1 bis 99 Prozent sein.', frei: 'Zahlungsplan: Der freie Text braucht 1 bis 500 Zeichen.', ziel: 'Zahlungsziel: Das Zahlungsziel muss eine ganze Zahl von 0 bis 60 Tagen sein.', datum: 'Auftrag vom: Bitte ein gültiges Datum angeben (JJJJ-MM-TT).', titel: 'Titel: Höchstens 200 Zeichen.', zeitquelle: 'Zeitquelle: Eine Liste von Ticket-Kennungen erwartet.', stundensatz: 'Stundensatz: Der Stundensatz muss zwischen 0,01 € und 100.000,00 € liegen.', schaetzung: 'Schätzung: Die Schätzung muss ein Vielfaches von 15 Minuten sein (15 bis 99.960).', } test('Festpreis: bereinigte Felder – Texte getrimmt, Summe berechnet, Aufwandsfelder leer, Vorgaben gesetzt', () => { const felder = pruefeEingabe( festpreisEingabe({ titel: ' Neue Website ', positionen: [{ beschreibung: ' Webdesign ', menge: 2, einzelpreisCents: 30000 }], zahlungsplan: { art: 'nach_abnahme' }, zahlungszielTage: undefined, stundensatzCents: 12345, // gehört nicht zu einem Festpreis und fällt weg schaetzungMinuten: 30, }), ) assert.deepEqual(felder, { projektId: 'p_projekt01', titel: 'Neue Website', kundenart: '', art: 'festpreis', positionen: [{ beschreibung: 'Webdesign', menge: 2, einzelpreisCents: 30000 }], stundensatzCents: null, schaetzungMinuten: null, summeCents: 60000, zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' }, zahlungszielTage: 14, auftragVom: '2026-10-05', auftragWeg: 'telefon', startAm: '', fertigBis: '', zeitquelle: [], }) }) test('Aufwand: Stundensatz-Vorgabe 100 €, Schätzung optional, Positionen fallen weg, Summe leer', () => { const felder = pruefeEingabe(aufwandEingabe({ stundensatzCents: undefined, schaetzungMinuten: null, positionen: festpreisEingabe().positionen })) assert.deepEqual(felder, { projektId: 'p_projekt01', titel: '', kundenart: '', art: 'aufwand', positionen: [], stundensatzCents: 10000, schaetzungMinuten: null, summeCents: null, zahlungsplan: { art: 'monatlich', prozent: null, text: '' }, zahlungszielTage: 14, auftragVom: '2026-10-05', auftragWeg: 'email', startAm: '', fertigBis: '', zeitquelle: [], }) }) test('Zahlungsplan: Anzahlung nimmt 50 % vor, frei trägt den Text, die anderen Arten verwerfen prozent und text', () => { const plan = (zahlungsplan, basis = festpreisEingabe) => pruefeEingabe(basis({ zahlungsplan })).zahlungsplan assert.deepEqual(plan({ art: 'anzahlung' }), { art: 'anzahlung', prozent: 50, text: '' }) assert.deepEqual(plan({ art: 'anzahlung', prozent: 30, text: 'ignoriert' }), { art: 'anzahlung', prozent: 30, text: '' }) assert.deepEqual(plan({ art: 'frei', prozent: 40, text: ' Zahlbar in zwei Raten. ' }), { art: 'frei', prozent: null, text: 'Zahlbar in zwei Raten.' }) assert.deepEqual(plan({ art: 'monatlich', text: 'x' }, aufwandEingabe), { art: 'monatlich', prozent: null, text: '' }) assert.deepEqual(plan({ art: 'nach_abnahme', prozent: 10 }, aufwandEingabe), { art: 'nach_abnahme', prozent: null, text: '' }) }) test('Grenzwerte, die noch gelten: 20 Positionen, Summe genau 999.999,99 €, Zahlungsziel 0 und 60, Schätzung 15 und 99.960 Minuten', () => { const lang = (cents = 100, menge = 1) => ({ beschreibung: 'x'.repeat(200), menge, einzelpreisCents: cents }) assert.equal(pruefeEingabe(festpreisEingabe({ positionen: Array.from({ length: 20 }, () => lang()) })).positionen.length, 20) assert.equal(pruefeEingabe(festpreisEingabe({ positionen: [...neunPositionen(), pos({ einzelpreisCents: 9_999_999 })] })).summeCents, 99_999_999) assert.equal(pruefeEingabe(festpreisEingabe({ positionen: [lang(100, 999)] })).summeCents, 99_900) assert.equal(pruefeEingabe(festpreisEingabe({ zahlungszielTage: 0 })).zahlungszielTage, 0) assert.equal(pruefeEingabe(festpreisEingabe({ zahlungszielTage: 60 })).zahlungszielTage, 60) assert.equal(pruefeEingabe(aufwandEingabe({ schaetzungMinuten: 15 })).schaetzungMinuten, 15) assert.equal(pruefeEingabe(aufwandEingabe({ schaetzungMinuten: 99_960 })).schaetzungMinuten, 99_960) for (const keine of [0, '', null]) assert.equal(pruefeEingabe(aufwandEingabe({ schaetzungMinuten: keine })).schaetzungMinuten, null) assert.equal(pruefeEingabe(aufwandEingabe({ stundensatzCents: 1 })).stundensatzCents, 1) assert.equal(pruefeEingabe(aufwandEingabe({ stundensatzCents: 10_000_000 })).stundensatzCents, 10_000_000) }) const FESTPREIS_FEHLER = [ ['Art fehlt', { art: undefined }, E.art], ['Art unbekannt', { art: 'pauschal' }, E.art], ['keine Positionen', { positionen: [] }, MELDUNG.positionen], ['Positionen fehlen', { positionen: undefined }, MELDUNG.positionen], ['21 Positionen', { positionen: Array.from({ length: 21 }, () => pos()) }, MELDUNG.positionen], ['Position ist kein Objekt', { positionen: ['x'] }, E.beschreibung()], ['Beschreibung leer', { positionen: [pos({ beschreibung: ' ' })] }, E.beschreibung()], ['Beschreibung 201 Zeichen (zweite Position)', { positionen: [pos(), pos({ beschreibung: 'x'.repeat(201) })] }, E.beschreibung(2)], ['Menge 0', { positionen: [pos({ menge: 0 })] }, E.menge], ['Menge 1000', { positionen: [pos({ menge: 1000 })] }, E.menge], ['Menge 1,5', { positionen: [pos({ menge: 1.5 })] }, E.menge], ['Menge als Text', { positionen: [pos({ menge: '2' })] }, E.menge], ['Einzelpreis 0 (zweite Position)', { positionen: [pos(), pos({ einzelpreisCents: 0 })] }, E.preis(2)], ['Einzelpreis über 100.000 €', { positionen: [pos({ einzelpreisCents: 10_000_001 })] }, E.preis()], ['Einzelpreis mit Nachkommastelle', { positionen: [pos({ einzelpreisCents: 99.5 })] }, E.preis()], ['Summe zu hoch', { positionen: [pos({ menge: 11, einzelpreisCents: 10_000_000 })] }, E.summe], ['Summe ein Cent über dem Höchstwert', { positionen: [...neunPositionen(), pos({ einzelpreisCents: 10_000_000 })] }, E.summe], ['Zahlungsplan fehlt', { zahlungsplan: undefined }, E.plan], ['Zahlungsart unbekannt', { zahlungsplan: { art: 'bar' } }, E.plan], ['Zahlungsplan kein Objekt', { zahlungsplan: 'anzahlung' }, E.plan], ['monatlich bei Festpreis', { zahlungsplan: { art: 'monatlich' } }, 'Zahlungsplan: „Monatlich nach Aufwand“ geht nur bei einem Auftrag nach Aufwand.'], ['Anzahlung 0 %', { zahlungsplan: { art: 'anzahlung', prozent: 0 } }, E.prozent], ['Anzahlung 100 %', { zahlungsplan: { art: 'anzahlung', prozent: 100 } }, E.prozent], ['Anzahlung 12,5 %', { zahlungsplan: { art: 'anzahlung', prozent: 12.5 } }, E.prozent], ['Anzahlung als Text', { zahlungsplan: { art: 'anzahlung', prozent: '50' } }, E.prozent], ['freier Text leer', { zahlungsplan: { art: 'frei', text: ' ' } }, E.frei], ['freier Text 501 Zeichen', { zahlungsplan: { art: 'frei', text: 'x'.repeat(501) } }, E.frei], ['Zahlungsziel -1', { zahlungszielTage: -1 }, E.ziel], ['Zahlungsziel 61', { zahlungszielTage: 61 }, E.ziel], ['Zahlungsziel 14,5', { zahlungszielTage: 14.5 }, E.ziel], ['Zahlungsziel als Text', { zahlungszielTage: '14' }, E.ziel], ['Auftragsdatum fehlt', { auftragVom: undefined }, E.datum], ['30. Februar', { auftragVom: '2026-02-30' }, E.datum], ['deutsches Datumsformat', { auftragVom: '05.10.2026' }, E.datum], ['Datum als Zahl', { auftragVom: 20261005 }, E.datum], ['Weg fehlt', { auftragWeg: undefined }, 'Weg: Bitte den Weg der Absprache wählen.'], ['Weg unbekannt', { auftragWeg: 'brieftaube' }, 'Weg: Bitte den Weg der Absprache wählen.'], ['Start kein Datum', { startAm: '2026-13-01' }, 'Start: Bitte ein gültiges Datum angeben (JJJJ-MM-TT) oder leer lassen.'], ['Fertigstellung kein Datum', { fertigBis: 'bald' }, 'Fertigstellung: Bitte ein gültiges Datum angeben (JJJJ-MM-TT) oder leer lassen.'], ['Titel 201 Zeichen', { titel: 'x'.repeat(201) }, E.titel], ['Titel keine Zeichenkette', { titel: 5 }, E.titel], ['Projekt-Kennung mit ../', { projektId: '../x' }, 'Projekt: Ungültige Projekt-Kennung.'], ['Kundenart unbekannt', { kundenart: 'firma' }, 'Kundenart: Bitte Unternehmen oder Privatperson wählen.'], ['Zeitquelle keine Liste', { zeitquelle: 't_1' }, E.zeitquelle], ['11 Tickets', { zeitquelle: Array.from({ length: 11 }, (_, i) => `t_${i}`) }, 'Zeitquelle: Höchstens 10 Tickets.'], ['Ticket-Kennung mit ../', { zeitquelle: ['t_ok', '../x'] }, 'Zeitquelle: Ungültige Ticket-Kennung.'], ] const AUFWAND_FEHLER = [ ['Stundensatz 0', { stundensatzCents: 0 }, E.stundensatz], ['Stundensatz über 100.000 €', { stundensatzCents: 10_000_001 }, E.stundensatz], ['Stundensatz mit Nachkommastelle', { stundensatzCents: 100.5 }, E.stundensatz], ['Stundensatz als Text', { stundensatzCents: '100' }, E.stundensatz], ['Schätzung 10 Minuten', { schaetzungMinuten: 10 }, E.schaetzung], ['Schätzung negativ', { schaetzungMinuten: -15 }, E.schaetzung], ['Schätzung über dem Höchstwert', { schaetzungMinuten: 99_975 }, E.schaetzung], ['Schätzung als Text', { schaetzungMinuten: '15' }, E.schaetzung], ['Anzahlung bei Aufwand', { zahlungsplan: { art: 'anzahlung', prozent: 50 } }, 'Zahlungsplan: Eine Anzahlung geht nur bei einem Festpreis.'], ] test('Ablehnungen beim Festpreis: jede Eingabe scheitert mit genau ihrer Meldung (400)', () => { for (const [name, patch, meldung] of FESTPREIS_FEHLER) wirftFehler(() => pruefeEingabe(festpreisEingabe(patch)), 400, meldung, name) }) test('Ablehnungen bei Aufwand: Stundensatz, Schätzung und Anzahlung', () => { for (const [name, patch, meldung] of AUFWAND_FEHLER) wirftFehler(() => pruefeEingabe(aufwandEingabe(patch)), 400, meldung, name) }) test('Eingaben, die kein einfaches Objekt sind, ergeben „Ungültige Eingabe.“ als AuftragError', () => { for (const eingabe of [null, undefined, 'x', 42, [], [festpreisEingabe()]]) { for (const teilweise of [false, true]) { assert.throws(() => pruefeEingabe(eingabe, { teilweise }), (err) => err instanceof AuftragError && err.status === 400 && err.message === 'Ungültige Eingabe.') } } }) test('Unbekannte und geschützte Felder landen nie in den bereinigten Feldern', () => { const sauber = pruefeEingabe(festpreisEingabe()) const mitMuell = pruefeEingabe({ ...festpreisEingabe(), status: 'bestaetigt', nummer: 'AB-2026-0001', jahr: 2026, tokenHash: 'x', ersetztId: 'a_alt', erstelltVon: 'boese@example.invalid', pdfDatei: 'c/a.pdf', kunde: { name: 'x' }, id: 'a_fremd', }) assert.deepEqual(mitMuell, sauber) }) test('teilweise: nur übergebene Felder werden geprüft und bereinigt, nichts wird ergänzt', () => { assert.deepEqual(pruefeEingabe({}, { teilweise: true }), {}) assert.deepEqual(pruefeEingabe({ kundenId: 'c_x', status: 'bestaetigt' }, { teilweise: true }), {}) assert.deepEqual(pruefeEingabe({ titel: ' Neu ', zeitquelle: ['t_1', 't_1', 't_2'] }, { teilweise: true }), { titel: 'Neu', zeitquelle: ['t_1', 't_2'] }) assert.deepEqual(pruefeEingabe({ art: 'aufwand' }, { teilweise: true }), { art: 'aufwand' }) assert.deepEqual(pruefeEingabe({ zahlungsplan: { art: 'anzahlung' } }, { teilweise: true }), { zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' } }) assert.deepEqual( pruefeEingabe({ positionen: [{ beschreibung: ' A ', menge: 1, einzelpreisCents: 5 }] }, { teilweise: true }), { positionen: [{ beschreibung: 'A', menge: 1, einzelpreisCents: 5 }] }, ) wirftFehler(() => pruefeEingabe({ zahlungszielTage: 99 }, { teilweise: true }), 400, E.ziel) wirftFehler(() => pruefeEingabe({ auftragVom: null }, { teilweise: true }), 400, E.datum) })