136 lines
9.6 KiB
JavaScript
136 lines
9.6 KiB
JavaScript
// 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()))
|
||
})
|