Files
Webklar-Kundenbereich/test/unterkunftPreis.test.js
T
knsoandClaude Opus 5.5 a4ade5f649 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>
2026-10-11 15:02:45 +00:00

323 lines
15 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.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { istTag, naechte, plusTage, tageVon, formatTag } from '../server/services/unterkunft/datum.js'
import {
berechnePreis,
summiere,
pruefePosten,
formatEuro,
preiseVollstaendig,
} from '../server/services/unterkunft/preis.js'
const PREISE = {
staffeln: [
{ abNaechten: 1, preisProNachtCent: 6500 },
{ abNaechten: 7, preisProNachtCent: 5500 },
{ abNaechten: 28, preisProNachtCent: 4500 },
],
reinigungCent: 6000,
personenInklusive: 2,
aufpreisPersonNachtCent: 1000,
direktrabattProzent: 10,
steuerart: 'regel',
ustUebernachtung: 7,
ustReinigung: 7,
}
test('datum: istTag, naechte, plusTage, tageVon, formatTag', () => {
assert.equal(istTag('2026-10-24'), true)
assert.equal(istTag('2026-02-30'), false)
assert.equal(istTag('2026-1-24'), false)
assert.equal(istTag(null), false)
assert.equal(naechte('2026-10-24', '2026-10-26'), 2)
assert.equal(naechte('2026-12-30', '2027-01-02'), 3)
assert.equal(plusTage('2026-12-31', 1), '2027-01-01')
assert.equal(plusTage('2026-03-01', -1), '2026-02-28')
assert.deepEqual(tageVon('2026-12-30', '2027-01-02'), ['2026-12-30', '2026-12-31', '2027-01-01'])
assert.equal(formatTag('2026-11-12'), '12.11.2026')
})
test('berechnePreis: 7 Naechte, 2 Personen', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-02', bis: '2026-11-09', personen: 2 })
assert.equal(r.naechte, 7)
assert.deepEqual(r.posten, [
{ text: 'Übernachtung, 7 Nächte', menge: 7, einzelCent: 5500, ustProzent: 7 },
{ text: 'Endreinigung', menge: 1, einzelCent: 6000, ustProzent: 7 },
{ text: 'Direktbuchungsrabatt 10 %', menge: 1, einzelCent: -3850, ustProzent: 7 },
])
assert.equal(r.summen.bruttoCent, 40650)
assert.deepEqual(r.summen.ust, [{ satz: 7, nettoCent: 37991, steuerCent: 2659 }])
})
test('berechnePreis: 30 Naechte, 3 Personen', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-01', bis: '2026-12-01', personen: 3 })
assert.equal(r.posten[0].menge, 30)
assert.equal(r.posten[0].einzelCent, 4500)
assert.equal(r.posten[1].text, 'Zusatzperson (1), 30 Nächte')
assert.equal(r.posten[1].menge, 30)
assert.equal(r.posten[1].einzelCent, 1000)
assert.equal(r.posten[2].einzelCent, 6000)
assert.equal(r.posten[3].einzelCent, -16500)
assert.equal(r.summen.bruttoCent, 154500)
})
test('berechnePreis: 3 Naechte, 2 Personen', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-02', bis: '2026-11-05', personen: 2 })
assert.equal(r.summen.bruttoCent, 23550)
})
test('berechnePreis: Mehrzahl Zusatzpersonen', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-02', bis: '2026-11-05', personen: 4 })
assert.equal(r.posten[1].text, 'Zusatzpersonen (2), 3 Nächte')
assert.equal(r.posten[1].menge, 6)
})
test('berechnePreis: unterschiedliche Saetze', () => {
const r = berechnePreis({
preise: { ...PREISE, ustReinigung: 19 },
von: '2026-11-02',
bis: '2026-11-09',
personen: 2,
})
assert.equal(r.summen.ust.length, 2)
const summe = r.summen.ust.reduce((a, u) => a + u.nettoCent + u.steuerCent, 0)
assert.equal(summe, r.summen.bruttoCent)
})
test('berechnePreis/summiere: Kleinunternehmer', () => {
const r = berechnePreis({
preise: { ...PREISE, steuerart: 'kleinunternehmer' },
von: '2026-11-02',
bis: '2026-11-09',
personen: 2,
})
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', () => {
const r = berechnePreis({
preise: { ...PREISE, direktrabattProzent: 0, reinigungCent: 0 },
von: '2026-11-02',
bis: '2026-11-09',
personen: 2,
})
assert.equal(r.posten.length, 1)
})
test('summiere: direkt', () => {
const s = summiere([{ text: 'a', menge: 2, einzelCent: 500, ustProzent: 19 }], 'regel')
assert.equal(s.bruttoCent, 1000)
assert.deepEqual(s.ust, [{ satz: 19, nettoCent: 840, steuerCent: 160 }])
})
test('pruefePosten', () => {
const ok = { text: ' Miete ', menge: 2, einzelCent: 1000, ustProzent: 7 }
assert.ok(pruefePosten([], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, einzelCent: 0 }], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, text: '' }], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, text: 'x'.repeat(201) }], 'regel').fehler.posten)
assert.ok(pruefePosten(Array(31).fill(ok), 'regel').fehler.posten)
assert.ok(pruefePosten([ok], 'kleinunternehmer').fehler.posten)
assert.ok(pruefePosten([{ ...ok, menge: 1, einzelCent: 10_000_000 }, ...Array(10).fill({ ...ok, menge: 1, einzelCent: 10_000_000 })], 'regel').fehler.posten, 'Summe ueber 999.999,99 EUR')
assert.ok(pruefePosten([{ ...ok, menge: 1, einzelCent: 9_999_999 }], 'regel').posten)
assert.ok(pruefePosten([{ ...ok, ustProzent: 5 }], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, menge: 0 }], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, menge: 1.5 }], 'regel').fehler.posten)
assert.ok(pruefePosten('nope', 'regel').fehler.posten)
const r = pruefePosten([ok], 'regel')
assert.equal(r.posten[0].text, 'Miete')
assert.equal(pruefePosten([{ ...ok, ustProzent: 0 }], 'kleinunternehmer').posten.length, 1)
})
test('formatEuro', () => {
assert.equal(formatEuro(123450), '1.234,50 €')
assert.equal(formatEuro(-3850), '−38,50 €')
assert.equal(formatEuro(5), '0,05 €')
})
test('preiseVollstaendig', () => {
assert.equal(preiseVollstaendig(PREISE), true)
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: 20001 }], 'regel').fehler.posten)
assert.ok(pruefePosten([{ ...ok, menge: 20000 }], 'regel').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)
})
test('berechnePreis: 20 Personen ueber 548 Naechte besteht pruefePosten', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-01', bis: plusTage('2026-11-01', 548), personen: 20 })
assert.equal(r.posten[1].menge, 18 * 548)
assert.ok(pruefePosten(r.posten, 'regel').posten)
})
// ---- Preise je Personenzahl (staffeln[].personenCent): Mimis Monteurwohnung, Preis pro Nacht fuer die ganze Wohnung
const MIMI = {
staffeln: [
{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] },
{ abNaechten: 7, preisProNachtCent: 5000, personenCent: [5000, 6500, 7700] },
{ abNaechten: 14, preisProNachtCent: 4500, personenCent: [4500, 6000, 7200] },
{ abNaechten: 28, preisProNachtCent: 4000, personenCent: [4000, 5500, 6700] },
],
reinigungCent: 4000,
personenInklusive: 2,
aufpreisPersonNachtCent: 1500,
direktrabattProzent: 0,
steuerart: 'kleinunternehmer',
ustUebernachtung: 0,
ustReinigung: 0,
}
const mimi = (n, personen, extra = {}) => berechnePreis({ preise: { ...MIMI, ...extra }, von: '2026-11-01', bis: plusTage('2026-11-01', n), personen })
test('Personenpreise: Mimis Matrix (Naechte x Personen) mit Endreinigung 40 EUR', () => {
const erwartet = [
// [Naechte, Personen, Preis pro Nacht in Cent]
[3, 1, 5500], [3, 2, 7000], [3, 3, 8200],
[6, 3, 8200], [7, 1, 5000], [7, 2, 6500], [7, 3, 7700],
[13, 3, 7700], [14, 1, 4500], [14, 2, 6000], [14, 3, 7200],
[27, 3, 7200], [28, 1, 4000], [28, 2, 5500], [28, 3, 6700], [30, 3, 6700],
]
for (const [n, p, nacht] of erwartet) {
const r = mimi(n, p)
assert.equal(r.posten[0].einzelCent, nacht, `${n} Naechte, ${p} Personen`)
assert.equal(r.posten[0].menge, n)
assert.equal(r.summen.bruttoCent, n * nacht + 4000, `${n} Naechte, ${p} Personen`)
}
assert.equal(mimi(7, 3).summen.bruttoCent, 57900)
assert.equal(mimi(3, 2).summen.bruttoCent, 25000)
assert.equal(mimi(28, 1).summen.bruttoCent, 116000)
assert.equal(mimi(30, 3).summen.bruttoCent, 205000)
})
test('Personenpreise: Postentexte mit Personenzahl, kein Zusatzperson-Posten', () => {
const r = mimi(7, 3)
assert.deepEqual(r.posten, [
{ text: 'Übernachtung, 7 Nächte, 3 Personen', menge: 7, einzelCent: 7700, ustProzent: 0 },
{ text: 'Endreinigung', menge: 1, einzelCent: 4000, ustProzent: 0 },
])
assert.equal(mimi(7, 1).posten[0].text, 'Übernachtung, 7 Nächte, 1 Person')
assert.equal(mimi(7, 2).posten[0].text, 'Übernachtung, 7 Nächte, 2 Personen')
assert.equal(mimi(1, 2).posten[0].text, 'Übernachtung, 1 Nächte, 2 Personen', 'Stil des bisherigen Textes: immer "Nächte"')
assert.ok(pruefePosten(mimi(30, 3).posten, 'kleinunternehmer').posten)
})
test('Personenpreise: personenInklusive wird ignoriert', () => {
const a = mimi(7, 3, { personenInklusive: 1 })
const b = mimi(7, 3, { personenInklusive: 3 })
assert.deepEqual(a, b)
assert.equal(a.posten.length, 2)
})
test('Personenpreise: mehr Gaeste als Preisspalten = letzte Spalte + Zusatzperson-Posten', () => {
const r = mimi(7, 5)
assert.deepEqual(r.posten, [
{ text: 'Übernachtung, 7 Nächte, 3 Personen', menge: 7, einzelCent: 7700, ustProzent: 0 },
{ text: 'Zusatzpersonen (2), 7 Nächte', menge: 14, einzelCent: 1500, ustProzent: 0 },
{ text: 'Endreinigung', menge: 1, einzelCent: 4000, ustProzent: 0 },
])
assert.equal(r.summen.bruttoCent, 7 * 7700 + 14 * 1500 + 4000)
assert.equal(mimi(7, 4).posten[1].text, 'Zusatzperson (1), 7 Nächte')
// ohne Aufpreis kein Zusatzperson-Posten
const ohne = mimi(7, 5, { aufpreisPersonNachtCent: 0 })
assert.equal(ohne.posten.length, 2)
assert.equal(ohne.posten[0].text, 'Übernachtung, 7 Nächte, 3 Personen')
})
test('Personenpreise: Direktrabatt auf Uebernachtung und Zusatzpersonen, nicht auf die Reinigung', () => {
const r = mimi(7, 5, { direktrabattProzent: 10 })
// Basis 7*7700 + 14*1500 = 74900; 10 % = 7490
assert.equal(r.posten.at(-1).text, 'Direktbuchungsrabatt 10 %')
assert.equal(r.posten.at(-1).einzelCent, -7490)
assert.equal(r.summen.bruttoCent, 74900 + 4000 - 7490)
const einfach = mimi(7, 2, { direktrabattProzent: 10 })
assert.equal(einfach.posten.at(-1).einzelCent, -Math.round((7 * 6500 * 10) / 100))
assert.equal(einfach.summen.bruttoCent, 45500 + 4000 - 4550)
})
test('Personenpreise: Regelbesteuerung 7 % Uebernachtung, 19 % Reinigung', () => {
const r = mimi(7, 3, { steuerart: 'regel', ustUebernachtung: 7, ustReinigung: 19 })
assert.deepEqual(r.posten.map((p) => p.ustProzent), [7, 19])
assert.equal(r.summen.bruttoCent, 57900)
assert.deepEqual(r.summen.ust, [
{ satz: 7, nettoCent: Math.round((53900 * 100) / 107), steuerCent: 53900 - Math.round((53900 * 100) / 107) },
{ satz: 19, nettoCent: Math.round((4000 * 100) / 119), steuerCent: 4000 - Math.round((4000 * 100) / 119) },
])
assert.equal(r.summen.ust.reduce((a, u) => a + u.nettoCent + u.steuerCent, 0), 57900)
})
test('Personenpreise: Staffelwahl wie bisher (hoechste abNaechten <= Naechte), ungeordnete Eingabe', () => {
const gemischt = { ...MIMI, staffeln: [...MIMI.staffeln].reverse() }
assert.equal(berechnePreis({ preise: gemischt, von: '2026-11-01', bis: plusTage('2026-11-01', 20), personen: 2 }).posten[0].einzelCent, 6000)
})
test('Personenpreise: Personenzahl kleiner 1 oder keine Zahl rechnet wie 1 Person', () => {
assert.equal(mimi(7, 0).posten[0].einzelCent, 5000)
assert.equal(mimi(7, NaN).posten[0].einzelCent, 5000)
})
test('Alter Modus ohne personenCent rechnet unveraendert (Text ohne Personenangabe)', () => {
const r = berechnePreis({ preise: PREISE, von: '2026-11-02', bis: '2026-11-09', personen: 3 })
assert.deepEqual(r.posten.slice(0, 2), [
{ text: 'Übernachtung, 7 Nächte', menge: 7, einzelCent: 5500, ustProzent: 7 },
{ text: 'Zusatzperson (1), 7 Nächte', menge: 7, einzelCent: 1000, ustProzent: 7 },
])
// personenCent: null zaehlt als nicht vorhanden
const mitNull = { ...PREISE, staffeln: PREISE.staffeln.map((s) => ({ ...s, personenCent: null })) }
assert.deepEqual(berechnePreis({ preise: mitNull, von: '2026-11-02', bis: '2026-11-09', personen: 3 }), r)
})
test('preiseVollstaendig mit personenCent: konsistent ja, sonst nein', () => {
const f = (staffeln) => preiseVollstaendig({ ...MIMI, staffeln })
const st = (o = {}) => MIMI.staffeln.map((s, i) => ({ ...s, ...(typeof o === 'function' ? o(s, i) : o) }))
assert.equal(preiseVollstaendig(MIMI), true)
assert.equal(f(st()), true)
// nur eine Staffel hat personenCent
assert.equal(f(st((s, i) => (i === 2 ? { personenCent: undefined } : {}))), false)
// ungleiche Laengen
assert.equal(f(st((s, i) => (i === 1 ? { personenCent: s.personenCent.slice(0, 2) } : {}))), false)
// Laenge 1 und 11
assert.equal(f(st((s) => ({ personenCent: [s.preisProNachtCent] }))), false)
assert.equal(f(st((s) => ({ personenCent: Array(11).fill(s.preisProNachtCent) }))), false)
assert.equal(f(st((s) => ({ personenCent: Array(10).fill(s.preisProNachtCent) }))), true)
assert.equal(f(st((s) => ({ personenCent: [s.preisProNachtCent, s.preisProNachtCent] }))), true)
// ungueltige Werte
assert.equal(f(st((s, i) => (i === 0 ? { personenCent: [5500, 99, 8200] } : {}))), false)
assert.equal(f(st((s, i) => (i === 0 ? { personenCent: [5500, 100001, 8200] } : {}))), false)
assert.equal(f(st((s, i) => (i === 0 ? { personenCent: [5500, 7000.5, 8200] } : {}))), false)
assert.equal(f(st((s, i) => (i === 0 ? { personenCent: [5500, '7000', 8200] } : {}))), false)
assert.equal(f(st((s, i) => (i === 0 ? { personenCent: 'x' } : {}))), false)
// preisProNachtCent muss personenCent[0] sein
assert.equal(f(st((s, i) => (i === 3 ? { preisProNachtCent: 4100 } : {}))), false)
// null zaehlt als nicht vorhanden
assert.equal(preiseVollstaendig({ ...PREISE, staffeln: PREISE.staffeln.map((s) => ({ ...s, personenCent: null })) }), true)
// alter Modus mit personenCent in nur einer Staffel
assert.equal(preiseVollstaendig({ ...PREISE, staffeln: [{ ...PREISE.staffeln[0], personenCent: [6500, 7500] }, PREISE.staffeln[1], PREISE.staffeln[2]] }), false)
})