Files

681 lines
47 KiB
JavaScript
Raw Permalink 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,
AUFTRAG_STATUS, PROJEKT_ZUORDNEN_STATUS, istEntwurf, zuordnenOptionen, projektZuordnenMoeglich, auftragAktionen,
auftragNotizen, auftragZeilen, auftragBestaetigung, auftragErfolg, auftragFehler, neueSperre,
} = 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'])
})
// --- Liste -------------------------------------------------------------------------------------------------------------
const A = (o = {}) => ({
id: 'a1', nummer: 'AB-2026-0002', status: 'versendet', kundenId: 'c1', projektId: 'p1', titel: 'Website Praxis Müller',
art: 'festpreis', stundensatzCents: null, schaetzungMinuten: null, summeCents: 80000,
gesendetAm: '2026-10-06T08:15:00.000+00:00', gesendetAn: 'info@praxis.test', mailStatus: 'gesendet', mailFehler: '',
bestaetigtAm: null, bestaetigtUeber: '', fruehbeginn: false, ersetztId: '', ersetztDurchId: '', widerrufenAm: null,
erstelltAm: '2026-10-05T16:00:00.000+00:00', geaendertAm: '2026-10-06T08:15:00.000+00:00',
pdfVorhanden: true, fruehesterBeginn: null, zahlungsplanText: '', aufwandText: null, ...o,
})
test('Status: Beschriftung und Ton für alle sieben Status (Vertrag V2)', () => {
assert.deepEqual(Object.entries(AUFTRAG_STATUS).map(([k, v]) => [k, v.label]), [
['entwurf', 'Entwurf'], ['versendet', 'Versendet'], ['bestaetigt', 'Bestätigt'], ['abgerechnet', 'Abgerechnet'],
['storniert', 'Storniert'], ['ersetzt', 'Ersetzt'], ['widerrufen', 'Widerrufen'],
])
assert.deepEqual(Object.values(AUFTRAG_STATUS).map((v) => v.tone), ['muted', 'info', 'ok', 'violet', 'plain', 'plain', 'danger'])
// „Projekt zuordnen“ gilt für diese Status (Übersicht, Nachträge; V5 `POST …/projekt` lehnt die übrigen mit 409 ab)
assert.deepEqual(PROJEKT_ZUORDNEN_STATUS, ['entwurf', 'versendet', 'bestaetigt', 'abgerechnet'])
})
test('auftragZeilen: Nummer, Titel, Datum, Summe und Status', () => {
const [z] = auftragZeilen([A()])
assert.deepEqual([z.id, z.nummerText, z.titel, z.datumText], ['a1', 'AB-2026-0002', 'Website Praxis Müller', '06.10.2026'])
assert.deepEqual([z.summeText, z.summeSub, z.istEntwurf], ['800,00 €', '', false])
assert.deepEqual([z.statusLabel, z.statusTone], ['Versendet', 'info'])
assert.equal(z.auftrag.id, 'a1')
})
test('auftragZeilen: Entwurf ohne Nummer, nach Aufwand, Projektname nur wenn er vom Titel abweicht', () => {
const konten = [{ projektId: 'p1', name: 'Praxis Müller – Relaunch' }]
const entwurf = A({
id: 'a2', nummer: '', status: 'entwurf', gesendetAm: null, pdfVorhanden: false, art: 'aufwand', summeCents: null,
stundensatzCents: 10000, schaetzungMinuten: 600, erstelltAm: '2026-10-05T22:30:00.000+00:00',
})
const [z] = auftragZeilen([entwurf], { konten })
assert.deepEqual([z.nummerText, z.istEntwurf, z.datumText], ['Entwurf', true, '06.10.2026'])
assert.deepEqual([z.summeText, z.summeSub], ['nach Aufwand', '100,00 € pro Std., Schätzung etwa 10 Std.'])
assert.equal(z.projektText, 'Praxis Müller – Relaunch')
assert.equal(auftragZeilen([A()], { konten: [{ projektId: 'p1', name: 'Website Praxis Müller' }] })[0].projektText, '')
assert.equal(auftragZeilen([A({ projektId: '' })], { konten })[0].projektText, '')
assert.equal(auftragZeilen([A({ projektId: 'p9' })], { konten })[0].projektText, '')
assert.equal(auftragZeilen([A()])[0].projektText, '')
assert.equal(auftragZeilen([A({ art: 'aufwand', summeCents: null, stundensatzCents: 9550, schaetzungMinuten: null })])[0].summeSub, '95,50 € pro Std.')
assert.equal(auftragZeilen([A({ summeCents: null })])[0].summeText, '–')
})
test('auftragZeilen: Notizen und Aktionen hängen an der Zeile, die Reihenfolge des Servers bleibt', () => {
const zeilen = auftragZeilen([A({ id: 'x2', mailStatus: 'fehler' }), A({ id: 'x1', status: 'entwurf', nummer: '' })])
assert.deepEqual(zeilen.map((z) => z.id), ['x2', 'x1'])
assert.deepEqual(zeilen[0].notizen.map((n) => n.text), ['Mail nicht zugestellt'])
assert.equal(zeilen[0].aktionen[1].label, 'Erneut senden')
assert.equal(zeilen[1].istEntwurf, true)
assert.deepEqual(auftragZeilen(null), [])
assert.deepEqual(auftragZeilen([null, 'x', A()]).map((z) => z.id), ['a1'])
})
test('Zusatzzeilen: Mail, bestätigt am, Frühbeginn, Arbeitsbeginn, Fassungen, Widerruf', () => {
const texte = (o, alle) => auftragNotizen(A(o), alle).map((n) => n.text)
const BESTAETIGT = '2026-10-07T09:00:00.000+00:00'
assert.deepEqual(texte({}), [])
assert.deepEqual(texte({ mailStatus: 'fehler', mailFehler: 'SMTP' }), ['Mail nicht zugestellt'])
assert.deepEqual(auftragNotizen(A({ mailStatus: 'fehler' })).map((n) => n.tone), ['warn'])
// nach der Bestätigung zählt ein früherer Mailfehler nicht mehr
assert.deepEqual(texte({ status: 'bestaetigt', mailStatus: 'fehler', bestaetigtAm: BESTAETIGT }), ['Bestätigt am 07.10.2026'])
assert.deepEqual(texte({ fruehesterBeginn: '2026-10-21' }), ['Arbeitsbeginn frühestens am 21.10.2026'])
assert.deepEqual(texte({ status: 'bestaetigt', fruehbeginn: true, bestaetigtAm: BESTAETIGT }), ['Bestätigt am 07.10.2026', 'Frühbeginn verlangt'])
assert.deepEqual(texte({ status: 'bestaetigt', fruehesterBeginn: '2026-10-21', bestaetigtAm: BESTAETIGT }), ['Bestätigt am 07.10.2026', 'Arbeitsbeginn frühestens am 21.10.2026'])
// ohne Wirkung bei Entwurf und bei beendeten ABs
assert.deepEqual(texte({ status: 'entwurf', fruehesterBeginn: '2026-10-21', fruehbeginn: true }), [])
assert.deepEqual(texte({ status: 'storniert', fruehesterBeginn: '2026-10-21' }), [])
assert.deepEqual(texte({ status: 'widerrufen', widerrufenAm: '2026-10-09T10:00:00.000+00:00' }), ['Widerrufen am 09.10.2026'])
const alle = [
A({ id: 'a1', nummer: 'AB-2026-0002', status: 'ersetzt', ersetztDurchId: 'a3' }),
A({ id: 'a3', nummer: 'AB-2026-0004', ersetztId: 'a1' }),
A({ id: 'a4', nummer: '', status: 'entwurf', ersetztId: 'a3' }),
]
assert.deepEqual(auftragNotizen(alle[0], alle).map((n) => n.text), ['Ersetzt durch AB-2026-0004'])
assert.deepEqual(auftragNotizen(alle[1], alle).map((n) => n.text), ['Neue Fassung zu AB-2026-0002'])
assert.deepEqual(auftragNotizen(alle[2], alle).map((n) => n.text), ['Neue Fassung zu AB-2026-0004'])
assert.deepEqual(texte({ ersetztId: 'fremd' }, []), ['Neue Fassung'])
assert.deepEqual(texte({ status: 'ersetzt', ersetztDurchId: 'fremd' }, []), ['Ersetzt durch eine neue Fassung'])
})
test('auftragAktionen: je Status genau die erlaubten, nach dem Senden nur noch „Neue Fassung“ statt Bearbeiten', () => {
const keys = (o) => auftragAktionen(A(o)).map((a) => a.key)
assert.deepEqual(keys({ status: 'entwurf', pdfVorhanden: false }), ['bearbeiten', 'senden', 'pdf', 'loeschen'])
assert.deepEqual(keys({ status: 'versendet' }), ['pdf', 'erinnern', 'neueFassung', 'stornieren'])
assert.deepEqual(keys({ status: 'bestaetigt' }), ['pdf', 'neueFassung', 'stornieren'])
assert.deepEqual(keys({ status: 'abgerechnet' }), ['pdf', 'neueFassung'])
for (const status of ['storniert', 'ersetzt', 'widerrufen']) assert.deepEqual(keys({ status }), ['pdf'], status)
for (const status of ['versendet', 'bestaetigt', 'abgerechnet', 'storniert', 'ersetzt', 'widerrufen']) {
assert.equal(keys({ status }).some((k) => ['bearbeiten', 'senden', 'loeschen'].includes(k)), false, status)
}
// ohne gespeichertes PDF keine PDF-Aktion (beim Entwurf gibt es die Vorschau immer)
assert.deepEqual(keys({ status: 'versendet', pdfVorhanden: false }), ['erinnern', 'neueFassung', 'stornieren'])
assert.deepEqual(keys({ status: 'storniert', pdfVorhanden: false }), [])
assert.deepEqual(auftragAktionen(A({ status: 'entwurf' })).map((a) => a.label), ['Bearbeiten', 'Senden', 'Vorschau', 'Löschen'])
assert.deepEqual(auftragAktionen(A()).map((a) => a.label), ['PDF', 'Erinnerung senden', 'Neue Fassung', 'Stornieren'])
assert.equal(auftragAktionen(A({ mailStatus: 'fehler' }))[1].label, 'Erneut senden')
assert.deepEqual(auftragAktionen(A({ status: 'entwurf' })).filter((a) => a.danger).map((a) => a.key), ['loeschen'])
assert.deepEqual(auftragAktionen(A()).filter((a) => a.danger).map((a) => a.key), ['stornieren'])
assert.deepEqual(auftragAktionen(A({ status: 'xyz', pdfVorhanden: false })), [])
assert.deepEqual(auftragAktionen(null), [])
assert.equal(istEntwurf(A({ status: 'entwurf' })), true)
assert.equal(istEntwurf(A()), false)
assert.equal(istEntwurf(null), false)
})
test('Projekt zuordnen: Auswahl aus den Konten, Verfügbarkeit nach Status und Projekt', () => {
const konten = [{ projektId: 'p1', name: 'Website Praxis Müller' }, { projektId: 'p2', name: ' Shop Relaunch ' }, { projektId: 'p3', name: '' }, { name: 'ohne Kennung' }, null]
assert.deepEqual(zuordnenOptionen(konten), [
{ value: 'p1', label: 'Website Praxis Müller' }, { value: 'p2', label: 'Shop Relaunch' }, { value: 'p3', label: 'p3' },
])
assert.deepEqual(zuordnenOptionen(undefined), [])
assert.deepEqual(zuordnenOptionen('x'), [])
const ohne = (o) => A({ projektId: '', ...o })
for (const status of ['entwurf', 'versendet', 'bestaetigt', 'abgerechnet']) {
assert.equal(projektZuordnenMoeglich(ohne({ status }), konten), true, status)
}
for (const status of ['storniert', 'ersetzt', 'widerrufen', 'xyz']) {
assert.equal(projektZuordnenMoeglich(ohne({ status }), konten), false, status)
}
assert.equal(projektZuordnenMoeglich(ohne({ projektId: null }), konten), true)
assert.equal(projektZuordnenMoeglich(ohne({ projektId: undefined }), konten), true)
// hat die AB schon ein Projekt (auch ein unbekanntes), gibt es die Aktion nicht: der Server lehnt sie mit 409 ab
assert.equal(projektZuordnenMoeglich(A({ projektId: 'p1' }), konten), false)
assert.equal(projektZuordnenMoeglich(A({ projektId: 'p9' }), konten), false)
// ohne Projekt zur Auswahl ist nichts zu tun
assert.equal(projektZuordnenMoeglich(ohne({}), []), false)
assert.equal(projektZuordnenMoeglich(ohne({}), undefined), false)
assert.equal(projektZuordnenMoeglich(null, konten), false)
})
test('auftragAktionen mit Konten: „Projekt zuordnen“ nur ohne Projekt, vor den gefährlichen Aktionen', () => {
const konten = [{ projektId: 'p1', name: 'Website' }]
const keys = (o, k = konten) => auftragAktionen(A({ projektId: '', ...o }), { konten: k }).map((a) => a.key)
assert.deepEqual(keys({ status: 'entwurf', pdfVorhanden: false }), ['bearbeiten', 'senden', 'pdf', 'projekt', 'loeschen'])
assert.deepEqual(keys({ status: 'versendet' }), ['pdf', 'erinnern', 'neueFassung', 'projekt', 'stornieren'])
assert.deepEqual(keys({ status: 'bestaetigt' }), ['pdf', 'neueFassung', 'projekt', 'stornieren'])
assert.deepEqual(keys({ status: 'abgerechnet' }), ['pdf', 'neueFassung', 'projekt'])
assert.deepEqual(keys({ status: 'versendet', pdfVorhanden: false }), ['erinnern', 'neueFassung', 'projekt', 'stornieren'])
for (const status of ['storniert', 'ersetzt', 'widerrufen']) assert.deepEqual(keys({ status }), ['pdf'], status)
// mit Projekt, ohne Konten und ohne Angabe der Konten gibt es die Aktion nicht
assert.deepEqual(auftragAktionen(A({ status: 'versendet' }), { konten }).map((a) => a.key), ['pdf', 'erinnern', 'neueFassung', 'stornieren'])
assert.deepEqual(keys({ status: 'versendet' }, []), ['pdf', 'erinnern', 'neueFassung', 'stornieren'])
assert.deepEqual(auftragAktionen(A({ projektId: '' })).map((a) => a.key), ['pdf', 'erinnern', 'neueFassung', 'stornieren'])
// Beschriftung, nie gefährlich; Bearbeiten, Senden und Löschen bleiben dem Entwurf vorbehalten
const aktion = auftragAktionen(A({ projektId: '' }), { konten }).find((a) => a.key === 'projekt')
assert.equal(aktion.label, 'Projekt zuordnen')
assert.notEqual(aktion.danger, true)
assert.equal(keys({ status: 'versendet' }).some((k) => ['bearbeiten', 'senden', 'loeschen'].includes(k)), false)
// die Zeilen geben die Konten weiter
assert.deepEqual(auftragZeilen([A({ projektId: '' })], { konten })[0].aktionen.map((a) => a.key), ['pdf', 'erinnern', 'neueFassung', 'projekt', 'stornieren'])
assert.deepEqual(auftragZeilen([A({ projektId: '' })])[0].aktionen.map((a) => a.key), ['pdf', 'erinnern', 'neueFassung', 'stornieren'])
})
test('Rückfragen: Senden, Löschen, Stornieren, Erinnerung, Projekt zuordnen', () => {
assert.equal(
auftragBestaetigung('senden', A({ status: 'entwurf', nummer: '' }), { email: 'info@praxis.test' }),
'Auftragsbestätigung „Website Praxis Müller“ jetzt festschreiben und an info@praxis.test senden? Danach lässt sie sich nicht mehr ändern, nur noch ersetzen oder stornieren.'
)
assert.equal(
auftragBestaetigung('senden', A({ status: 'entwurf', nummer: '' })),
'Auftragsbestätigung „Website Praxis Müller“ jetzt festschreiben und an den Kunden senden? Danach lässt sie sich nicht mehr ändern, nur noch ersetzen oder stornieren.'
)
assert.equal(auftragBestaetigung('loeschen', A({ status: 'entwurf' })), 'Den Entwurf „Website Praxis Müller“ wirklich löschen? Das lässt sich nicht rückgängig machen.')
assert.equal(
auftragBestaetigung('stornieren', A()),
'Die Auftragsbestätigung AB-2026-0002 „Website Praxis Müller“ wirklich stornieren? Der Kunde bekommt keine Mail, der Bestätigungslink wird ungültig. Das lässt sich nicht rückgängig machen.'
)
assert.equal(
auftragBestaetigung('erinnern', A()),
'Die Auftragsbestätigung AB-2026-0002 jetzt noch einmal an den Kunden senden? Der bisherige Bestätigungslink gilt danach nicht mehr.'
)
assert.equal(
auftragBestaetigung('projekt', A({ projektId: '' }), { projektName: 'Physio-Website' }),
'Die Auftragsbestätigung AB-2026-0002 „Website Praxis Müller“ dem Projekt „Physio-Website“ zuordnen? Das Projekt lässt sich danach nur noch über eine neue Fassung ändern.'
)
assert.match(auftragBestaetigung('projekt', A({ projektId: '' })), /„Website Praxis Müller“ einem Projekt zuordnen\? /)
// ein Entwurf bleibt über „Bearbeiten“ änderbar: keine Rückfrage
assert.equal(auftragBestaetigung('projekt', A({ status: 'entwurf', nummer: '', projektId: '' }), { projektName: 'Physio-Website' }), '')
for (const key of ['bearbeiten', 'pdf', 'neueFassung', 'gibtsnicht']) assert.equal(auftragBestaetigung(key, A()), '', key)
assert.match(auftragBestaetigung('senden', A({ titel: '' })), /„ohne Titel“/)
})
test('Erfolgsmeldungen (Wortlaut des Containers von W0 für „senden“)', () => {
const gesendet = A({ nummer: 'AB-2026-0003' }) // gesendetAn: info@praxis.test
assert.deepEqual(auftragErfolg('senden', gesendet), { tone: 'ok', text: 'Auftragsbestätigung AB-2026-0003 wurde an info@praxis.test gesendet.' })
assert.deepEqual(auftragErfolg('senden', A({ nummer: 'AB-2026-0003', gesendetAn: '' })), { tone: 'ok', text: 'Auftragsbestätigung AB-2026-0003 wurde gesendet.' })
assert.deepEqual(auftragErfolg('senden', gesendet, { mailFehler: true }), {
tone: 'warn',
text: 'Auftragsbestätigung AB-2026-0003 ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.',
})
assert.deepEqual(auftragErfolg('erinnern', gesendet), { tone: 'ok', text: 'Die Erinnerung zu AB-2026-0003 wurde an info@praxis.test gesendet.' })
assert.deepEqual(auftragErfolg('erinnern', gesendet, { mailFehler: true }), {
tone: 'warn', text: 'Die Erinnerung zu AB-2026-0003 konnte nicht zugestellt werden. „Erneut senden“ versucht es noch einmal.',
})
assert.deepEqual(auftragErfolg('stornieren', gesendet), { tone: 'ok', text: 'Auftragsbestätigung AB-2026-0003 wurde storniert.' })
assert.deepEqual(auftragErfolg('loeschen', A({ nummer: '' })), { tone: 'ok', text: 'Der Entwurf wurde gelöscht.' })
assert.deepEqual(auftragErfolg('neueFassung', A({ nummer: '' })), { tone: 'ok', text: 'Neue Fassung als Entwurf angelegt.' })
assert.deepEqual(auftragErfolg('projekt', gesendet, { projektName: 'Physio-Website' }), {
tone: 'ok', text: 'Auftragsbestätigung AB-2026-0003 wurde dem Projekt „Physio-Website“ zugeordnet.',
})
assert.deepEqual(auftragErfolg('projekt', gesendet), { tone: 'ok', text: 'Auftragsbestätigung AB-2026-0003 wurde einem Projekt zugeordnet.' })
assert.deepEqual(auftragErfolg('projekt', A({ nummer: '' }), { projektName: 'Physio-Website' }), {
tone: 'ok', text: 'Die Auftragsbestätigung wurde dem Projekt „Physio-Website“ zugeordnet.',
})
assert.deepEqual(auftragErfolg('senden', null), { tone: 'ok', text: 'Die Auftragsbestätigung wurde gesendet.' })
assert.deepEqual(auftragErfolg('senden', null, { mailFehler: true }), {
tone: 'warn', text: 'Die Auftragsbestätigung ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.',
})
assert.deepEqual(auftragErfolg('gibtsnicht', gesendet), { tone: 'ok', text: '' })
})
test('auftragFehler: Kundendaten-Fehler führen zur Übersicht, andere zeigen die Meldung des Servers (Texte aus Plan L3)', () => {
const fehler = (status, message) => auftragFehler(Object.assign(new Error(message), { status }))
assert.deepEqual(fehler(409, 'Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.'), {
art: 'adresse', text: 'Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.', zurUebersicht: true,
})
assert.deepEqual(fehler(400, 'Bitte zuerst die Kundenart angeben (Unternehmen oder Privatperson).'), {
art: 'kundenart',
text: 'Die Kundenart des Kunden fehlt. Bitte in der Übersicht des Kunden festlegen (Feld „Kundenart“) oder über „Bearbeiten“ und dann erneut senden.',
zurUebersicht: true,
})
assert.deepEqual(fehler(400, 'Für den Versand fehlt die E-Mail-Adresse des Kunden.'), {
art: 'email', text: 'Dem Kunden fehlt die E-Mail-Adresse. Bitte in der Übersicht eintragen und dann erneut senden.', zurUebersicht: true,
})
assert.deepEqual(fehler(400, 'Auftragsbestätigungen nur für feste Kunden.'), {
art: 'kunde', text: 'Das ist noch kein fester Kunde. Bitte in der Übersicht zum festen Kunden machen und dann erneut senden.', zurUebersicht: true,
})
// Entwurf unvollständig (L4, wird von L3 beim Senden und bei der Vorschau durchgereicht): Meldung unverändert, kein „aktueller Stand“
assert.deepEqual(fehler(409, 'Entwurf unvollständig: Titel fehlt.'), {
art: 'entwurf', text: 'Entwurf unvollständig: Titel fehlt.', zurUebersicht: false,
})
assert.equal(fehler(409, 'Entwurf unvollständig: Kundendaten fehlen.').art, 'entwurf')
assert.deepEqual(fehler(409, 'Nur ein Entwurf lässt sich senden.'), {
art: 'stand', text: 'Nur ein Entwurf lässt sich senden. Die Liste zeigt den aktuellen Stand.', zurUebersicht: false,
})
assert.deepEqual(fehler(400, 'Titel: Bitte einen Titel angeben oder ein Projekt wählen.'), {
art: 'eingabe', text: 'Titel: Bitte einen Titel angeben oder ein Projekt wählen.', zurUebersicht: false,
})
assert.deepEqual(fehler(500, 'Das PDF konnte nicht erzeugt werden.'), {
art: 'pdf', text: 'Das PDF konnte nicht erzeugt werden. Die Auftragsbestätigung bleibt ein Entwurf, es wurde keine Nummer vergeben.', zurUebersicht: false,
})
assert.deepEqual(fehler(500, 'Interner Fehler.'), {
art: 'server',
text: 'Interner Fehler. Bitte in der Liste prüfen, ob die Auftragsbestätigung gesendet wurde, und sonst erneut versuchen.',
zurUebersicht: false,
})
assert.deepEqual(fehler(0, 'Das Kundenportal ist gerade nicht erreichbar.'), { art: 'netz', text: 'Das Kundenportal ist gerade nicht erreichbar.', zurUebersicht: false })
assert.deepEqual(fehler(404, 'Auftragsbestätigung nicht gefunden.'), { art: 'sonst', text: 'Auftragsbestätigung nicht gefunden.', zurUebersicht: false })
assert.deepEqual(auftragFehler(new Error('')), { art: 'sonst', text: 'Die Aktion ist fehlgeschlagen.', zurUebersicht: false })
assert.deepEqual(auftragFehler(null), { art: 'sonst', text: 'Die Aktion ist fehlgeschlagen.', zurUebersicht: false })
})
test('neueSperre: ein zweiter Aufruf während des ersten wird ignoriert (Doppelklick)', async () => {
const sperre = neueSperre()
let aufrufe = 0
let fertig
const warten = new Promise((r) => { fertig = r })
const erster = sperre.run(async () => { aufrufe += 1; await warten; return 'ok' })
const zweiter = await sperre.run(async () => { aufrufe += 1; return 'doppelt' })
assert.deepEqual(zweiter, { uebersprungen: true })
assert.equal(sperre.belegt, true)
fertig()
assert.deepEqual(await erster, { uebersprungen: false, wert: 'ok' })
assert.equal(aufrufe, 1)
assert.equal(sperre.belegt, false)
assert.deepEqual(await sperre.run(async () => 'wieder'), { uebersprungen: false, wert: 'wieder' })
})
test('neueSperre: nach einem Fehler wieder frei, der Fehler wird weitergereicht', async () => {
const sperre = neueSperre()
await assert.rejects(() => sperre.run(async () => { throw new Error('kaputt') }), /kaputt/)
assert.equal(sperre.belegt, false)
})