AB-Kern: Eingabeprüfung für Auftragsbestätigungen (pruefeEingabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3df5383d1
commit
ef646f12de
2 files changed
+378
No files matched your search
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Abläufe der Auftragsbestätigung (AB): Eingabeprüfung, Entwurf, Vorschau, Senden, Erinnern, neue Fassung, Stornieren
|
||||
* und nachträgliches Zuordnen eines Projekts (Spec Abschnitt 2, Übersicht V4 „L3 – AB-Kern“).
|
||||
* Speicher nur über auftragStore.js und projektRechnungenStore.js, PDFs nur über ablage.js; PDF, Token und Mail kommen
|
||||
* über `deps` (L4/L5, Standard: dynamischer Import beim Aufruf). Jede Änderung an einer bestehenden AB läuft unter
|
||||
* sperreAuftrag(id) (konstanten.js), das Senden zusätzlich unter SPERRE_NUMMER; Mails gehen immer erst nach der Sperre.
|
||||
*/
|
||||
import {
|
||||
AUFTRAG_ARTEN, AUFTRAG_WEGE, KUNDENARTEN, LIMITS, STUNDENSATZ_STANDARD_CENTS, VIERTELSTUNDE, ZAHLUNGSPLAN_ARTEN,
|
||||
} from './konstanten.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// Eingabeprüfung (rein, synchron)
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Appwrite-Dokument-ID (wie in auftragStore.js). */
|
||||
export function istDokumentId(wert) {
|
||||
return typeof wert === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/.test(wert)
|
||||
}
|
||||
|
||||
const fehler400 = (meldung) => new AuftragError(400, meldung)
|
||||
const MELDUNG_POSITIONEN = 'Eine Auftragsbestätigung braucht 1 bis 20 Positionen.'
|
||||
const MELDUNG_ART = 'Art: Bitte Festpreis oder Nach Aufwand wählen.'
|
||||
const MELDUNG_PLAN = 'Zahlungsplan: Bitte eine Zahlungsart wählen.'
|
||||
const MELDUNG_VOM = 'Auftrag vom: Bitte ein gültiges Datum angeben (JJJJ-MM-TT).'
|
||||
const MELDUNG_WEG = 'Weg: Bitte den Weg der Absprache wählen.'
|
||||
const MELDUNG_SUMME = 'Die Auftragssumme ist zu hoch (höchstens 999.999,99 €).'
|
||||
const PREIS_GRENZE = 'zwischen 0,01 € und 100.000,00 € liegen.'
|
||||
|
||||
const istEinfachesObjekt = (wert) => Object.prototype.toString.call(wert) === '[object Object]'
|
||||
|
||||
function istKalendertag(wert) {
|
||||
if (typeof wert !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(wert)) return false
|
||||
const datum = new Date(`${wert}T00:00:00Z`)
|
||||
return !Number.isNaN(datum.getTime()) && datum.toISOString().slice(0, 10) === wert
|
||||
}
|
||||
|
||||
function pruefePosition(roh, n) {
|
||||
const p = istEinfachesObjekt(roh) ? roh : {}
|
||||
const beschreibung = typeof p.beschreibung === 'string' ? p.beschreibung.trim() : ''
|
||||
if (beschreibung.length < 1 || beschreibung.length > LIMITS.maxBeschreibung) {
|
||||
throw fehler400(`Position ${n}: Die Beschreibung braucht 1 bis ${LIMITS.maxBeschreibung} Zeichen.`)
|
||||
}
|
||||
if (!Number.isInteger(p.menge) || p.menge < 1 || p.menge > LIMITS.maxMenge) {
|
||||
throw fehler400(`Position ${n}: Die Menge muss eine ganze Zahl von 1 bis ${LIMITS.maxMenge} sein.`)
|
||||
}
|
||||
if (!Number.isInteger(p.einzelpreisCents) || p.einzelpreisCents < 1 || p.einzelpreisCents > LIMITS.maxEinzelpreisCents) {
|
||||
throw fehler400(`Position ${n}: Der Einzelpreis muss ${PREIS_GRENZE}`)
|
||||
}
|
||||
return { beschreibung, menge: p.menge, einzelpreisCents: p.einzelpreisCents }
|
||||
}
|
||||
|
||||
function pruefePlan(roh) {
|
||||
if (!istEinfachesObjekt(roh) || !ZAHLUNGSPLAN_ARTEN.includes(roh.art)) throw fehler400(MELDUNG_PLAN)
|
||||
let prozent = null
|
||||
let text = ''
|
||||
if (roh.art === 'anzahlung') {
|
||||
prozent = roh.prozent === undefined || roh.prozent === null ? LIMITS.standardAnzahlungProzent : roh.prozent
|
||||
if (!Number.isInteger(prozent) || prozent < 1 || prozent > 99) throw fehler400('Zahlungsplan: Die Anzahlung muss eine ganze Zahl von 1 bis 99 Prozent sein.')
|
||||
}
|
||||
if (roh.art === 'frei') {
|
||||
text = typeof roh.text === 'string' ? roh.text.trim() : ''
|
||||
if (text.length < 1 || text.length > LIMITS.maxZahlungsplanText) throw fehler400(`Zahlungsplan: Der freie Text braucht 1 bis ${LIMITS.maxZahlungsplanText} Zeichen.`)
|
||||
}
|
||||
return { art: roh.art, prozent, text }
|
||||
}
|
||||
|
||||
function pruefeOptionalesDatum(wert, meldung) {
|
||||
if (wert === null || wert === '') return ''
|
||||
if (!istKalendertag(wert)) throw fehler400(meldung)
|
||||
return wert
|
||||
}
|
||||
|
||||
/** Feldregeln: jedes übergebene Feld (Wert !== undefined) wird geprüft und bereinigt. */
|
||||
const FELDREGELN = {
|
||||
art: (w) => {
|
||||
if (!AUFTRAG_ARTEN.includes(w)) throw fehler400(MELDUNG_ART)
|
||||
return w
|
||||
},
|
||||
projektId: (w) => {
|
||||
if (w === null || w === '') return ''
|
||||
if (!istDokumentId(w)) throw fehler400('Projekt: Ungültige Projekt-Kennung.')
|
||||
return w
|
||||
},
|
||||
titel: (w) => {
|
||||
if (w === null) return ''
|
||||
if (typeof w !== 'string' || w.trim().length > LIMITS.maxTitel) throw fehler400(`Titel: Höchstens ${LIMITS.maxTitel} Zeichen.`)
|
||||
return w.trim()
|
||||
},
|
||||
kundenart: (w) => {
|
||||
if (w === null || w === '') return ''
|
||||
if (!KUNDENARTEN.includes(w)) throw fehler400('Kundenart: Bitte Unternehmen oder Privatperson wählen.')
|
||||
return w
|
||||
},
|
||||
positionen: (w) => {
|
||||
if (!Array.isArray(w) || w.length > LIMITS.maxPositionen) throw fehler400(MELDUNG_POSITIONEN)
|
||||
return w.map((p, i) => pruefePosition(p, i + 1))
|
||||
},
|
||||
stundensatzCents: (w) => {
|
||||
if (w === null) return null
|
||||
if (!Number.isInteger(w) || w < 1 || w > LIMITS.maxEinzelpreisCents) throw fehler400(`Stundensatz: Der Stundensatz muss ${PREIS_GRENZE}`)
|
||||
return w
|
||||
},
|
||||
schaetzungMinuten: (w) => {
|
||||
if (w === null || w === '' || w === 0) return null
|
||||
if (!Number.isInteger(w) || w % VIERTELSTUNDE !== 0 || w < VIERTELSTUNDE || w > LIMITS.maxSchaetzungMinuten) {
|
||||
throw fehler400(`Schätzung: Die Schätzung muss ein Vielfaches von ${VIERTELSTUNDE} Minuten sein (15 bis 99.960).`)
|
||||
}
|
||||
return w
|
||||
},
|
||||
zahlungsplan: pruefePlan,
|
||||
zahlungszielTage: (w) => {
|
||||
if (w === null) return null
|
||||
if (!Number.isInteger(w) || w < 0 || w > LIMITS.maxZahlungszielTage) {
|
||||
throw fehler400(`Zahlungsziel: Das Zahlungsziel muss eine ganze Zahl von 0 bis ${LIMITS.maxZahlungszielTage} Tagen sein.`)
|
||||
}
|
||||
return w
|
||||
},
|
||||
auftragVom: (w) => {
|
||||
if (!istKalendertag(w)) throw fehler400(MELDUNG_VOM)
|
||||
return w
|
||||
},
|
||||
auftragWeg: (w) => {
|
||||
if (typeof w !== 'string' || !Object.hasOwn(AUFTRAG_WEGE, w)) throw fehler400(MELDUNG_WEG)
|
||||
return w
|
||||
},
|
||||
startAm: (w) => pruefeOptionalesDatum(w, 'Start: Bitte ein gültiges Datum angeben (JJJJ-MM-TT) oder leer lassen.'),
|
||||
fertigBis: (w) => pruefeOptionalesDatum(w, 'Fertigstellung: Bitte ein gültiges Datum angeben (JJJJ-MM-TT) oder leer lassen.'),
|
||||
zeitquelle: (w) => {
|
||||
if (w === null) return []
|
||||
if (!Array.isArray(w)) throw fehler400('Zeitquelle: Eine Liste von Ticket-Kennungen erwartet.')
|
||||
if (!w.every(istDokumentId)) throw fehler400('Zeitquelle: Ungültige Ticket-Kennung.')
|
||||
const ohneDoppelte = [...new Set(w)]
|
||||
if (ohneDoppelte.length > LIMITS.maxZeitquellen) throw fehler400(`Zeitquelle: Höchstens ${LIMITS.maxZeitquellen} Tickets.`)
|
||||
return ohneDoppelte
|
||||
},
|
||||
}
|
||||
const FELD_REIHENFOLGE = Object.keys(FELDREGELN)
|
||||
|
||||
/**
|
||||
* Prüft und bereinigt die Eingabe einer AB. Nur Felder der Whitelist (FELDREGELN) werden übernommen; status, nummer,
|
||||
* tokenHash & Co. landen nie im Ergebnis. teilweise: nur die übergebenen Felder, ohne Pflicht-, Vorgabe- und Querprüfung.
|
||||
*/
|
||||
export function pruefeEingabe(eingabe, { teilweise = false } = {}) {
|
||||
if (!istEinfachesObjekt(eingabe)) throw fehler400('Ungültige Eingabe.')
|
||||
const felder = {}
|
||||
for (const name of FELD_REIHENFOLGE) {
|
||||
if (eingabe[name] !== undefined) felder[name] = FELDREGELN[name](eingabe[name])
|
||||
}
|
||||
if (teilweise) return felder
|
||||
|
||||
// Pflichtfelder: fehlt eines, kommt dieselbe Meldung wie bei einem ungültigen Wert
|
||||
if (felder.art === undefined) throw fehler400(MELDUNG_ART)
|
||||
if (felder.zahlungsplan === undefined) throw fehler400(MELDUNG_PLAN)
|
||||
if (felder.auftragVom === undefined) throw fehler400(MELDUNG_VOM)
|
||||
if (felder.auftragWeg === undefined) throw fehler400(MELDUNG_WEG)
|
||||
|
||||
const ergebnis = {
|
||||
projektId: felder.projektId ?? '',
|
||||
titel: felder.titel ?? '',
|
||||
kundenart: felder.kundenart ?? '',
|
||||
art: felder.art,
|
||||
positionen: [],
|
||||
stundensatzCents: null,
|
||||
schaetzungMinuten: null,
|
||||
summeCents: null,
|
||||
zahlungsplan: felder.zahlungsplan,
|
||||
zahlungszielTage: felder.zahlungszielTage ?? LIMITS.standardZahlungszielTage,
|
||||
auftragVom: felder.auftragVom,
|
||||
auftragWeg: felder.auftragWeg,
|
||||
startAm: felder.startAm ?? '',
|
||||
fertigBis: felder.fertigBis ?? '',
|
||||
zeitquelle: felder.zeitquelle ?? [],
|
||||
}
|
||||
if (felder.art === 'festpreis') {
|
||||
if (!felder.positionen || felder.positionen.length === 0) throw fehler400(MELDUNG_POSITIONEN)
|
||||
const summe = felder.positionen.reduce((s, p) => s + p.menge * p.einzelpreisCents, 0)
|
||||
if (summe > LIMITS.maxSummeCents) throw fehler400(MELDUNG_SUMME)
|
||||
if (felder.zahlungsplan.art === 'monatlich') throw fehler400('Zahlungsplan: „Monatlich nach Aufwand“ geht nur bei einem Auftrag nach Aufwand.')
|
||||
ergebnis.positionen = felder.positionen
|
||||
ergebnis.summeCents = summe
|
||||
} else {
|
||||
if (felder.zahlungsplan.art === 'anzahlung') throw fehler400('Zahlungsplan: Eine Anzahlung geht nur bei einem Festpreis.')
|
||||
ergebnis.stundensatzCents = felder.stundensatzCents ?? STUNDENSATZ_STANDARD_CENTS
|
||||
ergebnis.schaetzungMinuten = felder.schaetzungMinuten ?? null
|
||||
}
|
||||
return ergebnis
|
||||
}
|
||||
Reference in new issue
Block a user