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

553 lines
26 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,
forecastRows,
forecastNotes,
taxView,
aboView,
productLabel,
aboStatus,
} = 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)
}
})
// --- Vorschau 30 Tage (Block 3) -----------------------------------------------------------------------------
const posten = () => [
{ date: '2026-10-03', label: 'Rechnung WK-0005 · Kunde Eins', amountCents: 16879, sicherheit: 'erwartet' },
{ date: '2026-10-06', label: 'Hetzner', amountCents: -1000, sicherheit: 'sicher' },
{ date: '2026-10-10', label: 'Stripe-Auszahlung', amountCents: 2500, sicherheit: 'sicher' },
{ date: '2026-10-11', label: 'Miete', amountCents: -30000, sicherheit: 'sicher' },
]
test('forecastRows: laufende Salden, sicher zählt nur sichere Posten, erwartet alle', () => {
const { rows, hasBalance } = forecastRows(posten(), 10893)
assert.equal(hasBalance, true)
assert.deepEqual(rows.map((r) => r.saldoSicherCents), [10893, 9893, 12393, -17607])
assert.deepEqual(rows.map((r) => r.saldoErwartetCents), [27772, 26772, 29272, -728])
})
test('forecastRows: Texte (Datum TT.MM.JJJJ, Betrag mit Vorzeichen, Salden mit U+2212)', () => {
const { rows } = forecastRows(posten(), 10893)
assert.equal(rows[0].dateText, '03.10.2026')
assert.equal(rows[0].label, 'Rechnung WK-0005 · Kunde Eins')
assert.equal(rows[0].sicherheit, 'erwartet')
assert.equal(rows[0].amountText, '+168,79 €')
assert.equal(rows[0].saldoSicherText, '108,93 €')
assert.equal(rows[0].saldoErwartetText, '277,72 €')
assert.equal(rows[1].sicherheit, 'sicher')
assert.equal(rows[1].amountText, `${MINUS}10,00 €`)
assert.equal(rows[3].amountText, `${MINUS}300,00 €`)
assert.equal(rows[3].saldoSicherText, `${MINUS}176,07 €`)
assert.equal(rows[3].saldoErwartetText, `${MINUS}7,28 €`)
assert.equal(rows[3].saldoSicherNegative, true)
assert.equal(rows[0].saldoSicherNegative, false)
})
test('forecastRows: der tiefste laufende Saldo passt zu minSicher/minErwartet des Servers', () => {
const { rows } = forecastRows(posten(), 10893)
assert.equal(Math.min(10893, ...rows.map((r) => r.saldoSicherCents)), -17607)
assert.equal(Math.min(10893, ...rows.map((r) => r.saldoErwartetCents)), -728)
})
test('forecastRows: ohne Kontostand keine Salden', () => {
for (const none of [null, undefined, NaN]) {
const { rows, hasBalance } = forecastRows(posten(), none)
assert.equal(hasBalance, false)
assert.equal(rows.length, 4)
assert.ok(rows.every((r) => r.saldoSicherCents === null && r.saldoErwartetCents === null))
assert.ok(rows.every((r) => r.saldoSicherText === '–' && r.saldoErwartetText === '–'))
}
})
test('forecastRows: Kontostand 0 ist ein Kontostand', () => {
const { rows, hasBalance } = forecastRows(posten().slice(1, 2), 0)
assert.equal(hasBalance, true)
assert.deepEqual(rows.map((r) => r.saldoSicherCents), [-1000])
})
test('forecastRows: leer, kaputte Einträge und unbekannte Sicherheit (zählt als erwartet)', () => {
for (const none of [null, undefined, [], 'x', {}]) assert.deepEqual(forecastRows(none, 100).rows, [])
const { rows } = forecastRows(
[
null,
{ date: '2026-10-03', label: 'ohne Betrag', amountCents: null, sicherheit: 'sicher' },
{ date: '2026-10-04', label: ' ', amountCents: 500, sicherheit: 'irgendwas' },
],
1000
)
assert.equal(rows.length, 1)
assert.equal(rows[0].label, 'Zahlung')
assert.equal(rows[0].sicherheit, 'erwartet')
assert.equal(rows[0].saldoSicherCents, 1000)
assert.equal(rows[0].saldoErwartetCents, 1500)
})
test('forecastRows: Serverreihenfolge bleibt, Schlüssel sind eindeutig', () => {
const same = { date: '2026-10-03', label: 'Hetzner', amountCents: -100, sicherheit: 'sicher' }
const { rows } = forecastRows([same, same], 500)
assert.deepEqual(rows.map((r) => r.saldoSicherCents), [400, 300])
assert.notEqual(rows[0].key, rows[1].key)
})
test('forecastNotes: Hinweise zu Kontostand, Stripe und Abos', () => {
assert.deepEqual(forecastNotes({ kontostandCents: 100, stripeDown: false, abosFehler: false, hasRows: true }), [])
const all = forecastNotes({ kontostandCents: null, stripeDown: true, abosFehler: true, hasRows: true })
assert.deepEqual(all.map((n) => n.key), ['kontostand', 'stripe', 'abos'])
assert.equal(all[2].text, 'Abo-Verlängerungen fehlen, weil die Abos nicht geladen werden konnten.')
assert.match(all[0].text, /Ohne Kontostand/)
assert.match(all[1].text, /Stripe/)
// ohne Zeilen gibt es nichts zu saldieren
assert.deepEqual(forecastNotes({ kontostandCents: null, stripeDown: false, abosFehler: false, hasRows: false }), [])
assert.deepEqual(forecastNotes({ kontostandCents: null, stripeDown: false, abosFehler: true, hasRows: false }).map((n) => n.key), ['abos'])
assert.deepEqual(forecastNotes({}).map((n) => n.key), [])
})
// --- Steuer (Block 4) ---------------------------------------------------------------------------------------
const steuer = (extra = {}, klein = {}) => ({
jahr: 2026,
einnahmenCents: 16879,
ausgabenCents: 5986,
gewinnCents: 10893,
ruecklageCents: 3268,
prozent: 30,
gewerbesteuerHinweis: false,
kleinunternehmer: {
jahrCents: 8000,
vorjahrCents: null,
grenzeFolgejahrCents: 2500000,
grenzeHartCents: 10000000,
anteilFolgejahr: 0.0032,
...klein,
},
naechsteVorauszahlung: null,
...extra,
})
test('taxView: Rücklage mit Prozentsatz, Gewinn und Jahr aus den echten Werten', () => {
const v = taxView(steuer())
assert.equal(v.reserve.amountText, '32,68 €')
assert.equal(v.reserve.basisText, '30 % von 108,93 € Gewinn 2026')
const other = taxView(steuer({ jahr: 2027, prozent: 27.5, gewinnCents: 123456789, ruecklageCents: 100 }))
assert.equal(other.reserve.basisText, '27,5 % von 1.234.567,89 € Gewinn 2027')
})
test('taxView: Verlust zeigt den negativen Gewinn mit U+2212', () => {
const v = taxView(steuer({ gewinnCents: -5000, ruecklageCents: 0 }))
assert.equal(v.reserve.amountText, '0,00 €')
assert.equal(v.reserve.basisText, `30 % von ${MINUS}50,00 € Gewinn 2026`)
})
test('taxView: Kleinunternehmer-Zähler „<Betrag> von 25.000 € (2026)“ und harte Grenze', () => {
const v = taxView(steuer())
assert.equal(v.small.known, true)
assert.equal(v.small.counterText, '80,00 € von 25.000 € (2026)')
assert.equal(v.small.shareText, '0,3 % der Grenze')
assert.equal(v.small.hardText, 'Grenze 100.000 €: darüber endet die Regelung sofort.')
assert.equal(v.small.previousText, null)
assert.equal(taxView(steuer({}, { anteilFolgejahr: 0.704 })).small.shareText, '70,4 % der Grenze')
assert.equal(taxView(steuer({}, { jahrCents: 0, anteilFolgejahr: 0 })).small.counterText, '0,00 € von 25.000 € (2026)')
})
test('taxView: Stripe nicht erreichbar -> unbekannt statt 0', () => {
const v = taxView(steuer({}, { jahrCents: null, anteilFolgejahr: null }))
assert.equal(v.small.known, false)
assert.equal(v.small.counterText, 'unbekannt (Stripe nicht erreichbar)')
assert.equal(v.small.shareText, null)
assert.equal(v.small.hardText, 'Grenze 100.000 €: darüber endet die Regelung sofort.')
// nur der Zähler ist unbekannt, der Rest der Karte bleibt
assert.equal(v.reserve.amountText, '32,68 €')
})
test('taxView: Vorjahr nur wenn gesetzt, über der Grenze mit Hinweis', () => {
assert.equal(taxView(steuer({}, { vorjahrCents: 1200000 })).small.previousText, 'Vorjahr: 12.000,00 €')
assert.equal(taxView(steuer({}, { vorjahrCents: 0 })).small.previousText, 'Vorjahr: 0,00 €')
assert.equal(taxView(steuer({}, { vorjahrCents: 2500000 })).small.previousText, 'Vorjahr: 25.000,00 €')
assert.equal(
taxView(steuer({}, { vorjahrCents: 2500001 })).small.previousText,
'Vorjahr: 25.000,01 € (über der Grenze von 25.000 €)'
)
})
test('taxView: Gewerbesteuer-Hinweis nur mit gewerbesteuerHinweis', () => {
assert.equal(taxView(steuer()).gewerbeText, null)
const t = taxView(steuer({ gewerbesteuerHinweis: true })).gewerbeText
assert.match(t, /Gewerbesteuer/)
assert.match(t, /24\.500 €/)
})
test('taxView: nächste ESt-Vorauszahlung nur wenn gesetzt', () => {
assert.equal(taxView(steuer()).prepayment, null)
assert.deepEqual(taxView(steuer({ naechsteVorauszahlung: { date: '2026-12-10', amountCents: 100000 } })).prepayment, {
dateText: '10.12.2026',
amountText: '1.000,00 €',
})
})
test('taxView: fehlende Teile werden nicht zu Müll', () => {
const v = taxView(null)
assert.equal(v.reserve.amountText, '–')
assert.equal(v.reserve.basisText, '')
assert.equal(v.small.known, false)
assert.equal(v.gewerbeText, null)
assert.equal(v.prepayment, null)
// ohne Grenzen aus dem Server gelten die gesetzlichen Werte
const bare = taxView({ jahr: 2026, kleinunternehmer: { jahrCents: 100 } })
assert.equal(bare.small.counterText, '1,00 € von 25.000 € (2026)')
assert.equal(bare.small.hardText, 'Grenze 100.000 €: darüber endet die Regelung sofort.')
})
// --- Abos (Block 5) -----------------------------------------------------------------------------------------
const abos = (extra = {}) => ({
monatlichCents: 7000,
zahlendeKunden: 1,
gefaehrdetCents: 5000,
liste: [
{ customerId: 'c2', kunde: 'Zwei GmbH', produkt: 'wartung', status: 'past_due', betragCents: 5000, naechsteAbbuchung: null },
{ customerId: 'c1', kunde: 'Kunde Eins', produkt: 'hosting', status: 'active', betragCents: 2000, naechsteAbbuchung: '2026-10-21' },
],
...extra,
})
test('aboView: Kennzahlen und Liste', () => {
const v = aboView(abos())
assert.equal(v.error, '')
assert.equal(v.monthlyText, '70,00 €')
assert.equal(v.payingText, '1')
assert.equal(v.atRiskText, '50,00 €')
assert.equal(v.rows.length, 2)
assert.deepEqual(
{ ...v.rows[0], key: undefined },
{
key: undefined,
customer: 'Zwei GmbH',
product: 'Wartungs-Abo',
statusLabel: 'Zahlung fehlgeschlagen',
tone: 'danger',
amountText: '50,00 €',
nextText: '–',
}
)
assert.deepEqual(
{ ...v.rows[1], key: undefined },
{ key: undefined, customer: 'Kunde Eins', product: 'Hosting-Abo', statusLabel: 'aktiv', tone: 'ok', amountText: '20,00 €', nextText: '21.10.2026' }
)
assert.notEqual(v.rows[0].key, v.rows[1].key)
})
test('aboView: „gefährdet“ nur über 0', () => {
assert.equal(aboView(abos({ gefaehrdetCents: 0 })).atRiskText, null)
assert.equal(aboView(abos({ gefaehrdetCents: null })).atRiskText, null)
assert.equal(aboView(abos({ gefaehrdetCents: 1 })).atRiskText, '0,01 €')
})
test('aboView: Ladefehler zeigt den Text des Servers, keine Zahlen', () => {
const v = aboView({ monatlichCents: null, zahlendeKunden: null, gefaehrdetCents: null, liste: [], fehler: 'Abos konnten nicht geladen werden.' })
assert.equal(v.error, 'Abos konnten nicht geladen werden.')
assert.equal(v.monthlyText, '–')
assert.equal(v.payingText, '–')
assert.equal(v.atRiskText, null)
assert.deepEqual(v.rows, [])
// leerer Text -> Standardtext
assert.equal(aboView({ fehler: true, liste: [] }).error, 'Abos konnten nicht geladen werden.')
})
test('aboView: leer oder fehlend', () => {
for (const none of [null, undefined, {}, { liste: [] }, { liste: 'x' }]) {
const v = aboView(none)
assert.deepEqual(v.rows, [])
assert.equal(v.error, '')
}
assert.equal(aboView({ monatlichCents: 0, zahlendeKunden: 0, gefaehrdetCents: 0, liste: [] }).monthlyText, '0,00 €')
})
test('aboView: unbekannter Kunde, unbekanntes Produkt und unbekannter Status', () => {
const v = aboView({
liste: [
{ customerId: '', kunde: '', produkt: '', status: 'trialing', betragCents: 100, naechsteAbbuchung: '2026-11-01' },
{ customerId: 'c3', kunde: ' Drei ', produkt: 'sonst', status: 'unpaid', betragCents: null, naechsteAbbuchung: 'kaputt' },
{ customerId: 'c4', kunde: 'Vier', produkt: 'hosting', status: 'paused', betragCents: 0, naechsteAbbuchung: null },
null,
],
})
assert.equal(v.rows.length, 3)
assert.equal(v.rows[0].customer, 'Unbekannter Kunde')
assert.equal(v.rows[0].product, 'Abo')
assert.equal(v.rows[0].statusLabel, 'Testphase')
assert.equal(v.rows[0].tone, 'info')
assert.equal(v.rows[1].customer, 'Drei')
assert.equal(v.rows[1].product, 'sonst')
assert.equal(v.rows[1].statusLabel, 'unbezahlt')
assert.equal(v.rows[1].tone, 'danger')
assert.equal(v.rows[1].amountText, '–')
assert.equal(v.rows[1].nextText, '–')
assert.equal(v.rows[2].statusLabel, 'paused')
assert.equal(v.rows[2].tone, 'muted')
})
test('productLabel und aboStatus', () => {
assert.equal(productLabel('hosting'), 'Hosting-Abo')
assert.equal(productLabel('wartung'), 'Wartungs-Abo')
assert.equal(productLabel(''), 'Abo')
assert.equal(productLabel(undefined), 'Abo')
assert.equal(productLabel('x_y'), 'x_y')
assert.deepEqual(aboStatus('active'), { label: 'aktiv', tone: 'ok' })
assert.deepEqual(aboStatus('past_due'), { label: 'Zahlung fehlgeschlagen', tone: 'danger' })
assert.deepEqual(aboStatus('weird'), { label: 'weird', tone: 'muted' })
assert.deepEqual(aboStatus(''), { label: 'unbekannt', tone: 'muted' })
})