From c38c8e7c13ac10cdfe0a8008de2e0306a5c2e255 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 13:49:20 +0000 Subject: [PATCH] =?UTF-8?q?Buchungskalender:=20Preisrechnung=20nachgesch?= =?UTF-8?q?=C3=A4rft=20(Summe,=20Kleinunternehmer,=20Vollst=C3=A4ndigkeit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- server/services/unterkunft/preis.js | 32 +++++++++++++++++++++-------- test/unterkunftPreis.test.js | 28 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/server/services/unterkunft/preis.js b/server/services/unterkunft/preis.js index 72b18ba..71beb29 100644 --- a/server/services/unterkunft/preis.js +++ b/server/services/unterkunft/preis.js @@ -15,9 +15,12 @@ function staffelPreis(staffeln, n) { 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: preise.ustUebernachtung }) + 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) { @@ -25,12 +28,12 @@ export function berechnePreis({ preise, von, bis, personen }) { text: `Zusatzperson${extra > 1 ? 'en' : ''} (${extra}), ${n} Nächte`, menge: extra * n, einzelCent: preise.aufpreisPersonNachtCent, - ustProzent: preise.ustUebernachtung, + ustProzent: satzUe, }) basis += extra * n * preise.aufpreisPersonNachtCent } if (preise.reinigungCent > 0) { - posten.push({ text: 'Endreinigung', menge: 1, einzelCent: preise.reinigungCent, ustProzent: preise.ustReinigung }) + posten.push({ text: 'Endreinigung', menge: 1, einzelCent: preise.reinigungCent, ustProzent: satzRe }) } if (preise.direktrabattProzent > 0) { const rabatt = -Math.round((basis * preise.direktrabattProzent) / 100) @@ -38,7 +41,7 @@ export function berechnePreis({ preise, von, bis, personen }) { text: `Direktbuchungsrabatt ${preise.direktrabattProzent} %`, menge: 1, einzelCent: rabatt, - ustProzent: preise.ustUebernachtung, + ustProzent: satzUe, }) } return { naechte: n, posten, summen: summiere(posten, preise.steuerart) } @@ -70,13 +73,13 @@ export function pruefePosten(posten, steuerart) { 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 (!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 darf nicht 0 sein.') + 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 } } @@ -90,8 +93,19 @@ export function formatEuro(cent) { 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 + 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 - return Number.isInteger(preise.personenInklusive) && preise.personenInklusive >= 1 + 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 } diff --git a/test/unterkunftPreis.test.js b/test/unterkunftPreis.test.js index c398743..7a8bb23 100644 --- a/test/unterkunftPreis.test.js +++ b/test/unterkunftPreis.test.js @@ -93,6 +93,8 @@ test('berechnePreis/summiere: Kleinunternehmer', () => { }) assert.deepEqual(r.summen.ust, []) assert.equal(r.summen.nettoCent, r.summen.bruttoCent) + assert.ok(r.posten.every((p) => p.ustProzent === 0)) + assert.ok(pruefePosten(r.posten, 'kleinunternehmer').posten) }) test('berechnePreis: Rabatt 0 und Reinigung 0 ohne Posten', () => { @@ -139,3 +141,29 @@ test('preiseVollstaendig', () => { assert.equal(preiseVollstaendig({ ...PREISE, staffeln: [] }), false) assert.equal(preiseVollstaendig(null), false) }) + +test('pruefePosten: negative Summe, Grenzen', () => { + const ok = { text: 'a', menge: 1, einzelCent: 1000, ustProzent: 7 } + assert.ok(pruefePosten([{ ...ok, einzelCent: -500 }], 'regel').fehler.posten) + assert.ok(pruefePosten([{ ...ok, menge: 1001 }], 'regel').fehler.posten) + assert.ok(pruefePosten([{ ...ok, einzelCent: 10_000_001 }], 'regel').fehler.posten) + assert.ok(pruefePosten([{ ...ok, menge: 2 ** 53 }], 'regel').fehler.posten) +}) + +test('preiseVollstaendig: strenge Faelle', () => { + const f = (o) => preiseVollstaendig({ ...PREISE, ...o }) + const st = (ab) => ab.map((a) => ({ abNaechten: a, preisProNachtCent: 100 })) + assert.equal(f({ staffeln: st([1, 1]) }), false) + assert.equal(f({ staffeln: st([1, 0]) }), false) + assert.equal(f({ staffeln: st([1, 2.5]) }), false) + assert.equal(f({ ustUebernachtung: 5 }), false) + assert.equal(f({ ustReinigung: 5 }), false) + assert.equal(f({ reinigungCent: 0, ustReinigung: 5 }), true) + assert.equal(f({ steuerart: 'kleinunternehmer', ustUebernachtung: 5 }), true) + assert.equal(f({ reinigungCent: -1 }), false) + assert.equal(f({ aufpreisPersonNachtCent: 1.5 }), false) + assert.equal(f({ direktrabattProzent: 51 }), false) + assert.equal(f({ direktrabattProzent: -1 }), false) + assert.equal(f({ direktrabattProzent: 2.5 }), false) + assert.equal(f({ direktrabattProzent: 50 }), true) +})