Files
Webklar-Kundenbereich/test/unterkunftRechnungen.test.js

129 lines
7.0 KiB
JavaScript

import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Buchungskalender Task 4: Rechnungsnummern (rechnungen.js) und Token (token.js)
const NAMEN = ['ukUnterkuenfte', 'ukPreise', 'ukSperren', 'ukAnfragen', 'ukRechnungen', 'ukEinstellungen']
const aw = await startAppwriteMock({ schema: Object.fromEntries(NAMEN.map((n) => [n, schemaKeys(n)])) })
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1/v1' })
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const re = await import('../server/services/unterkunft/rechnungen.js')
const tk = await import('../server/services/unterkunft/token.js')
const sp = await import('../server/services/unterkunft/speicher.js')
after(() => aw.close())
const NOW = new Date('2026-11-01T12:00:00Z')
beforeEach(async () => {
for (const c of NAMEN) for (const id of aw.docs(c).keys()) await deleteDocument(c, id)
})
test('Nummern laufen hoch', async () => {
assert.equal(await re.naechsteNummer('f1', NOW), 'RE-2026-0001')
assert.equal(await re.naechsteNummer('f1', NOW), 'RE-2026-0002')
assert.equal(await re.naechsteNummer('f2', NOW), 'RE-2026-0001')
})
test('10 parallele Nummern: verschieden und lueckenlos', async () => {
const nr = await Promise.all(Array.from({ length: 10 }, () => re.naechsteNummer('f1', NOW)))
assert.deepEqual([...nr].sort(), Array.from({ length: 10 }, (_, i) => `RE-2026-${String(i + 1).padStart(4, '0')}`))
})
test('Jahreswechsel nach Berliner Kalenderjahr', async () => {
assert.equal(await re.naechsteNummer('f1', new Date('2026-12-31T22:30:00Z')), 'RE-2026-0001')
assert.equal(await re.naechsteNummer('f1', new Date('2026-12-31T23:30:00Z')), 'RE-2027-0001')
assert.equal(await re.naechsteNummer('f1', new Date('2027-01-01T00:30:00+01:00')), 'RE-2027-0002')
})
test('Praefix aus den Einstellungen', async () => {
await sp.speichereEinstellungen('f1', { rechnungsPraefix: 'DH' })
assert.equal(await re.naechsteNummer('f1', NOW), 'DH-2026-0001')
})
test('stelleAus storno negiert Posten und Summen, traegt bezugNummer', async () => {
const anfrage = {
$id: 'a1', unterkunftId: 'u1', nummer: 'A-ABCD', von: '2026-11-10', bis: '2026-11-12', personen: 2, zahlart: 'vorort',
rechnungsadresse: { name: 'Gast' },
posten: [{ text: 'Nacht', menge: 2, einzelCent: 10000, ustProzent: 7 }],
summen: { bruttoCent: 20000, nettoCent: 18692, ust: [{ satz: 7, nettoCent: 18692, steuerCent: 1308 }] },
}
const ctx = { featureId: 'f1', einstellungen: { firma: 'Haus GmbH', iban: 'DE00' } }
const r = await re.stelleAus(ctx, anfrage, { art: 'rechnung', now: NOW })
assert.equal(r.nummer, 'RE-2026-0001')
assert.equal(r.inhalt.aussteller.firma, 'Haus GmbH')
assert.equal(r.inhalt.naechte, 2)
const s = await re.stelleAus(ctx, anfrage, { art: 'storno', bezugNummer: r.nummer, now: NOW })
assert.equal(s.nummer, 'RE-2026-0002')
assert.equal(s.art, 'storno')
assert.equal(s.inhalt.posten[0].einzelCent, -10000)
assert.equal(s.inhalt.summen.bruttoCent, -20000)
assert.equal(s.inhalt.summen.ust[0].steuerCent, -1308)
assert.equal(s.inhalt.bezugNummer, 'RE-2026-0001')
assert.equal((await re.listeRechnungen('f1')).length, 2)
assert.equal((await re.ladeRechnung(s.$id)).nummer, 'RE-2026-0002')
})
const ANF = {
$id: 'a9', unterkunftId: 'u1', nummer: 'A-WXYZ', von: '2026-11-10', bis: '2026-11-12', personen: 2, zahlart: 'vorort',
rechnungsadresse: { name: 'Gast Neu' },
posten: [{ text: 'Nacht', menge: 2, einzelCent: 10000, ustProzent: 7 }],
summen: { bruttoCent: 20000, nettoCent: 18692, ust: [{ satz: 7, nettoCent: 18692, steuerCent: 1308 }] },
}
test('Rechnungsnummer: scheitert die Dokument-Anlage, bleibt keine Luecke (Zaehler zurueck)', async () => {
const ctx = { featureId: 'f1', einstellungen: { firma: 'Haus GmbH' } }
const r1 = await re.stelleAus(ctx, ANF, { art: 'rechnung', now: NOW })
assert.equal(r1.nummer, 'RE-2026-0001')
aw.failNext({ collection: 'ukRechnungen', method: 'POST', status: 500 })
await assert.rejects(re.stelleAus(ctx, ANF, { art: 'rechnung', now: NOW }))
assert.deepEqual((await sp.ladeEinstellungen('f1')).rechnungZaehler, { jahr: 2026, n: 1 })
const r2 = await re.stelleAus(ctx, ANF, { art: 'rechnung', now: NOW })
assert.equal(r2.nummer, 'RE-2026-0002')
// erste Rechnung ueberhaupt scheitert: Zaehler bleibt leer
const ctx2 = { featureId: 'f2', einstellungen: {} }
aw.failNext({ collection: 'ukRechnungen', method: 'POST', status: 500 })
await assert.rejects(re.stelleAus(ctx2, ANF, { art: 'rechnung', now: NOW }))
assert.ok(!((await sp.ladeEinstellungen('f2'))?.rechnungZaehler?.n > 0))
assert.equal((await re.stelleAus(ctx2, ANF, { art: 'rechnung', now: NOW })).nummer, 'RE-2026-0001')
})
test('Rechnungsnummer: parallele Ausstellung mit einem Fehler dazwischen bleibt lueckenlos', async () => {
const ctx = { featureId: 'f1', einstellungen: {} }
aw.failNext({ collection: 'ukRechnungen', method: 'POST', status: 500 })
const res = await Promise.allSettled(Array.from({ length: 4 }, () => re.stelleAus(ctx, ANF, { art: 'rechnung', now: NOW })))
const nr = res.filter((x) => x.status === 'fulfilled').map((x) => x.value.nummer).sort()
assert.deepEqual(nr, ['RE-2026-0001', 'RE-2026-0002', 'RE-2026-0003'])
})
test('Stornorechnung traegt Aussteller und Empfaenger der Ursprungsrechnung, nicht die heutigen', async () => {
const alt = { featureId: 'f1', einstellungen: { firma: 'Alte Firma', iban: 'DE-ALT', steuernummer: '1/1' } }
const r = await re.stelleAus(alt, ANF, { art: 'rechnung', now: NOW })
const neu = { featureId: 'f1', einstellungen: { firma: 'Neue Firma', iban: 'DE-NEU' } }
const s = await re.stelleAus(neu, { ...ANF, rechnungsadresse: { name: 'Anderer Name' } }, { art: 'storno', bezug: r, now: NOW })
assert.equal(s.inhalt.aussteller.firma, 'Alte Firma')
assert.equal(s.inhalt.aussteller.iban, 'DE-ALT')
assert.deepEqual(s.inhalt.empfaenger, { name: 'Gast Neu' })
assert.equal(s.inhalt.bezugNummer, r.nummer)
})
test('Schema: Rechnungsnummer ist je Feature eindeutig (unique-Index)', async () => {
const { UNTERKUNFT_COLLECTIONS } = await import('../server/services/unterkunft/schema.js')
const i = UNTERKUNFT_COLLECTIONS.ukRechnungen.indexes.find((x) => x.type === 'unique')
assert.deepEqual(i.attributes, ['featureId', 'nummer'])
})
test('Token: Hash passt nur zum Token; Formtoken', () => {
const { token, hash } = tk.neuesToken()
assert.equal(tk.tokenPasst(hash, token), true)
assert.equal(tk.tokenPasst(hash, token + 'x'), false)
assert.equal(tk.tokenPasst('', token), false)
const t = tk.formToken('abc', new Date(0 + 1e12))
assert.equal(tk.pruefeFormToken('abc', t, new Date(1e12 + 10000)), 'ok')
assert.equal(tk.pruefeFormToken('abc', t, new Date(1e12 + 100)), 'zu_schnell')
assert.equal(tk.pruefeFormToken('abd', t, new Date(1e12 + 10000)), 'ungueltig')
assert.equal(tk.pruefeFormToken('abc', t, new Date(1e12 + 3 * 3600e3)), 'abgelaufen')
assert.match(tk.ipHashVon('1.2.3.4'), /^[0-9a-f]{16}$/)
})