Files
tickte-system/tests/unit/financeView.test.mjs
T
2026-10-01 15:12:18 +00:00

259 lines
13 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 { loadModule } from './load.mjs'
const {
INVOICES_ANCHOR,
TODO_LIMIT,
bankStatus,
stripeStatus,
statusItems,
breakdownRows,
lowPointLines,
visibleTodos,
urgencyOf,
todoKey,
safeUrl,
todoButtonLabel,
fixedCostFromSuggestion,
planTodoAction,
} = await loadModule('src/lib/financeView.js')
const MINUS = '−'
const NOW = Date.parse('2026-10-01T13:55:21.691Z') // 15:55 Berliner Zeit
const bank = (extra = {}) => ({
status: 'aktiv',
ibanMasked: 'DE00 •••• 9313',
validUntil: '2027-03-29T00:00:00.000Z',
lastSyncAt: '2026-10-01T10:55:21.691Z',
lastSyncOk: true,
lastError: '',
balanceBookedCents: 10893,
balanceAvailableCents: 9999,
balanceDate: '2026-10-01',
latestBookingDate: '2026-09-29',
...extra,
})
// --- Statuszeile ---------------------------------------------------------------------------------------------
test('bankStatus: Stand, jüngste Buchung und Zugang bis (Spec Block 0)', () => {
assert.deepEqual(bankStatus(bank(), NOW), {
ok: true,
text: 'Bank: Stand vor 3 Std. · jüngste Buchung 29.09.2026 · Zugang bis 29.03.2027',
})
})
test('bankStatus: ohne Verbindung', () => {
for (const none of [null, undefined, 'x']) {
assert.deepEqual(bankStatus(none, NOW), { ok: false, text: 'Bank: nicht verbunden' })
}
})
test('bankStatus: Abruf fehlgeschlagen zeigt den Fehler und weiter den Zugang', () => {
const r = bankStatus(bank({ lastSyncOk: false, lastError: 'Bank antwortet nicht' }), NOW)
assert.equal(r.ok, false)
assert.equal(r.text, 'Bank: Abruf fehlgeschlagen: Bank antwortet nicht · Zugang bis 29.03.2027')
assert.equal(bankStatus(bank({ lastSyncOk: false, lastError: '' }), NOW).text, 'Bank: Abruf fehlgeschlagen · Zugang bis 29.03.2027')
assert.equal(bankStatus(bank({ status: 'fehler', lastError: 'x' }), NOW).ok, false)
})
test('bankStatus: abgelaufener Zugang (Status oder Datum)', () => {
assert.deepEqual(bankStatus(bank({ status: 'abgelaufen' }), NOW), { ok: false, text: 'Bank: Zugang abgelaufen am 29.03.2027' })
const past = bankStatus(bank({ validUntil: '2026-09-30T00:00:00.000Z' }), NOW)
assert.deepEqual(past, { ok: false, text: 'Bank: Zugang abgelaufen am 30.09.2026' })
// am letzten Tag ist der Zugang noch gültig
assert.equal(bankStatus(bank({ validUntil: '2026-10-01T00:00:00.000Z' }), NOW).ok, true)
})
test('bankStatus: noch kein Abruf, keine Buchung, kein Datum', () => {
const r = bankStatus(bank({ lastSyncAt: null, latestBookingDate: null, validUntil: null }), NOW)
assert.deepEqual(r, { ok: true, text: 'Bank: noch kein Abruf' })
})
test('stripeStatus: Stand und Ausfall mit dem Text des Servers', () => {
assert.deepEqual(stripeStatus({ ok: true, abgerufenUm: '2026-10-01T13:55:00.000Z' }, NOW), { ok: true, text: 'Stripe: Stand gerade eben' })
assert.deepEqual(stripeStatus({ ok: false, fehler: 'Stripe ist gerade nicht erreichbar.', abgerufenUm: null }, NOW), {
ok: false,
text: 'Stripe: Abruf fehlgeschlagen: Stripe ist gerade nicht erreichbar.',
})
assert.equal(stripeStatus({ ok: false }, NOW).text, 'Stripe: Abruf fehlgeschlagen: Stripe ist gerade nicht erreichbar.')
assert.deepEqual(stripeStatus({ ok: true, abgerufenUm: null }, NOW), { ok: true, text: 'Stripe: Stand unbekannt' })
assert.deepEqual(stripeStatus(null, NOW), { ok: true, text: 'Stripe: Stand unbekannt' })
})
test('statusItems: Bank und Stripe, fehlender Stand ist kein Fehler im Code', () => {
const items = statusItems({ bank: null, stripe: { ok: true, abgerufenUm: '2026-10-01T13:00:00.000Z' } }, NOW)
assert.deepEqual(items.map((i) => i.key), ['bank', 'stripe'])
assert.equal(items[0].text, 'Bank: nicht verbunden')
assert.equal(items[1].text, 'Stripe: Stand vor 55 Min.')
assert.equal(statusItems(undefined, NOW).length, 2)
})
// --- Liquiditätskopf -----------------------------------------------------------------------------------------
test('breakdownRows: Kontostand minus Rücklage, Fixkosten und Puffer ergibt Verfügbar', () => {
const rows = breakdownRows({ kontostandCents: 10893, steuerRuecklageCents: 3268, fixkosten30Cents: 1449, mindestpufferCents: 2000 }, 4176)
assert.deepEqual(
rows.map((r) => [r.label, r.text]),
[
['Kontostand (gebucht)', '108,93 €'],
['Steuer-Rücklage', `${MINUS}32,68 €`],
['Fixkosten der nächsten 30 Tage', `${MINUS}14,49 €`],
['Mindestpuffer', `${MINUS}20,00 €`],
['Verfügbar nach Rücklagen', '41,76 €'],
]
)
assert.deepEqual(rows.map((r) => r.total), [false, false, false, false, true])
})
test('breakdownRows: Puffer 0 ohne Vorzeichen, negatives Ergebnis, fehlende Werte', () => {
const rows = breakdownRows({ kontostandCents: 1000, steuerRuecklageCents: 0, fixkosten30Cents: 5000, mindestpufferCents: 0 }, -4000)
assert.equal(rows[1].text, '0,00 €')
assert.equal(rows[4].text, `${MINUS}40,00 €`)
const none = breakdownRows(null, null)
assert.deepEqual(none.map((r) => r.text), ['–', '–', '–', '–', '–'])
})
test('lowPointLines: sicher und erwartet mit ganzen Euro und Datum', () => {
const [sicher, erwartet] = lowPointLines({
tiefsterSicher: { amountCents: 9444, date: '2026-10-11' },
tiefsterErwartet: { amountCents: 10893, date: '2026-10-01' },
})
assert.deepEqual(sicher, { key: 'sicher', label: 'sicher', amountText: '94 €', dateText: '11.10.2026', negative: false })
assert.deepEqual(erwartet, { key: 'erwartet', label: 'erwartet', amountText: '109 €', dateText: '01.10.2026', negative: false })
})
test('lowPointLines: negativ mit Minus U+2212, ohne Kontostand null', () => {
const [sicher] = lowPointLines({ tiefsterSicher: { amountCents: -12000, date: '2026-10-20' }, tiefsterErwartet: null })
assert.equal(sicher.amountText, `${MINUS}120 €`)
assert.equal(sicher.negative, true)
assert.deepEqual(lowPointLines({ tiefsterSicher: null, tiefsterErwartet: null }), [null, null])
assert.deepEqual(lowPointLines(undefined), [null, null])
})
// --- Zu erledigen --------------------------------------------------------------------------------------------
const todos = (n) => Array.from({ length: n }, (_, i) => ({ art: 'abo-zahlung', dringlichkeit: 'hoch', titel: `T${i}`, text: '', datum: '2026-10-01', aktion: { typ: 'kunde-oeffnen', customerId: `c${i}` } }))
test('visibleTodos: höchstens 7, Rest hinter „Alle N anzeigen“', () => {
assert.equal(TODO_LIMIT, 7)
const few = visibleTodos(todos(7))
assert.equal(few.shown.length, 7)
assert.equal(few.hidden, 0)
const many = visibleTodos(todos(12))
assert.equal(many.shown.length, 7)
assert.equal(many.total, 12)
assert.equal(many.hidden, 5)
assert.deepEqual(many.shown.map((t) => t.titel), ['T0', 'T1', 'T2', 'T3', 'T4', 'T5', 'T6'])
const all = visibleTodos(todos(12), true)
assert.equal(all.shown.length, 12)
assert.equal(all.hidden, 0)
})
test('visibleTodos: leer, ungültig, Nicht-Objekte werden ausgelassen', () => {
assert.deepEqual(visibleTodos([]), { shown: [], total: 0, hidden: 0 })
assert.deepEqual(visibleTodos(null), { shown: [], total: 0, hidden: 0 })
assert.deepEqual(visibleTodos(undefined), { shown: [], total: 0, hidden: 0 })
assert.equal(visibleTodos([null, 'x', 3, todos(1)[0]]).total, 1)
})
test('urgencyOf: Badge-Text und Ton, unbekannt gilt als Hinweis', () => {
assert.deepEqual(urgencyOf({ dringlichkeit: 'hoch' }), { label: 'Dringend', tone: 'danger' })
assert.deepEqual(urgencyOf({ dringlichkeit: 'mittel' }), { label: 'Bald', tone: 'warn' })
assert.deepEqual(urgencyOf({ dringlichkeit: 'info' }), { label: 'Hinweis', tone: 'muted' })
assert.deepEqual(urgencyOf({ dringlichkeit: 'x' }), { label: 'Hinweis', tone: 'muted' })
assert.deepEqual(urgencyOf(null), { label: 'Hinweis', tone: 'muted' })
})
test('todoKey: unterscheidet Aufgaben auch bei gleichem Titel', () => {
const [a, b] = todos(2)
assert.notEqual(todoKey(a, 0), todoKey(b, 1))
assert.notEqual(todoKey({ ...a }, 0), todoKey({ ...a }, 1))
assert.equal(typeof todoKey(null), 'string')
})
test('safeUrl: nur http(s)', () => {
assert.equal(safeUrl('https://invoice.stripe.com/i/acct_1/test_x'), 'https://invoice.stripe.com/i/acct_1/test_x')
assert.equal(safeUrl(' http://example.test/a '), 'http://example.test/a')
for (const bad of ['javascript:alert(1)', 'data:text/html,x', 'ftp://x.test', 'invoice.stripe.com/i', '', ' ', null, undefined, 5]) {
assert.equal(safeUrl(bad), '', String(bad))
}
})
test('todoButtonLabel: ein Knopf je Aktion', () => {
const label = (aktion) => todoButtonLabel({ aktion })
assert.equal(label({ typ: 'bank-verbinden' }), 'Bank verbinden')
assert.equal(label({ typ: 'bank-aktualisieren' }), 'Aktualisieren')
assert.equal(label({ typ: 'rechnung-oeffnen', hostedUrl: 'https://x.test/i' }), 'Zahlungsseite öffnen')
assert.equal(label({ typ: 'rechnung-oeffnen', hostedUrl: null }), 'Zu den Rechnungen')
assert.equal(label({ typ: 'rechnung-oeffnen', hostedUrl: 'javascript:alert(1)' }), 'Zu den Rechnungen')
assert.equal(label({ typ: 'zuordnen' }), 'Zuordnen')
assert.equal(label({ typ: 'kunde-oeffnen' }), 'Kunde öffnen')
assert.equal(label({ typ: 'fixkosten-uebernehmen' }), 'Übernehmen')
assert.equal(label({ typ: 'einstellungen' }), 'Einstellungen')
assert.equal(label({ typ: 'neu' }), '')
assert.equal(todoButtonLabel({}), '')
assert.equal(todoButtonLabel(null), '')
})
const suggestion = (extra = {}) => ({ key: 'hetzner', name: 'Hetzner', amountCents: 449, interval: 'monat', lastDate: '2026-09-27', count: 4, ...extra })
test('fixedCostFromSuggestion: Anfrage mit nächstem Termin und Rückfrage-Text', () => {
assert.deepEqual(fixedCostFromSuggestion(suggestion()), {
payload: { name: 'Hetzner', amountCents: 449, interval: 'monat', nextDate: '2026-10-27', counterpartyMatch: 'hetzner' },
confirmText: 'Als Fixkosten übernehmen: Hetzner, 4,49 € monatlich?',
})
const q = fixedCostFromSuggestion(suggestion({ interval: 'quartal', lastDate: '2026-11-30', amountCents: 123456 }))
assert.equal(q.payload.nextDate, '2027-02-28')
assert.equal(q.confirmText, 'Als Fixkosten übernehmen: Hetzner, 1.234,56 € vierteljährlich?')
assert.equal(fixedCostFromSuggestion(suggestion({ interval: 'jahr' })).confirmText, 'Als Fixkosten übernehmen: Hetzner, 4,49 € jährlich?')
})
test('fixedCostFromSuggestion: unvollständig ergibt null', () => {
assert.equal(fixedCostFromSuggestion(null), null)
assert.equal(fixedCostFromSuggestion({}), null)
assert.equal(fixedCostFromSuggestion(suggestion({ name: ' ' })), null)
assert.equal(fixedCostFromSuggestion(suggestion({ amountCents: 0 })), null)
assert.equal(fixedCostFromSuggestion(suggestion({ amountCents: 4.5 })), null)
assert.equal(fixedCostFromSuggestion(suggestion({ interval: 'woche' })), null)
assert.equal(fixedCostFromSuggestion(suggestion({ lastDate: 'Mai' })), null)
assert.equal(fixedCostFromSuggestion(suggestion({ key: undefined })).payload.counterpartyMatch, '')
})
test('planTodoAction: jede Aktionsart des Portals', () => {
assert.deepEqual(planTodoAction({ typ: 'bank-verbinden' }), { kind: 'bank-connect' })
assert.deepEqual(planTodoAction({ typ: 'bank-aktualisieren' }), { kind: 'bank-refresh' })
assert.deepEqual(planTodoAction({ typ: 'rechnung-oeffnen', invoiceId: 'in_1', hostedUrl: 'https://x.test/i' }), { kind: 'open-url', url: 'https://x.test/i' })
assert.deepEqual(planTodoAction({ typ: 'rechnung-oeffnen', invoiceId: 'in_1', hostedUrl: null }), { kind: 'scroll', id: 'finanzen-rechnungen' })
assert.equal(INVOICES_ANCHOR, 'finanzen-rechnungen')
assert.deepEqual(planTodoAction({ typ: 'rechnung-oeffnen', hostedUrl: 'javascript:alert(1)' }), { kind: 'scroll', id: 'finanzen-rechnungen' })
assert.deepEqual(planTodoAction({ typ: 'zuordnen', transactionId: 'b1' }), { kind: 'assign', transactionId: 'b1' })
assert.deepEqual(planTodoAction({ typ: 'kunde-oeffnen', customerId: 'c2' }), { kind: 'navigate', to: '/customers/c2' })
assert.deepEqual(planTodoAction({ typ: 'kunde-oeffnen', customerId: 'a/b c' }), { kind: 'navigate', to: '/customers/a%2Fb%20c' })
assert.deepEqual(planTodoAction({ typ: 'einstellungen' }), { kind: 'navigate', to: '/finance/einstellungen' })
const fixed = planTodoAction({ typ: 'fixkosten-uebernehmen', vorschlag: suggestion() })
assert.equal(fixed.kind, 'fixed-cost')
assert.equal(fixed.payload.nextDate, '2026-10-27')
assert.equal(fixed.confirmText, 'Als Fixkosten übernehmen: Hetzner, 4,49 € monatlich?')
})
test('planTodoAction: fehlende Angaben und unbekannte Aktionen geben eine Meldung', () => {
for (const aktion of [
{ typ: 'zuordnen' },
{ typ: 'zuordnen', transactionId: '' },
{ typ: 'kunde-oeffnen' },
{ typ: 'kunde-oeffnen', customerId: ' ' },
{ typ: 'fixkosten-uebernehmen' },
{ typ: 'fixkosten-uebernehmen', vorschlag: suggestion({ amountCents: -1 }) },
{ typ: 'unbekannt' },
null,
undefined,
]) {
const plan = planTodoAction(aktion)
assert.equal(plan.kind, 'error', JSON.stringify(aktion))
assert.ok(plan.message.length > 0)
}
})