Buchungskalender: Preisrechnung und Datumshilfen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 13:47:41 +00:00
1 parent 5717a5cdd0
commit 84e7a99f41
3 files changed
+277

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
// Datumshilfen fuer Buchungskalender: reine Funktionen auf Tagesstrings (YYYY-MM-DD), UTC-Arithmetik, keine I/O.
const TAG_RE = /^\d{4}-\d{2}-\d{2}$/
const TAG_MS = 86400000
function zuMs(tag) {
const [j, m, t] = tag.split('-').map(Number)
return Date.UTC(j, m - 1, t)
}
/** Gueltiger Kalendertag im Format YYYY-MM-DD. */
export function istTag(s) {
if (typeof s !== 'string' || !TAG_RE.test(s)) return false
const d = new Date(zuMs(s))
return d.toISOString().slice(0, 10) === s
}
/** Anzahl Naechte zwischen Anreise und Abreise. */
export function naechte(von, bis) {
return Math.round((zuMs(bis) - zuMs(von)) / TAG_MS)
}
/** Tag um n Tage verschoben. */
export function plusTage(tag, n) {
return new Date(zuMs(tag) + n * TAG_MS).toISOString().slice(0, 10)
}
/** Belegte Naechte: von inklusive, bis exklusive. */
export function tageVon(von, bis) {
const n = naechte(von, bis)
const out = []
for (let i = 0; i < n; i++) out.push(plusTage(von, i))
return out
}
/** '2026-11-12' wird '12.11.2026'. */
export function formatTag(tag) {
const [j, m, t] = tag.split('-')
return `${t}.${m}.${j}`
}
+97
View File
@@ -0,0 +1,97 @@
// Preisrechnung Buchungskalender: reine Funktionen, keine I/O. Betraege in Cent.
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) {
let best = null
for (const s of staffeln || []) {
if (s.abNaechten <= n && (!best || s.abNaechten > best.abNaechten)) best = s
}
return best ? best.preisProNachtCent : 0
}
export function berechnePreis({ preise, von, bis, personen }) {
const n = naechte(von, bis)
const posten = []
const nachtPreis = staffelPreis(preise.staffeln, n)
posten.push({ text: `Übernachtung, ${n} Nächte`, menge: n, einzelCent: nachtPreis, ustProzent: preise.ustUebernachtung })
let basis = n * nachtPreis
const extra = Math.max(0, Number(personen) - Number(preise.personenInklusive))
if (extra > 0 && preise.aufpreisPersonNachtCent > 0) {
posten.push({
text: `Zusatzperson${extra > 1 ? 'en' : ''} (${extra}), ${n} Nächte`,
menge: extra * n,
einzelCent: preise.aufpreisPersonNachtCent,
ustProzent: preise.ustUebernachtung,
})
basis += extra * n * preise.aufpreisPersonNachtCent
}
if (preise.reinigungCent > 0) {
posten.push({ text: 'Endreinigung', menge: 1, einzelCent: preise.reinigungCent, ustProzent: preise.ustReinigung })
}
if (preise.direktrabattProzent > 0) {
const rabatt = -Math.round((basis * preise.direktrabattProzent) / 100)
posten.push({
text: `Direktbuchungsrabatt ${preise.direktrabattProzent} %`,
menge: 1,
einzelCent: rabatt,
ustProzent: preise.ustUebernachtung,
})
}
return { naechte: n, posten, summen: summiere(posten, preise.steuerart) }
}
export function summiere(posten, steuerart) {
const bruttoCent = posten.reduce((a, p) => a + p.menge * p.einzelCent, 0)
if (steuerart === 'kleinunternehmer') return { bruttoCent, nettoCent: bruttoCent, ust: [] }
const proSatz = new Map()
for (const p of posten) proSatz.set(p.ustProzent, (proSatz.get(p.ustProzent) || 0) + p.menge * p.einzelCent)
const ust = [...proSatz.entries()]
.sort((a, b) => a[0] - b[0])
.map(([satz, brutto]) => {
const nettoCent = Math.round((brutto * 100) / (100 + satz))
return { satz, nettoCent, steuerCent: brutto - nettoCent }
})
const nettoCent = ust.reduce((a, u) => a + u.nettoCent, 0)
return { bruttoCent, nettoCent, ust }
}
export function pruefePosten(posten, steuerart) {
const fehler = (m) => ({ fehler: { posten: m } })
const maxPosten = config.unterkunft?.maxPosten ?? 30
const textMax = config.unterkunft?.postenTextMax ?? 200
if (!Array.isArray(posten) || posten.length === 0) return fehler('Mindestens ein Posten ist nötig.')
if (posten.length > maxPosten) return fehler(`Höchstens ${maxPosten} Posten sind erlaubt.`)
const sauber = []
for (const p of posten) {
if (!p || typeof p !== 'object') return fehler('Ungültiger Posten.')
const text = typeof p.text === 'string' ? p.text.trim() : ''
if (!text || text.length > textMax) return fehler(`Der Text eines Postens muss 1 bis ${textMax} Zeichen lang sein.`)
if (!Number.isInteger(p.menge) || p.menge < 1) return fehler('Die Menge muss eine positive ganze Zahl sein.')
if (!Number.isInteger(p.einzelCent)) return fehler('Der Einzelpreis ist ungültig.')
if (!SAETZE.includes(p.ustProzent)) return fehler('Der Steuersatz muss 0, 7 oder 19 sein.')
if (steuerart === 'kleinunternehmer' && p.ustProzent !== 0) return fehler('Bei Kleinunternehmern ist keine Umsatzsteuer erlaubt.')
sauber.push({ text, menge: p.menge, einzelCent: p.einzelCent, ustProzent: p.ustProzent })
}
if (sauber.reduce((a, p) => a + p.menge * p.einzelCent, 0) === 0) return fehler('Die Summe darf nicht 0 sein.')
return { posten: sauber }
}
export function formatEuro(cent) {
const neg = cent < 0
const abs = Math.abs(Math.round(cent))
const euro = String(Math.floor(abs / 100)).replace(/\B(?=(\d{3})+(?!\d))/g, '.')
const rest = String(abs % 100).padStart(2, '0')
return `${neg ? '−' : ''}${euro},${rest} €`
}
export function preiseVollstaendig(preise) {
if (!preise || !Array.isArray(preise.staffeln) || preise.staffeln.length === 0) return false
if (!preise.staffeln.some((s) => s.abNaechten === 1)) return false
if (!preise.staffeln.every((s) => Number.isInteger(s.preisProNachtCent) && s.preisProNachtCent > 0)) return false
if (!['regel', 'kleinunternehmer'].includes(preise.steuerart)) return false
return Number.isInteger(preise.personenInklusive) && preise.personenInklusive >= 1
}