Files
tickte-system/tests/unit/auftragView.test.mjs
T

401 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const {
AUFTRAG_LIMITS, STUNDENSATZ_STANDARD_CENTS, AUFTRAG_WEGE, SCHRITTE, zahlungsplanOptionen,
parseStunden, stundenText, parseProzent, parseTage, heuteBerlin,
neuePosition, neuesFormular, auftragZuFormular, waehleProjekt, wechselArt, formularSummeCents,
pruefeSchritt, formularZuNutzlast, zahlungsplanVorschau, sendeGate, zeitquelleOptionen, schalteZeitquelle,
kundenartWirksam, VOR_ORT_HINWEIS, vorOrtHinweis, auftragHinweise,
} = await loadModule('src/lib/auftragView.js')
const KONTEN = [
{ projektId: 'p1', name: 'Website Praxis Müller', tickets: [{ id: 't1', woid: '100131' }] },
{ projektId: 'p2', name: 'Shop Relaunch', tickets: [{ id: 't2', woid: '100140' }, { id: 't3', woid: '100141' }] },
]
const KUNDE = { $id: 'c1', name: 'Praxis Müller', email: 'info@praxis.test', street: 'Hauptstr. 1', postalCode: '67722', city: 'Winnweiler', kundenart: '', customerStatus: 'customer' }
const festpreisForm = (over = {}) => ({
...neuesFormular({ heute: '2026-10-06' }),
titel: 'Website', kundenart: 'privat', auftragWeg: 'telefon',
positionen: [{ uid: 1, description: 'Startseite', quantity: '1', price: '800,00' }],
...over,
})
const aufwandForm = (over = {}) => festpreisForm({ art: 'aufwand', positionen: [], ...over })
const OK = { ok: true, fehler: {}, erstes: '' }
test('Konstanten gleichen dem Vertrag V2 und der Spec (Limits, Wege, Zahlungspläne, Schritte)', () => {
assert.deepEqual(AUFTRAG_LIMITS, {
maxPositionen: 20, maxBeschreibung: 200, maxMenge: 999, maxEinzelpreisCents: 10_000_000,
maxSummeCents: 99_999_999, maxTitel: 200, maxZahlungsplanText: 500, maxZahlungszielTage: 60,
standardZahlungszielTage: 14, standardAnzahlungProzent: 50, maxZeitquellen: 10,
maxSchaetzungMinuten: 99_960, maxLeistungszeitraum: 140,
})
assert.equal(STUNDENSATZ_STANDARD_CENTS, 10000)
assert.deepEqual(AUFTRAG_WEGE, [
{ value: 'muendlich', label: 'Mündlich' }, { value: 'telefon', label: 'Telefonisch' },
{ value: 'email', label: 'Per E-Mail' }, { value: 'vor_ort', label: 'Vor Ort' }, { value: 'chat', label: 'Im Chat' },
])
assert.deepEqual(zahlungsplanOptionen('festpreis'), [
{ value: 'nach_abnahme', label: 'Alles nach Abnahme' },
{ value: 'anzahlung', label: 'Anzahlung bei Auftrag, Rest nach Abnahme' },
{ value: 'frei', label: 'Frei (eigener Text)' },
])
assert.deepEqual(zahlungsplanOptionen('aufwand'), [
{ value: 'nach_abnahme', label: 'Alles nach Abnahme' },
{ value: 'monatlich', label: 'Monatlich nach Aufwand' },
{ value: 'frei', label: 'Frei (eigener Text)' },
])
assert.deepEqual(SCHRITTE, [
{ id: 1, label: 'Projekt' }, { id: 2, label: 'Auftrag' }, { id: 3, label: 'Zahlung und Termine' }, { id: 4, label: 'Vorschau' },
])
})
test('parseStunden: Stunden mit Komma in 15-Minuten-Schritten werden Minuten', () => {
const gut = { '10': 600, '10,25': 615, '10,5': 630, '10,50': 630, '10,75': 645, '0,25': 15, '1': 60, '12,5 h': 750, '2 Std.': 120, ' 8 ': 480, '1666': 99960 }
for (const [text, minuten] of Object.entries(gut)) assert.equal(parseStunden(text), minuten, text)
for (const schlecht of ['', '0', '0,00', '10,3', '10,30', '10,1', '10.5', '-1', 'abc', '1666,25', '1667', '10,255', '1e3', ',5', '10 Minuten', null, undefined]) {
assert.equal(parseStunden(schlecht), null, String(schlecht))
}
})
test('stundenText: Minuten als Stunden mit Komma, ohne überflüssige Nullen', () => {
assert.deepEqual([15, 30, 45, 60, 75, 120, 135, 150, 600, 99960].map(stundenText), ['0,25', '0,5', '0,75', '1', '1,25', '2', '2,25', '2,5', '10', '1666'])
assert.equal(stundenText(null), '–')
assert.equal(stundenText('x'), '–')
})
test('parseProzent und parseTage', () => {
assert.deepEqual(['50', ' 30 % ', '1', '99'].map(parseProzent), [50, 30, 1, 99])
for (const schlecht of ['', '0', '100', '-5', '33,3', '5a', 'x', null]) assert.equal(parseProzent(schlecht), null, String(schlecht))
assert.deepEqual(['14', '0', '60', ' 7 '].map(parseTage), [14, 0, 60, 7])
for (const schlecht of ['', '61', '-1', '1,5', 'x', '1000', null]) assert.equal(parseTage(schlecht), null, String(schlecht))
})
test('heuteBerlin: Berliner Kalendertag, nicht der UTC-Tag', () => {
assert.equal(heuteBerlin(Date.parse('2026-10-05T22:30:00.000Z')), '2026-10-06')
assert.equal(heuteBerlin(Date.parse('2026-10-05T21:59:00.000Z')), '2026-10-05')
assert.match(heuteBerlin(), /^\d{4}-\d{2}-\d{2}$/)
})
test('neuesFormular: Vorgaben; ein Projekt füllt Titel und Zeitquelle', () => {
const f = neuesFormular({ projektId: 'p2', konten: KONTEN, heute: '2026-10-06' })
const { positionen, ...rest } = f
assert.equal(positionen.length, 1)
const { uid, ...zeile } = positionen[0]
assert.equal(typeof uid, 'number')
assert.deepEqual(zeile, { description: '', quantity: '1', price: '' })
assert.deepEqual(rest, {
projektId: 'p2', titel: 'Shop Relaunch', titelGeaendert: false, kundenart: '', art: 'festpreis',
auftragVom: '2026-10-06', auftragWeg: '',
stundensatz: '100,00', schaetzung: '',
zahlungsart: 'nach_abnahme', prozent: '50', zahlungstext: '', zahlungsziel: '14',
startAm: '', fertigBis: '', zeitquelle: ['t2', 't3'], zeitquelleGeaendert: false,
})
assert.deepEqual(neuesFormular({ konten: KONTEN, heute: '2026-10-06' }).zeitquelle, [])
assert.equal(neuesFormular({ projektId: 'gibtsnicht', konten: KONTEN, heute: '2026-10-06' }).projektId, '')
assert.equal(neuesFormular({ projektId: 'p1', heute: '2026-10-06' }).projektId, '')
assert.notEqual(neuePosition().uid, neuePosition().uid)
})
test('auftragZuFormular: Entwurf vom Server wird zum Formular (Euro- und Stundentexte)', () => {
const entwurf = {
id: 'a1', status: 'entwurf', projektId: 'p1', titel: 'Website', art: 'festpreis',
positionen: [{ beschreibung: 'Startseite', menge: 2, einzelpreisCents: 40000 }],
stundensatzCents: null, schaetzungMinuten: null, summeCents: 80000,
zahlungsplan: { art: 'anzahlung', prozent: 30, text: '' }, zahlungszielTage: 21,
auftragVom: '2026-10-05', auftragWeg: 'email', startAm: '2026-11-01', fertigBis: '', zeitquelle: ['t1'],
}
const f = auftragZuFormular(entwurf)
assert.deepEqual(f.positionen.map(({ uid, ...p }) => p), [{ description: 'Startseite', quantity: '2', price: '400,00' }])
const { positionen, ...rest } = f
assert.deepEqual(rest, {
projektId: 'p1', titel: 'Website', titelGeaendert: true, kundenart: '', art: 'festpreis',
auftragVom: '2026-10-05', auftragWeg: 'email',
stundensatz: '100,00', schaetzung: '',
zahlungsart: 'anzahlung', prozent: '30', zahlungstext: '', zahlungsziel: '21',
startAm: '2026-11-01', fertigBis: '', zeitquelle: ['t1'], zeitquelleGeaendert: true,
})
const aufwand = auftragZuFormular({
...entwurf, art: 'aufwand', positionen: [], stundensatzCents: 9550, schaetzungMinuten: 750, summeCents: null,
zahlungsplan: { art: 'frei', prozent: null, text: 'Nach Absprache' },
})
assert.equal(aufwand.stundensatz, '95,50')
assert.equal(aufwand.schaetzung, '12,5')
assert.equal(aufwand.positionen.length, 1) // eine leere Zeile für den Wechsel auf Festpreis
assert.deepEqual([aufwand.zahlungsart, aufwand.prozent, aufwand.zahlungstext], ['frei', '50', 'Nach Absprache'])
})
test('waehleProjekt: Titel und Zeitquelle folgen dem Projekt, bis sie von Hand geändert wurden', () => {
const f0 = neuesFormular({ konten: KONTEN, heute: '2026-10-06' })
const f1 = waehleProjekt(f0, 'p1', KONTEN)
assert.deepEqual([f1.projektId, f1.titel, f1.zeitquelle], ['p1', 'Website Praxis Müller', ['t1']])
const f2 = waehleProjekt(f1, 'p2', KONTEN)
assert.deepEqual([f2.titel, f2.zeitquelle], ['Shop Relaunch', ['t2', 't3']])
const f3 = waehleProjekt(f2, '', KONTEN)
assert.deepEqual([f3.projektId, f3.titel, f3.zeitquelle], ['', '', []])
assert.equal(waehleProjekt(f1, 'gibtsnicht', KONTEN).projektId, '')
const hand = { ...f1, titel: 'Eigener Titel', titelGeaendert: true, zeitquelle: ['t7'], zeitquelleGeaendert: true }
const f4 = waehleProjekt(hand, 'p2', KONTEN)
assert.deepEqual([f4.projektId, f4.titel, f4.zeitquelle], ['p2', 'Eigener Titel', ['t7']])
assert.equal(f0.projektId, '') // die Eingabe bleibt unverändert
})
test('wechselArt: ein unpassender Zahlungsplan wird zurückgesetzt', () => {
const f = { ...neuesFormular({ heute: '2026-10-06' }), zahlungsart: 'anzahlung' }
assert.equal(wechselArt(f, 'aufwand').art, 'aufwand')
assert.equal(wechselArt(f, 'aufwand').zahlungsart, 'nach_abnahme')
assert.equal(wechselArt(f, 'festpreis').zahlungsart, 'anzahlung')
assert.equal(wechselArt({ ...f, art: 'aufwand', zahlungsart: 'monatlich' }, 'festpreis').zahlungsart, 'nach_abnahme')
assert.equal(wechselArt({ ...f, zahlungsart: 'frei' }, 'aufwand').zahlungsart, 'frei')
assert.equal(wechselArt(f, 'quatsch'), f)
})
test('formularSummeCents: Festpreis summiert gültige Positionen, Aufwand hat keine Summe', () => {
const f = festpreisForm({
positionen: [
{ uid: 1, description: 'a', quantity: '2', price: '100,00' },
{ uid: 2, description: 'b', quantity: '1', price: '50' },
{ uid: 3, description: 'c', quantity: 'x', price: '9,99' },
],
})
assert.equal(formularSummeCents(f), 25000)
assert.equal(formularSummeCents(festpreisForm({ positionen: [] })), 0)
assert.equal(formularSummeCents(aufwandForm()), null)
})
test('pruefeSchritt 1: Titel ist Pflicht, die Kundenart nur, wenn sie am Kunden fehlt', () => {
const f = neuesFormular({ heute: '2026-10-06' })
const r = pruefeSchritt(1, f, { kunde: KUNDE })
assert.equal(r.ok, false)
assert.deepEqual(r.fehler, { titel: 'Bitte einen Titel eingeben (höchstens 200 Zeichen).', kundenart: 'Bitte die Kundenart wählen.' })
assert.equal(r.erstes, 'titel')
assert.deepEqual(pruefeSchritt(1, { ...f, titel: ' Website ', kundenart: 'privat' }, { kunde: KUNDE }), OK)
assert.deepEqual(pruefeSchritt(1, { ...f, titel: 'x' }, { kunde: { ...KUNDE, kundenart: 'privat' } }), OK)
assert.equal(pruefeSchritt(1, { ...f, titel: 'x'.repeat(201), kundenart: 'privat' }, { kunde: KUNDE }).ok, false)
assert.equal(pruefeSchritt(1, { ...f, titel: 'x'.repeat(200), kundenart: 'privat' }, { kunde: KUNDE }).ok, true)
assert.equal(pruefeSchritt(1, { ...f, titel: 'x', kundenart: 'firma' }, { kunde: KUNDE }).fehler.kundenart, 'Bitte die Kundenart wählen.')
})
test('pruefeSchritt 2 (Festpreis): Datum, Weg, Positionen und Summe', () => {
const pruefe = (over) => pruefeSchritt(2, festpreisForm(over), { kunde: KUNDE })
assert.deepEqual(pruefe(), OK)
const r = pruefe({ auftragVom: '', auftragWeg: '' })
assert.deepEqual(r.fehler, { auftragVom: 'Bitte das Datum der Absprache angeben.', auftragWeg: 'Bitte den Weg der Absprache wählen.' })
assert.equal(r.erstes, 'auftragVom')
for (const schlecht of ['2026-02-30', '06.10.2026', '2026-1-1', 'morgen']) {
assert.equal(pruefe({ auftragVom: schlecht }).fehler.auftragVom, 'Bitte das Datum der Absprache angeben.', schlecht)
}
assert.equal(pruefe({ auftragWeg: 'brieftaube' }).fehler.auftragWeg, 'Bitte den Weg der Absprache wählen.')
assert.equal(pruefe({ art: 'xyz' }).fehler.art, 'Bitte die Art des Auftrags wählen.')
const pos = (o = {}) => ({ uid: 1, description: 'x', quantity: '1', price: '10,00', ...o })
const meldung = (positionen) => pruefe({ positionen }).fehler.positionen
const PREIS = 'Der Einzelpreis muss ein Betrag zwischen 0,01 € und 100.000,00 € sein (z. B. 120,00).'
assert.equal(meldung([pos({ description: ' ' })]), 'Position 1: Bitte eine Beschreibung eingeben.')
assert.equal(meldung([pos(), pos({ description: 'y'.repeat(201) })]), 'Position 2: Die Beschreibung darf höchstens 200 Zeichen lang sein.')
assert.equal(meldung([pos({ quantity: '0' })]), 'Position 1: Die Menge muss eine ganze Zahl von 1 bis 999 sein.')
assert.equal(meldung([pos({ quantity: '1000' })]), 'Position 1: Die Menge muss eine ganze Zahl von 1 bis 999 sein.')
assert.equal(meldung([pos({ price: '' })]), `Position 1: ${PREIS}`)
assert.equal(meldung([pos({ price: '100.000,01' })]), `Position 1: ${PREIS}`)
assert.equal(meldung([]), 'Eine Auftragsbestätigung braucht 1 bis 20 Positionen.')
assert.equal(meldung(Array.from({ length: 21 }, () => pos())), 'Eine Auftragsbestätigung braucht 1 bis 20 Positionen.')
assert.equal(meldung(Array.from({ length: 20 }, () => pos())), undefined)
// Summe: genau 999.999,99 € gehen, 1.000.000,00 € nicht
const neun = Array.from({ length: 9 }, () => pos({ price: '100.000,00' }))
assert.equal(meldung([...neun, pos({ price: '99.999,99' })]), undefined)
assert.equal(meldung([...neun, pos({ price: '100.000,00' })]), 'Die Summe ist zu hoch (höchstens 999.999,99 €).')
})
test('pruefeSchritt 2 (Aufwand): Stundensatz und Schätzung, Positionen zählen nicht', () => {
const pruefe = (over) => pruefeSchritt(2, aufwandForm(over), { kunde: KUNDE })
const SATZ = 'Der Stundensatz muss ein Betrag zwischen 0,01 € und 100.000,00 € sein (z. B. 100,00).'
assert.deepEqual(pruefe(), OK)
assert.equal(pruefe({ schaetzung: '12,5' }).ok, true)
assert.equal(pruefe({ stundensatz: '' }).fehler.stundensatz, SATZ)
assert.equal(pruefe({ stundensatz: '12.50' }).fehler.stundensatz, SATZ)
assert.equal(pruefe({ schaetzung: '10,3' }).fehler.schaetzung, 'Die Schätzung geht in Schritten von 15 Minuten (z. B. 10 oder 12,5 Stunden, höchstens 1666).')
assert.equal(pruefe({ positionen: [{ uid: 1, description: '', quantity: 'x', price: '' }] }).ok, true)
})
test('pruefeSchritt 3: Zahlungsplan, Zahlungsziel, Termine, Zeitquelle', () => {
const pruefe = (f) => pruefeSchritt(3, f, { kunde: KUNDE })
assert.deepEqual(pruefe(festpreisForm()), OK)
assert.equal(pruefe(festpreisForm({ zahlungsart: 'anzahlung', prozent: '50' })).ok, true)
for (const schlecht of ['', '0', '100', '33,3']) {
assert.equal(pruefe(festpreisForm({ zahlungsart: 'anzahlung', prozent: schlecht })).fehler.prozent, 'Die Anzahlung muss eine ganze Zahl von 1 bis 99 Prozent sein.', schlecht)
}
assert.equal(pruefe(festpreisForm({ zahlungsart: 'nach_abnahme', prozent: 'x' })).ok, true) // Prozent zählt nur bei „Anzahlung“
const TEXT = 'Bitte den Zahlungsplan als Text eingeben (höchstens 500 Zeichen).'
assert.equal(pruefe(festpreisForm({ zahlungsart: 'frei', zahlungstext: ' ' })).fehler.zahlungstext, TEXT)
assert.equal(pruefe(festpreisForm({ zahlungsart: 'frei', zahlungstext: 'x'.repeat(501) })).fehler.zahlungstext, TEXT)
assert.equal(pruefe(festpreisForm({ zahlungsart: 'frei', zahlungstext: 'x'.repeat(500) })).ok, true)
for (const schlecht of ['', '61', '-1', '1,5']) {
assert.equal(pruefe(festpreisForm({ zahlungsziel: schlecht })).fehler.zahlungsziel, 'Das Zahlungsziel muss eine ganze Zahl von 0 bis 60 Tagen sein.', schlecht)
}
assert.equal(pruefe(festpreisForm({ zahlungsziel: '0' })).ok, true)
assert.equal(pruefe(festpreisForm({ startAm: '2026-11-01', fertigBis: '2026-11-30' })).ok, true)
assert.equal(pruefe(festpreisForm({ startAm: '2026-11-01', fertigBis: '2026-11-01' })).ok, true)
assert.equal(pruefe(festpreisForm({ startAm: '2026-11-02', fertigBis: '2026-11-01' })).fehler.fertigBis, 'Die Fertigstellung liegt vor dem Start.')
assert.equal(pruefe(festpreisForm({ startAm: '2026-13-01' })).fehler.startAm, 'Bitte ein gültiges Datum wählen.')
assert.equal(pruefe(festpreisForm({ fertigBis: 'bald' })).fehler.fertigBis, 'Bitte ein gültiges Datum wählen.')
const ids = (n) => Array.from({ length: n }, (_, i) => `t${i}`)
assert.equal(pruefe(festpreisForm({ zeitquelle: ids(10) })).ok, true)
assert.equal(pruefe(festpreisForm({ zeitquelle: ids(11) })).fehler.zeitquelle, 'Höchstens 10 Tickets als Zeitquelle.')
const PASST = 'Dieser Zahlungsplan passt nicht zur Art des Auftrags.'
assert.equal(pruefe(festpreisForm({ zahlungsart: 'monatlich' })).fehler.zahlungsart, PASST)
assert.equal(pruefe(aufwandForm({ zahlungsart: 'anzahlung' })).fehler.zahlungsart, PASST)
})
test('formularZuNutzlast: Festpreis in Cent, getrimmte Texte, Kundenart nur wenn sie am Kunden fehlt', () => {
const f = festpreisForm({
titel: ' Website Praxis ', projektId: 'p1',
positionen: [
{ uid: 1, description: ' Startseite ', quantity: '2', price: '400,00' },
{ uid: 2, description: 'Kontaktformular', quantity: '1', price: '1.234,50' },
],
zahlungsart: 'anzahlung', prozent: '30', zahlungsziel: '21',
startAm: '2026-11-01', fertigBis: '2026-11-30', zeitquelle: ['t1', 't2'],
})
const r = formularZuNutzlast(f, { kunde: KUNDE })
assert.equal(r.ok, true)
assert.equal(r.summeCents, 203450)
assert.deepEqual(r.payload, {
projektId: 'p1', titel: 'Website Praxis', kundenart: 'privat', art: 'festpreis',
positionen: [
{ beschreibung: 'Startseite', menge: 2, einzelpreisCents: 40000 },
{ beschreibung: 'Kontaktformular', menge: 1, einzelpreisCents: 123450 },
],
stundensatzCents: null, schaetzungMinuten: null,
zahlungsplan: { art: 'anzahlung', prozent: 30, text: '' }, zahlungszielTage: 21,
auftragVom: '2026-10-06', auftragWeg: 'telefon', startAm: '2026-11-01', fertigBis: '2026-11-30',
zeitquelle: ['t1', 't2'],
})
assert.equal('kundenart' in formularZuNutzlast(f, { kunde: { ...KUNDE, kundenart: 'unternehmen' } }).payload, false)
const frei = formularZuNutzlast(festpreisForm({ zahlungsart: 'frei', zahlungstext: ' Zahlung in zwei Raten ' }), { kunde: KUNDE })
assert.deepEqual(frei.payload.zahlungsplan, { art: 'frei', prozent: null, text: 'Zahlung in zwei Raten' })
assert.equal(frei.payload.projektId, '')
assert.deepEqual([frei.payload.startAm, frei.payload.fertigBis], ['', ''])
})
test('formularZuNutzlast: Aufwand mit Satz in Cent und Schätzung in Minuten, ohne Positionen und Summe', () => {
const r = formularZuNutzlast(aufwandForm({ stundensatz: '95,50', schaetzung: '12,5', zahlungsart: 'monatlich' }), { kunde: { ...KUNDE, kundenart: 'privat' } })
assert.equal(r.ok, true)
assert.equal(r.summeCents, null)
assert.deepEqual(r.payload.positionen, [])
assert.equal(r.payload.art, 'aufwand')
assert.equal(r.payload.stundensatzCents, 9550)
assert.equal(r.payload.schaetzungMinuten, 750)
assert.deepEqual(r.payload.zahlungsplan, { art: 'monatlich', prozent: null, text: '' })
assert.equal(formularZuNutzlast(aufwandForm({ schaetzung: '' }), { kunde: KUNDE }).payload.schaetzungMinuten, null)
})
test('formularZuNutzlast: Fehler nennen den Schritt und das erste Feld', () => {
const r1 = formularZuNutzlast(festpreisForm({ titel: '', zahlungsziel: '99' }), { kunde: KUNDE })
assert.deepEqual([r1.ok, r1.schritt, r1.erstes], [false, 1, 'titel'])
assert.equal(r1.fehler.titel, 'Bitte einen Titel eingeben (höchstens 200 Zeichen).')
const r2 = formularZuNutzlast(festpreisForm({ auftragWeg: '' }), { kunde: KUNDE })
assert.deepEqual([r2.ok, r2.schritt, r2.erstes], [false, 2, 'auftragWeg'])
const r3 = formularZuNutzlast(festpreisForm({ zahlungsziel: '99' }), { kunde: KUNDE })
assert.deepEqual([r3.ok, r3.schritt, r3.erstes], [false, 3, 'zahlungsziel'])
})
test('zahlungsplanVorschau: Wortlaut sinngemäß wie in der Auftragsbestätigung', () => {
const v = (o) => zahlungsplanVorschau({ art: 'nach_abnahme', prozent: null, text: '', summeCents: 80000, zielTage: 14, ...o })
assert.equal(v({}), 'Gesamtbetrag (800,00 €) nach Abnahme; zahlbar innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ summeCents: null }), 'Alles nach Abnahme; zahlbar innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'anzahlung', prozent: 50 }), 'Anzahlung 50 % (400,00 €) nach Auftragsbestätigung, Rest (400,00 €) nach Abnahme; zahlbar jeweils innerhalb von 14 Tagen nach Rechnung.')
// Rundung wie im Vertrag V3: Anzahlung = Math.round(Summe × Prozent / 100), Rest = Summe − Anzahlung
assert.equal(v({ art: 'anzahlung', prozent: 50, summeCents: 33333 }), 'Anzahlung 50 % (166,67 €) nach Auftragsbestätigung, Rest (166,66 €) nach Abnahme; zahlbar jeweils innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'anzahlung', prozent: 33, summeCents: 33333 }), 'Anzahlung 33 % (110,00 €) nach Auftragsbestätigung, Rest (223,33 €) nach Abnahme; zahlbar jeweils innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'anzahlung', prozent: 50, summeCents: null }), 'Anzahlung 50 % nach Auftragsbestätigung, Rest nach Abnahme; zahlbar jeweils innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'anzahlung', prozent: null }), '')
assert.equal(v({ art: 'monatlich', summeCents: null }), 'Monatlich nach Aufwand; zahlbar innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'frei', text: ' Zwei Raten, je zur Hälfte ' }), 'Zwei Raten, je zur Hälfte (zahlbar innerhalb von 14 Tagen nach Rechnung).')
assert.equal(v({ art: 'frei', text: '' }), '')
assert.equal(v({ zielTage: 0 }), 'Gesamtbetrag (800,00 €) nach Abnahme; zahlbar sofort nach Rechnung.')
assert.equal(v({ zielTage: 1 }), 'Gesamtbetrag (800,00 €) nach Abnahme; zahlbar innerhalb von 1 Tag nach Rechnung.')
assert.equal(v({ zielTage: null }), 'Gesamtbetrag (800,00 €) nach Abnahme; zahlbar innerhalb von 14 Tagen nach Rechnung.')
assert.equal(v({ art: 'unbekannt' }), '')
})
test('sendeGate: Lead, E-Mail, Anschrift und Kundenart halten das Senden auf, der Entwurf bleibt möglich', () => {
assert.deepEqual(sendeGate(KUNDE, 'privat'), { erlaubt: true, grund: '', zurUebersicht: false })
assert.deepEqual(sendeGate({ ...KUNDE, kundenart: 'privat' }, ''), { erlaubt: true, grund: '', zurUebersicht: false })
assert.deepEqual(sendeGate(KUNDE, ''), { erlaubt: false, grund: 'Die Kundenart fehlt. Bitte im ersten Schritt wählen.', zurUebersicht: false })
assert.deepEqual(sendeGate({ ...KUNDE, street: '' }, 'privat'), {
erlaubt: false, grund: 'Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.', zurUebersicht: true,
})
assert.deepEqual(sendeGate({ ...KUNDE, email: ' ' }, 'privat'), { erlaubt: false, grund: 'E-Mail fehlt', zurUebersicht: true })
assert.deepEqual(sendeGate({ ...KUNDE, customerStatus: 'lead' }, 'privat'), { erlaubt: false, grund: 'Noch kein fester Kunde', zurUebersicht: true })
assert.deepEqual(sendeGate(null, 'privat'), { erlaubt: false, grund: 'Kein Kunde ausgewählt', zurUebersicht: false })
})
const TICKETS = [
{ $id: 't9', woid: '100149', topic: 'Neueste Anfrage' },
{ $id: 't2', woid: '100140', topic: 'Shop Relaunch' },
{ $id: 't1', woid: '100131', topic: 'Website Praxis' },
{ $id: 'xyz12345', woid: '', title: 'Ohne Nummer' },
]
test('zeitquelleOptionen: Projekt-Tickets zuerst, dann gewählte, dann der Rest; unbekannte Auswahl bleibt sichtbar', () => {
const o = zeitquelleOptionen(TICKETS, ['t9', 'alt77'], ['t1'])
assert.deepEqual(o.map((x) => x.id), ['t1', 't9', 'alt77', 't2', 'xyz12345'])
assert.deepEqual(o.map((x) => x.gewaehlt), [false, true, true, false, false])
assert.deepEqual(o.map((x) => x.projekt), [true, false, false, false, false])
assert.equal(o[0].label, '#100131 Website Praxis')
assert.equal(o[1].label, '#100149 Neueste Anfrage')
assert.equal(o[2].label, 'Ticket alt77')
assert.equal(o[4].label, '#12345 Ohne Nummer')
assert.deepEqual(zeitquelleOptionen(null, [], []), [])
})
test('schalteZeitquelle: höchstens 10, keine Doppelten, Reihenfolge der Auswahl', () => {
assert.deepEqual(schalteZeitquelle(['a'], 'b', true), ['a', 'b'])
assert.deepEqual(schalteZeitquelle(['a', 'b'], 'a', false), ['b'])
assert.deepEqual(schalteZeitquelle(['a'], 'a', true), ['a'])
assert.deepEqual(schalteZeitquelle(undefined, 'a', true), ['a'])
const zehn = Array.from({ length: 10 }, (_, i) => `t${i}`)
assert.deepEqual(schalteZeitquelle(zehn, 'neu', true), zehn)
assert.deepEqual(schalteZeitquelle(zehn, 't3', false), zehn.filter((id) => id !== 't3'))
})
test('kundenartWirksam: die Kundenart am Kunden gilt, sonst die im Formular gewählte', () => {
assert.equal(kundenartWirksam({ kundenart: 'unternehmen' }, 'privat'), 'unternehmen')
assert.equal(kundenartWirksam({ kundenart: '' }, 'privat'), 'privat')
assert.equal(kundenartWirksam({}, 'unternehmen'), 'unternehmen')
assert.equal(kundenartWirksam(null, 'privat'), 'privat')
assert.equal(kundenartWirksam({ kundenart: 'privat' }, ''), 'privat')
assert.equal(kundenartWirksam({ kundenart: 'firma' }, ''), '')
assert.equal(kundenartWirksam({ kundenart: '' }, 'firma'), '')
assert.equal(kundenartWirksam(null, undefined), '')
})
test('vorOrtHinweis: nur bei „Vor Ort“ und Privatperson (§ 312f Abs. 1 BGB), nie als Sperre', () => {
const TEXT = 'Bei Aufträgen vor Ort mit Privatpersonen die Auftragsbestätigung zusätzlich ausdrucken und übergeben.'
assert.equal(VOR_ORT_HINWEIS, TEXT)
assert.equal(vorOrtHinweis('vor_ort', 'privat'), TEXT)
for (const [weg, art] of [['vor_ort', 'unternehmen'], ['vor_ort', ''], ['telefon', 'privat'], ['muendlich', 'privat'], ['email', 'privat'], ['chat', 'privat'], ['', 'privat'], [undefined, 'privat']]) {
assert.equal(vorOrtHinweis(weg, art), '', `${weg}/${art}`)
}
})
test('auftragHinweise: fehlende Steuernummer, dann „vor Ort“ bei Privatperson; sonst nichts', () => {
const STN = 'Die Steuernummer fehlt noch – sie steht dann nicht auf der Auftragsbestätigung. Eintragen unter Finanzen → Einstellungen.'
const VOR = 'Bei Aufträgen vor Ort mit Privatpersonen die Auftragsbestätigung zusätzlich ausdrucken und übergeben.'
const hinweise = (o) => auftragHinweise({ kunde: KUNDE, form: festpreisForm(), einstellungen: { steuernummer: '12/345/67890' }, ...o })
assert.deepEqual(hinweise(), []) // festpreisForm: Privatperson, Weg „telefon“, Steuernummer vorhanden
assert.deepEqual(hinweise({ einstellungen: { steuernummer: '' } }), [{ key: 'steuernummer', text: STN }])
assert.deepEqual(hinweise({ einstellungen: {} }).map((h) => h.key), ['steuernummer'])
// Einstellungen noch nicht geladen (undefined) oder nicht ladbar (null): keine Behauptung
assert.deepEqual(hinweise({ einstellungen: undefined }), [])
assert.deepEqual(hinweise({ einstellungen: null }), [])
assert.deepEqual(hinweise({ form: festpreisForm({ auftragWeg: 'vor_ort' }) }), [{ key: 'vorOrt', text: VOR }])
assert.deepEqual(hinweise({ form: festpreisForm({ auftragWeg: 'vor_ort', kundenart: 'unternehmen' }) }), [])
assert.deepEqual(hinweise({ form: festpreisForm({ auftragWeg: 'vor_ort', kundenart: '' }) }), [])
// die Kundenart des Kunden hat Vorrang vor dem Formular
const privatAmKunden = { kunde: { ...KUNDE, kundenart: 'privat' }, form: festpreisForm({ auftragWeg: 'vor_ort', kundenart: '' }) }
assert.deepEqual(hinweise(privatAmKunden).map((h) => h.key), ['vorOrt'])
assert.deepEqual(hinweise({ ...privatAmKunden, einstellungen: { steuernummer: '' } }).map((h) => h.key), ['steuernummer', 'vorOrt'])
})