Buchungskalender: Preise je Personenzahl (Server)

Jede Staffel darf personenCent tragen (Preis pro Nacht für 1 bis k Personen, k = 2 bis 10), ohne Schemaänderung im JSON-Feld staffeln. pruefePreise prüft Länge, Werte und Gleichheit über alle Staffeln und setzt preisProNachtCent = personenCent[0]. berechnePreis wählt die Spalte der Gästezahl (Posten mit Personenzahl), bei mehr Gästen als Spalten gilt die letzte plus Zusatzperson-Aufpreis. preiseVollstaendig prüft die Konsistenz. Ohne personenCent rechnet alles wie bisher. Tests mit Mimis Preismatrix, Doku in unterkunft-api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-11 15:02:45 +00:00
1 parent b264d7f734
commit a4ade5f649
5 files changed
+379 -12

No files matched your search

+51 -6
View File
@@ -1,16 +1,31 @@
// Preisrechnung Buchungskalender: reine Funktionen, keine I/O. Betraege in Cent.
//
// Preismodelle (preise.staffeln, Staffel = hoechste abNaechten <= Naechte, sie gilt fuer alle Naechte der Buchung):
// - pauschal (ohne personenCent): preisProNachtCent gilt fuer die ersten personenInklusive Gaeste, jede weitere Person
// kostet aufpreisPersonNachtCent je Nacht (eigener Posten "Zusatzperson").
// - je Personenzahl (alle Staffeln mit personenCent gleicher Laenge k, 2 bis 10): personenCent[p - 1] ist der Preis pro
// Nacht fuer die ganze Unterkunft bei p Gaesten. personenInklusive wird dann ignoriert; bei mehr als k Gaesten gilt
// personenCent[k - 1] plus der Posten "Zusatzperson" (aufpreisPersonNachtCent) fuer die Gaeste ueber k. Der Server
// haelt preisProNachtCent === personenCent[0] (pruefePreise), damit aeltere Leser weiter rechnen koennen.
// Der Direktrabatt gilt in beiden Modellen auf Uebernachtung und Zusatzpersonen, nicht auf die Reinigung.
import { config } from '../../config.js'
import { naechte } from './datum.js'
const SAETZE = [0, 7, 19]
/** Staffel: hoechste abNaechten <= naechte gilt fuer alle Naechte. */
function staffelPreis(staffeln, n) {
/** Grenzen der Preise je Personenzahl: Laenge k von personenCent (1 bis k Gaeste) und Wert in Cent. */
export const PERSONEN_PREISE_MIN = 2
export const PERSONEN_PREISE_MAX = 10
export const NACHT_CENT_MIN = 100
export const NACHT_CENT_MAX = 100000
/** Die Staffel, die fuer n Naechte gilt (hoechste abNaechten <= n), sonst null. */
function waehleStaffel(staffeln, n) {
let best = null
for (const s of staffeln || []) {
if (s.abNaechten <= n && (!best || s.abNaechten > best.abNaechten)) best = s
}
return best ? best.preisProNachtCent : 0
return best
}
export function berechnePreis({ preise, von, bis, personen }) {
@@ -19,10 +34,23 @@ export function berechnePreis({ preise, von, bis, personen }) {
const satzUe = klein ? 0 : preise.ustUebernachtung
const satzRe = klein ? 0 : preise.ustReinigung
const posten = []
const nachtPreis = staffelPreis(preise.staffeln, n)
posten.push({ text: `Übernachtung, ${n} Nächte`, menge: n, einzelCent: nachtPreis, ustProzent: satzUe })
const staffel = waehleStaffel(preise.staffeln, n)
const pc = staffel && Array.isArray(staffel.personenCent) && staffel.personenCent.length > 0 ? staffel.personenCent : null
let nachtPreis
let inklusive
if (pc) {
// je Personenzahl: die Spalte der Gaestezahl (hoechstens die letzte), der Rest ist Zusatzperson
const gaeste = Number.isFinite(Number(personen)) && Number(personen) >= 1 ? Math.floor(Number(personen)) : 1
inklusive = Math.min(gaeste, pc.length)
nachtPreis = pc[inklusive - 1]
posten.push({ text: `Übernachtung, ${n} Nächte, ${inklusive} ${inklusive === 1 ? 'Person' : 'Personen'}`, menge: n, einzelCent: nachtPreis, ustProzent: satzUe })
} else {
nachtPreis = staffel ? staffel.preisProNachtCent : 0
inklusive = Number(preise.personenInklusive)
posten.push({ text: `Übernachtung, ${n} Nächte`, menge: n, einzelCent: nachtPreis, ustProzent: satzUe })
}
let basis = n * nachtPreis
const extra = Math.max(0, Number(personen) - Number(preise.personenInklusive))
const extra = Math.max(0, Number(personen) - inklusive)
if (extra > 0 && preise.aufpreisPersonNachtCent > 0) {
posten.push({
text: `Zusatzperson${extra > 1 ? 'en' : ''} (${extra}), ${n} Nächte`,
@@ -96,12 +124,29 @@ export function formatEuro(cent) {
return `${neg ? '−' : ''}${euro},${rest} €`
}
/** Preise je Personenzahl: entweder keine Staffel hat personenCent, oder alle gleich lang (2 bis 10) mit gueltigen Werten und preisProNachtCent === personenCent[0]. */
function personenPreiseKonsistent(staffeln) {
const hat = (s) => s.personenCent !== undefined && s.personenCent !== null
if (!staffeln.some(hat)) return true
const k = staffeln[0].personenCent?.length
return staffeln.every(
(s) =>
Array.isArray(s.personenCent) &&
s.personenCent.length === k &&
k >= PERSONEN_PREISE_MIN &&
k <= PERSONEN_PREISE_MAX &&
s.personenCent.every((c) => Number.isInteger(c) && c >= NACHT_CENT_MIN && c <= NACHT_CENT_MAX) &&
s.preisProNachtCent === s.personenCent[0],
)
}
export function preiseVollstaendig(preise) {
if (!preise || !Array.isArray(preise.staffeln) || preise.staffeln.length === 0) return false
const ab = preise.staffeln.map((s) => s && s.abNaechten)
if (!ab.every((a) => Number.isInteger(a) && a > 0)) return false
if (new Set(ab).size !== ab.length || !ab.includes(1)) return false
if (!preise.staffeln.every((s) => Number.isInteger(s.preisProNachtCent) && s.preisProNachtCent > 0)) return false
if (!personenPreiseKonsistent(preise.staffeln)) return false
if (!['regel', 'kleinunternehmer'].includes(preise.steuerart)) return false
if (!Number.isInteger(preise.personenInklusive) || preise.personenInklusive < 1) return false
const nn = (v) => Number.isInteger(v) && v >= 0
+43 -4
View File
@@ -3,6 +3,7 @@ import { config } from '../../config.js'
import { istTag, naechte } from './datum.js'
import { AKTIVE_STATUS, ueberschneidet } from './belegung.js'
import { listeAnfragenNachStatus, listeSperren } from './speicher.js'
import { NACHT_CENT_MAX, NACHT_CENT_MIN, PERSONEN_PREISE_MAX, PERSONEN_PREISE_MIN } from './preis.js'
/**
* Buchungskalender, Verwaltung durch den Vermieter: Validierung der Eingaben (reine Funktionen, Rueckgabe { werte } oder
@@ -26,6 +27,8 @@ export const PLATZHALTER_TEXT = 'Bitte ersetze die Platzhalter in eckigen Klamme
export const MAX_UNTERKUENFTE = 20
export const MAX_ICAL_IMPORTE = 5
export const MAX_STAFFELN = 6
/** Groesse des Textfelds ukPreise.staffeln (schema.js); test/unterkunftVerwaltung.test.js vergleicht beide Werte. */
export const STAFFELN_MAX_ZEICHEN = 2000
// ---- Einstellungen
@@ -149,7 +152,12 @@ export function pruefeUnterkunft(eingabe, { partiell = false } = {}) {
// ---- Preise
/** Pruefung der Preise einer Unterkunft (Staffeln aufsteigend, erste ab 1 Nacht, hoechstens 6). */
/**
* Pruefung der Preise einer Unterkunft (Staffeln aufsteigend, erste ab 1 Nacht, hoechstens 6).
* Jede Staffel darf personenCent tragen (Preis pro Nacht fuer 1..k Personen, k = 2 bis 10): entweder alle Staffeln mit gleich
* vielen Werten oder keine (Modell "pauschal", unveraendert). Im Personen-Modus setzt der Server preisProNachtCent = personenCent[0]
* (eine mitgeschickte Zahl wird ueberschrieben).
*/
export function pruefePreise(eingabe) {
const e = eingabe && typeof eingabe === 'object' && !Array.isArray(eingabe) ? eingabe : {}
const fehler = {}
@@ -160,15 +168,42 @@ export function pruefePreise(eingabe) {
else {
const sauber = []
let letzte = 0
let personenAnzahl // undefined = noch keine Staffel gesehen, 0 = pauschal, sonst Laenge k von personenCent
for (const s of st) {
if (!s || typeof s !== 'object' || !ganz(s.abNaechten) || !ganz(s.preisProNachtCent)) {
if (!s || typeof s !== 'object' || !ganz(s.abNaechten)) {
fehler.staffeln = 'Ungültige Preisstufe.'
break
}
if (s.preisProNachtCent < 100 || s.preisProNachtCent > 100000) {
const hatPersonen = s.personenCent !== undefined && s.personenCent !== null
if (hatPersonen) {
const pc = s.personenCent
if (!Array.isArray(pc)) {
fehler.staffeln = 'Die Preise je Personenzahl sind ungültig.'
break
}
if (pc.length < PERSONEN_PREISE_MIN || pc.length > PERSONEN_PREISE_MAX) {
fehler.staffeln = `Der Preis je Personenzahl braucht ${PERSONEN_PREISE_MIN} bis ${PERSONEN_PREISE_MAX} Werte (für 1 bis ${PERSONEN_PREISE_MAX} Personen).`
break
}
if (!pc.every((c) => ganz(c) && c >= NACHT_CENT_MIN && c <= NACHT_CENT_MAX)) {
fehler.staffeln = 'Der Preis pro Nacht muss zwischen 1 € und 1.000 € liegen.'
break
}
} else if (!ganz(s.preisProNachtCent)) {
fehler.staffeln = 'Ungültige Preisstufe.'
break
} else if (s.preisProNachtCent < NACHT_CENT_MIN || s.preisProNachtCent > NACHT_CENT_MAX) {
fehler.staffeln = 'Der Preis pro Nacht muss zwischen 1 € und 1.000 € liegen.'
break
}
const anzahl = hatPersonen ? s.personenCent.length : 0
if (personenAnzahl !== undefined && anzahl !== personenAnzahl) {
fehler.staffeln = anzahl === 0 || personenAnzahl === 0
? 'Entweder haben alle Preisstufen Preise je Personenzahl oder keine.'
: 'Alle Preisstufen brauchen gleich viele Preise je Personenzahl.'
break
}
personenAnzahl = anzahl
if (sauber.length === 0 && s.abNaechten !== 1) {
fehler.staffeln = 'Die erste Preisstufe muss ab 1 Nacht gelten.'
break
@@ -178,8 +213,12 @@ export function pruefePreise(eingabe) {
break
}
letzte = s.abNaechten
sauber.push({ abNaechten: s.abNaechten, preisProNachtCent: s.preisProNachtCent })
sauber.push(hatPersonen
? { abNaechten: s.abNaechten, preisProNachtCent: s.personenCent[0], personenCent: [...s.personenCent] }
: { abNaechten: s.abNaechten, preisProNachtCent: s.preisProNachtCent })
}
// Sicherung: das Textfeld ukPreise.staffeln fasst STAFFELN_MAX_ZEICHEN (schema.js); der Hoechstfall (6 x 10 Werte) braucht rund 800
if (!fehler.staffeln && JSON.stringify(sauber).length > STAFFELN_MAX_ZEICHEN) fehler.staffeln = 'Die Preisstufen sind zu umfangreich.'
if (!fehler.staffeln) werte.staffeln = sauber
}
const zahl = (k, min, max, label, std) => {