129 lines
7.0 KiB
JavaScript
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}$/)
|
|
})
|