// test/auftraegeAuftragSicht.test.js import { test } from 'node:test' import assert from 'node:assert/strict' import { kundeSnapshot, fruehesterBeginn, zahlungsplanText, aufwandText, auftragFuerTeam, oeffentlicheAnsicht } from '../server/services/auftraege/auftragSicht.js' import { AGB_FASSUNG } from '../server/services/auftraege/konstanten.js' import { AB_TEXTE, BESTAETIGUNG_TEXT, FRUEHBEGINN_TEXT } from '../server/services/auftraege/texte.js' import { euroDe } from '../server/services/finance/format.js' const KUNDE = { name: 'Anna Beispiel', firma: '', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: '', email: 'anna@example.invalid', kundenart: 'privat', kundennummer: 'K-1001' } const festpreis = (felder = {}) => ({ id: 'ab1', nummer: 'AB-2026-0002', jahr: 2026, laufnummer: 2, status: 'versendet', kundenId: 'c1', kunde: KUNDE, projektId: 'p1', titel: 'Website Bäckerei', zeitquelle: ['t1'], art: 'festpreis', positionen: [{ beschreibung: 'Startseite', menge: 2, einzelpreisCents: 30000 }, { beschreibung: 'Kontaktformular', menge: 1, einzelpreisCents: 20000 }], stundensatzCents: null, schaetzungMinuten: null, summeCents: 80000, zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' }, zahlungszielTage: 14, auftragVom: '2026-10-05', auftragWeg: 'telefon', startAm: '2026-11-01', fertigBis: '', ersetztId: '', ersetztDurchId: '', texteFassung: '2026-10-06', agbFassung: '2026-09-30', pdfDatei: 'c1/ab1.pdf', pdfSha256: 'a'.repeat(64), gesendetAm: '2026-10-06T10:00:00.000Z', gesendetAn: 'anna@example.invalid', mailStatus: 'gesendet', mailFehler: '', tokenHash: 'b'.repeat(64), tokenGueltigBis: '2026-12-05T10:00:00.000Z', bestaetigtAm: null, bestaetigtUeber: '', bestaetigtPdfSha256: '', fruehbeginn: false, fruehbeginnFassung: '', storniertAm: null, widerrufenAm: null, erstelltVon: 'kenso@webklar.com', erstelltAm: '2026-10-05T09:00:00.000Z', geaendertAm: '2026-10-06T10:00:00.000Z', ...felder, }) const aufwand = (felder = {}) => festpreis({ art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000, schaetzungMinuten: 600, zahlungsplan: { art: 'monatlich', prozent: null, text: '' }, ...felder }) const LEER = { name: '', firma: '', strasse: '', plz: '', ort: '', land: '', email: '', kundenart: '', kundennummer: '' } test('kundeSnapshot übernimmt die Kundendaten getrimmt; eine unbekannte Kundenart wird leer; null und {} sind leer', () => { const doc = { $id: 'c1', name: ' Anna Beispiel ', companyName: '', street: 'Hauptstr. 1', postalCode: '67722', city: 'Winnweiler', country: '', email: 'anna@example.invalid', kundenart: 'privat', code: 'K-1001', phone: '123', customerStatus: 'active' } assert.deepEqual(kundeSnapshot(doc), KUNDE) assert.deepEqual(kundeSnapshot({ ...doc, companyName: ' Bäckerei Beispiel GmbH ', kundenart: 'unternehmen' }), { ...KUNDE, firma: 'Bäckerei Beispiel GmbH', kundenart: 'unternehmen' }) assert.equal(kundeSnapshot({ name: 'X', kundenart: 'firma' }).kundenart, '') assert.deepEqual(kundeSnapshot({}), LEER) assert.deepEqual(kundeSnapshot(null), LEER) }) test('kundeSnapshot kürzt lange Werte, so dass die Spalte kundeJson (2000 Zeichen) reicht', () => { const lang = 'x'.repeat(500) const snapshot = kundeSnapshot({ name: lang, companyName: lang, street: lang, postalCode: lang, city: lang, country: lang, email: lang, kundenart: 'privat', code: lang }) assert.ok(JSON.stringify(snapshot).length <= 2000) assert.equal(snapshot.name.length, 120) }) test('fruehesterBeginn: Tag nach Ablauf der Widerrufsfrist, nur für Privatpersonen ohne Frühbeginn', () => { const faelle = [ ['2026-10-06T10:00:00.000Z', '2026-10-21'], ['2026-10-06T22:30:00.000Z', '2026-10-22'], // in Berlin schon der 07.10. ['2026-10-10T08:00:00.000Z', '2026-10-27'], // Fristende fiele auf Samstag, 26.10. ist der Montag ['2026-10-17T08:00:00.000Z', '2026-11-03'], // 31.10. (Samstag, Feiertag) und 01.11. (Sonntag), Fristende Montag 02.11. ] for (const [gesendetAm, erwartet] of faelle) assert.equal(fruehesterBeginn(festpreis({ gesendetAm })), erwartet, gesendetAm) assert.equal(fruehesterBeginn(festpreis({ fruehbeginn: true })), null) for (const kundenart of ['unternehmen', '']) assert.equal(fruehesterBeginn(festpreis({ kunde: { ...KUNDE, kundenart } })), null, kundenart) assert.equal(fruehesterBeginn(festpreis({ kunde: null })), null) assert.equal(fruehesterBeginn(festpreis({ gesendetAm: null, status: 'entwurf' })), null) assert.equal(fruehesterBeginn(festpreis({ gesendetAm: 'kaputt' })), null) }) test('zahlungsplanText: Anzahlung nach Prozent gerundet, Rest als Differenz', () => { const text = zahlungsplanText(festpreis()) assert.equal(text.split(euroDe(40000)).length, 3, text) // Anzahlung und Rest je 400,00 € assert.match(text, /50\s?%/) assert.match(text, /\b14\b/) // Math.round(10001 × 33 / 100) = 3300, Rest 6701 const ungerade = zahlungsplanText(festpreis({ summeCents: 10001, zahlungsplan: { art: 'anzahlung', prozent: 33, text: '' } })) assert.ok(ungerade.includes(euroDe(3300)) && ungerade.includes(euroDe(6701)), ungerade) }) test('zahlungsplanText: nach Abnahme, monatlich, frei; Anzahlung ohne Summe fällt auf nach Abnahme zurück', () => { assert.match(zahlungsplanText(festpreis({ zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } })), /\b14\b/) assert.match(zahlungsplanText(aufwand({ zahlungszielTage: 7 })), /\b7\b/) assert.ok(zahlungsplanText(festpreis({ zahlungsplan: { art: 'frei', prozent: null, text: 'Zahlung in zwei Raten.' } })).includes('Zahlung in zwei Raten')) const ohneSumme = aufwand({ zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' } }) assert.equal(zahlungsplanText(ohneSumme), zahlungsplanText({ ...ohneSumme, zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } })) }) test('aufwandText: nur bei Aufwand, mit Stundensatz und Schätzung in Stunden', () => { const text = aufwandText(aufwand()) assert.ok(text.includes(euroDe(10000))) assert.match(text, /\b10\b/) assert.doesNotMatch(aufwandText(aufwand({ schaetzungMinuten: null })), /\b10\b/) assert.match(aufwandText(aufwand({ schaetzungMinuten: 135 })), /2,25/) assert.match(aufwandText(aufwand({ schaetzungMinuten: 90 })), /1,5\b/) assert.ok(aufwandText(aufwand({ stundensatzCents: null })).includes(euroDe(10000)), 'ohne Satz gilt der Standard') assert.equal(aufwandText(festpreis()), null) }) test('auftragFuerTeam: ohne Token-Hash, PDF-Pfad und Bestätigungs-Prüfsumme, mit den abgeleiteten Feldern', () => { const auftrag = festpreis() const kopie = structuredClone(auftrag) const sicht = auftragFuerTeam(auftrag) assert.deepEqual(auftrag, kopie, 'verändert die AB nicht') const entfernt = ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256'] for (const feld of entfernt) assert.equal(Object.hasOwn(sicht, feld), false, feld) assert.equal(sicht.pdfSha256, 'a'.repeat(64)) assert.equal(sicht.pdfVorhanden, true) assert.equal(sicht.fruehesterBeginn, '2026-10-21') assert.equal(sicht.zahlungsplanText, zahlungsplanText(auftrag)) assert.equal(sicht.aufwandText, null) assert.deepEqual( Object.keys(sicht).sort(), [...Object.keys(auftrag).filter((k) => !entfernt.includes(k)), 'pdfVorhanden', 'fruehesterBeginn', 'zahlungsplanText', 'aufwandText'].sort(), ) assert.equal(auftragFuerTeam(festpreis({ pdfDatei: '' })).pdfVorhanden, false) }) test('oeffentlicheAnsicht: genau die Felder aus V3, Beträge je Position, Berliner Datum, nichts Internes', () => { const ansicht = oeffentlicheAnsicht(festpreis()) assert.deepEqual( Object.keys(ansicht).sort(), ['agbUrl', 'aufwandText', 'art', 'bestaetigtAm', 'datum', 'fertigBis', 'fruehbeginn', 'fruehesterBeginn', 'kundenart', 'nummer', 'positionen', 'schaetzungMinuten', 'startAm', 'status', 'stundensatzCents', 'summeCents', 'texte', 'titel', 'zahlungsplanText'].sort(), ) assert.deepEqual(ansicht.positionen, [ { beschreibung: 'Startseite', menge: 2, einzelpreisCents: 30000, summeCents: 60000 }, { beschreibung: 'Kontaktformular', menge: 1, einzelpreisCents: 20000, summeCents: 20000 }, ]) assert.deepEqual( [ansicht.nummer, ansicht.datum, ansicht.titel, ansicht.art, ansicht.summeCents, ansicht.startAm, ansicht.kundenart, ansicht.status], ['AB-2026-0002', '2026-10-06', 'Website Bäckerei', 'festpreis', 80000, '2026-11-01', 'privat', 'versendet'], ) assert.equal(ansicht.fruehesterBeginn, '2026-10-21') assert.deepEqual(ansicht.texte, { bestaetigung: BESTAETIGUNG_TEXT, fruehbeginn: FRUEHBEGINN_TEXT, kleinunternehmer: AB_TEXTE.kleinunternehmer }) assert.equal(ansicht.texte.kleinunternehmer, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.') assert.equal(ansicht.agbUrl, '/rechtliches/agb-2026-09-30.pdf') const text = JSON.stringify(ansicht) for (const geheim of ['b'.repeat(64), 'c1/ab1.pdf', 'anna@example.invalid', 'Hauptstr. 1', 'kenso@webklar.com']) assert.ok(!text.includes(geheim), geheim) }) test('oeffentlicheAnsicht: Unternehmen ohne Frühbeginn-Text, Datum nach Berliner Zeit, AGB-Adresse ohne gespeicherte Fassung', () => { const firma = oeffentlicheAnsicht(festpreis({ kunde: { ...KUNDE, kundenart: 'unternehmen' }, gesendetAm: '2026-10-06T22:30:00.000Z', agbFassung: '' })) assert.equal(firma.texte.fruehbeginn, null) assert.equal(firma.texte.kleinunternehmer, AB_TEXTE.kleinunternehmer) assert.equal(firma.fruehesterBeginn, null) assert.equal(firma.datum, '2026-10-07') assert.equal(firma.agbUrl, `/rechtliches/agb-${AGB_FASSUNG}.pdf`) const aufwandAnsicht = oeffentlicheAnsicht(aufwand()) assert.deepEqual([aufwandAnsicht.positionen, aufwandAnsicht.stundensatzCents, aufwandAnsicht.schaetzungMinuten], [[], 10000, 600]) assert.equal(aufwandAnsicht.aufwandText, aufwandText(aufwand())) })