// 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 klein = preise.steuerart === 'kleinunternehmer' 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 }) 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: satzUe, }) basis += extra * n * preise.aufpreisPersonNachtCent } if (preise.reinigungCent > 0) { posten.push({ text: 'Endreinigung', menge: 1, einzelCent: preise.reinigungCent, ustProzent: satzRe }) } if (preise.direktrabattProzent > 0) { const rabatt = -Math.round((basis * preise.direktrabattProzent) / 100) posten.push({ text: `Direktbuchungsrabatt ${preise.direktrabattProzent} %`, menge: 1, einzelCent: rabatt, ustProzent: satzUe, }) } 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.isSafeInteger(p.menge) || p.menge < 1 || p.menge > 1000) return fehler('Die Menge muss eine positive ganze Zahl sein.') if (!Number.isSafeInteger(p.einzelCent) || Math.abs(p.einzelCent) > 10_000_000) 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 muss größer als 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 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 (!['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 if (!nn(preise.reinigungCent) || !nn(preise.aufpreisPersonNachtCent)) return false const r = preise.direktrabattProzent if (!Number.isInteger(r) || r < 0 || r > 50) return false if (preise.steuerart === 'regel') { if (!SAETZE.includes(preise.ustUebernachtung)) return false if (preise.reinigungCent > 0 && !SAETZE.includes(preise.ustReinigung)) return false } return true }