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) })