Files
Webklar-Kundenbereich/test/auftraegeAbEingabe.test.js

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