Files
Webklar-Kundenbereich/server/services/unterkunft/preis.js
T

112 lines
5.2 KiB
JavaScript
Raw 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.
// 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
}