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>
This commit is contained in:
1 parent
b264d7f734
commit
a4ade5f649
5 files changed
+379
-12
No files matched your search
@@ -24,7 +24,7 @@ const zahlung = await import('../server/services/unterkunft/zahlung.js')
|
||||
const { UkFehler } = await import('../server/services/unterkunft/fehler.js')
|
||||
const { default: ukRoutes, resetUnterkunftRoutesForTests } = await import('../server/routes/unterkunft.js')
|
||||
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
|
||||
const { pruefeIcalUrl, pruefePreise, pruefeEinstellungen } = await import('../server/services/unterkunft/verwaltung.js')
|
||||
const { pruefeIcalUrl, pruefePreise, pruefeEinstellungen, STAFFELN_MAX_ZEICHEN } = await import('../server/services/unterkunft/verwaltung.js')
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, '/api/unterkunft': ukRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
@@ -257,6 +257,128 @@ test('Preise: erste Staffel abNaechten:2, absteigend, mehr als 6, Preisgrenzen,
|
||||
assert.ok(pruefePreise(PREISE).werte)
|
||||
})
|
||||
|
||||
// Preise je Personenzahl (staffeln[].personenCent)
|
||||
const MIMI_PREISE = {
|
||||
...PREISE,
|
||||
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: 0, direktrabattProzent: 0, steuerart: 'kleinunternehmer',
|
||||
}
|
||||
const mitStaffeln = (staffeln) => pruefePreise({ ...MIMI_PREISE, staffeln })
|
||||
const einzelne = (personenCent, extra = {}) => mitStaffeln([{ abNaechten: 1, preisProNachtCent: personenCent?.[0] ?? 5500, personenCent, ...extra }])
|
||||
|
||||
test('pruefePreise: Preise je Personenzahl gueltig, personenCent wird sauber uebernommen', () => {
|
||||
const r = pruefePreise(MIMI_PREISE)
|
||||
assert.deepEqual(r.fehler, undefined)
|
||||
assert.deepEqual(r.werte.staffeln, MIMI_PREISE.staffeln)
|
||||
assert.equal(r.werte.staffeln[0].personenCent !== MIMI_PREISE.staffeln[0].personenCent, true, 'Kopie, nicht dasselbe Array')
|
||||
// Grenzen: 2 und 10 Werte, 100 und 100000 Cent
|
||||
assert.ok(einzelne([100, 100000]).werte)
|
||||
assert.ok(einzelne(Array(10).fill(100000)).werte)
|
||||
// unbekannte Felder in der Staffel fallen weg
|
||||
const mitMuell = mitStaffeln([{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000], x: 1 }])
|
||||
assert.deepEqual(mitMuell.werte.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000] }])
|
||||
// alter Modus bleibt: kein personenCent im Ergebnis, auch bei null
|
||||
const alt = pruefePreise(PREISE)
|
||||
assert.deepEqual(alt.werte.staffeln, PREISE.staffeln)
|
||||
assert.ok(alt.werte.staffeln.every((s) => !('personenCent' in s)))
|
||||
const mitNull = mitStaffeln([{ abNaechten: 1, preisProNachtCent: 5500, personenCent: null }])
|
||||
assert.deepEqual(mitNull.werte.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500 }])
|
||||
})
|
||||
|
||||
test('pruefePreise: preisProNachtCent wird im Personen-Modus mit personenCent[0] ueberschrieben', () => {
|
||||
const r = einzelne([5500, 7000, 8200], { preisProNachtCent: 9999 })
|
||||
assert.equal(r.werte.staffeln[0].preisProNachtCent, 5500)
|
||||
const ohne = mitStaffeln([{ abNaechten: 1, personenCent: [5500, 7000] }])
|
||||
assert.equal(ohne.werte.staffeln[0].preisProNachtCent, 5500)
|
||||
})
|
||||
|
||||
test('pruefePreise: personenCent ungueltig -> klare Meldung unter staffeln', () => {
|
||||
const faelle = {
|
||||
'Laenge 1': [5500],
|
||||
'Laenge 11': Array(11).fill(5500),
|
||||
'leer': [],
|
||||
'Wert 99': [5500, 99],
|
||||
'Wert 100001': [5500, 100001],
|
||||
'Kommazahl': [5500, 7000.5],
|
||||
'Text': [5500, '7000'],
|
||||
'null': [5500, null],
|
||||
'NaN': [5500, NaN],
|
||||
'kein Array': 'viele',
|
||||
'Objekt': { 0: 5500, 1: 7000 },
|
||||
'Zahl': 5500,
|
||||
}
|
||||
for (const [name, wert] of Object.entries(faelle)) {
|
||||
const r = einzelne(wert)
|
||||
assert.ok(r.fehler?.staffeln, name)
|
||||
assert.ok(/\S{3,}/.test(r.fehler.staffeln), name)
|
||||
assert.equal(Object.keys(r.fehler).length, 1, `${name}: nur das Feld staffeln`)
|
||||
}
|
||||
assert.match(einzelne([5500]).fehler.staffeln, /2 bis 10/)
|
||||
assert.match(einzelne([5500, 99]).fehler.staffeln, /1 € und 1\.000 €/)
|
||||
})
|
||||
|
||||
test('pruefePreise: Staffeln mit unterschiedlich vielen oder teils fehlenden personenCent -> Fehler', () => {
|
||||
const s = (ab, pc) => ({ abNaechten: ab, preisProNachtCent: 5000, ...(pc ? { personenCent: pc } : {}) })
|
||||
assert.match(mitStaffeln([s(1, [5000, 6000, 7000]), s(7, [5000, 6000])]).fehler.staffeln, /gleich viele/)
|
||||
assert.match(mitStaffeln([s(1, [5000, 6000, 7000]), s(7)]).fehler.staffeln, /alle Preisstufen/i)
|
||||
assert.match(mitStaffeln([s(1), s(7, [5000, 6000, 7000])]).fehler.staffeln, /alle Preisstufen/i)
|
||||
assert.ok(mitStaffeln([s(1, [5000, 6000]), s(7, [4500, 5500])]).werte)
|
||||
})
|
||||
|
||||
test('pruefePreise: bestehende Staffel-Regeln gelten auch im Personen-Modus', () => {
|
||||
const s = (ab, pc = [5500, 7000]) => ({ abNaechten: ab, preisProNachtCent: pc[0], personenCent: pc })
|
||||
assert.match(mitStaffeln([s(2)]).fehler.staffeln, /ab 1 Nacht/)
|
||||
assert.match(mitStaffeln([s(1), s(7), s(3)]).fehler.staffeln, /aufsteigend/)
|
||||
assert.match(mitStaffeln([s(1), s(1)]).fehler.staffeln, /aufsteigend/)
|
||||
assert.match(mitStaffeln([1, 2, 3, 4, 5, 6, 7].map((n) => s(n))).fehler.staffeln, /Höchstens 6/)
|
||||
assert.ok(mitStaffeln([1, 2, 3, 4, 5, 6].map((n) => s(n))).werte)
|
||||
assert.match(mitStaffeln([{ abNaechten: 1.5, personenCent: [5500, 7000] }]).fehler.staffeln, /Ungültige Preisstufe/)
|
||||
})
|
||||
|
||||
test('pruefePreise: schlimmster Fall (6 Staffeln x 10 Werte x 100000, ab 365) passt in das Textfeld staffeln (2000 Zeichen)', async () => {
|
||||
const { UNTERKUNFT_COLLECTIONS } = await import('../server/services/unterkunft/schema.js')
|
||||
const groesse = UNTERKUNFT_COLLECTIONS.ukPreise.attributes.find((a) => a.key === 'staffeln').size
|
||||
assert.equal(STAFFELN_MAX_ZEICHEN, groesse, 'Konstante und Schema stimmen ueberein')
|
||||
const staffeln = [360, 361, 362, 363, 364, 365].map((ab, i) => ({ abNaechten: i === 0 ? 1 : ab, preisProNachtCent: 100000, personenCent: Array(10).fill(100000) }))
|
||||
const r = mitStaffeln(staffeln)
|
||||
assert.ok(r.werte, JSON.stringify(r.fehler))
|
||||
assert.ok(JSON.stringify(r.werte.staffeln).length <= groesse, `${JSON.stringify(r.werte.staffeln).length} > ${groesse}`)
|
||||
assert.ok(JSON.stringify(r.werte.staffeln).length < 1200, 'deutlich unter der Grenze')
|
||||
})
|
||||
|
||||
test('Preise (HTTP): Personen-Modus speichern, lesen, Vorschau; preisProNachtCent wird ueberschrieben', async () => {
|
||||
const r = await put('/unterkuenfte/u1/preise', { ...MIMI_PREISE, staffeln: MIMI_PREISE.staffeln.map((s) => ({ ...s, preisProNachtCent: 1234 })) })
|
||||
assert.equal(r.status, 200, JSON.stringify(r.body))
|
||||
assert.deepEqual(r.body.preise.staffeln, MIMI_PREISE.staffeln)
|
||||
assert.deepEqual((await get('/unterkuenfte/u1/preise')).body.preise.staffeln, MIMI_PREISE.staffeln)
|
||||
assert.deepEqual(JSON.parse(aw.docs('ukPreise').get('pr1').staffeln), MIMI_PREISE.staffeln)
|
||||
const v = await post('/preisvorschau', { unterkunftId: 'u1', von: '2026-12-01', bis: '2026-12-08', personen: 3 })
|
||||
assert.equal(v.status, 200)
|
||||
assert.equal(v.body.summen.bruttoCent, 7 * 7700 + 4000)
|
||||
assert.equal(v.body.posten[0].text, 'Übernachtung, 7 Nächte, 3 Personen')
|
||||
// zurueck in den alten Modus: ohne personenCent gespeichert
|
||||
const alt = await put('/unterkuenfte/u1/preise', PREISE)
|
||||
assert.equal(alt.status, 200)
|
||||
assert.deepEqual(JSON.parse(aw.docs('ukPreise').get('pr1').staffeln), PREISE.staffeln)
|
||||
})
|
||||
|
||||
test('Preise (HTTP): ungueltige personenCent -> 400 felder.staffeln, gespeicherte Preise bleiben', async () => {
|
||||
const vorherText = aw.docs('ukPreise').get('pr1').staffeln
|
||||
for (const pc of [[5500], Array(11).fill(5500), [5500, 99], [5500, 100001], [5500, 'x']]) {
|
||||
const r = await put('/unterkuenfte/u1/preise', { ...MIMI_PREISE, staffeln: [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: pc }] })
|
||||
assert.equal(r.status, 400, JSON.stringify(pc))
|
||||
assert.ok(r.body.felder.staffeln, JSON.stringify(pc))
|
||||
}
|
||||
const ungleich = await put('/unterkuenfte/u1/preise', { ...MIMI_PREISE, staffeln: [MIMI_PREISE.staffeln[0], { ...MIMI_PREISE.staffeln[1], personenCent: [5000, 6500] }] })
|
||||
assert.equal(ungleich.status, 400)
|
||||
assert.equal(aw.docs('ukPreise').get('pr1').staffeln, vorherText)
|
||||
})
|
||||
|
||||
test('Preisvorschau: berechnePreis mit gespeicherten Preisen; ohne Preise 409; ungueltige Eingaben 400', async () => {
|
||||
const r = await post('/preisvorschau', { unterkunftId: 'u1', von: '2026-12-01', bis: '2026-12-03', personen: 3 })
|
||||
assert.equal(r.status, 200)
|
||||
@@ -384,6 +506,20 @@ test('Anfragen: Liste mit Filter und ohne Geheimnisse; Detail mit Vorschlag', as
|
||||
assert.equal((await get('/anfragen/nix')).status, 404)
|
||||
})
|
||||
|
||||
test('Anfragen im Personen-Modus: Vorschlag und Freigabe ohne Posten rechnen mit der Spalte der Gaestezahl', async () => {
|
||||
aw.seed('ukPreise', { $id: 'pr1', unterkunftId: 'u1', featureId: FEATURE, ...MIMI_PREISE, staffeln: JSON.stringify(MIMI_PREISE.staffeln) })
|
||||
anfrage('a1', { status: 'neu', von: '2026-11-01', bis: '2026-11-08', personen: 3 })
|
||||
const d = await get('/anfragen/a1')
|
||||
assert.equal(d.status, 200)
|
||||
assert.deepEqual(d.body.vorschlag.posten.map((p) => [p.text, p.menge, p.einzelCent]), [['Übernachtung, 7 Nächte, 3 Personen', 7, 7700], ['Endreinigung', 1, 4000]])
|
||||
assert.equal(d.body.vorschlag.summen.bruttoCent, 57900)
|
||||
// Freigabe ohne eigene Posten uebernimmt die berechneten
|
||||
const f = await post('/anfragen/a1/freigeben', {})
|
||||
assert.equal(f.status, 200, JSON.stringify(f.body))
|
||||
assert.equal(f.body.anfrage.summen.bruttoCent, 57900)
|
||||
assert.equal(f.body.anfrage.posten[0].text, 'Übernachtung, 7 Nächte, 3 Personen')
|
||||
})
|
||||
|
||||
test('Freigeben: Posten + Frist, Antwort mit buchungsLink (Token passt zum Hash), nie tokenHash; Anfrage freigegeben', async () => {
|
||||
anfrage('a1')
|
||||
const r = await post('/anfragen/a1/freigeben', { posten: POSTEN, fristTage: 5, nachricht: 'Bis bald' })
|
||||
|
||||
Reference in new issue
Block a user