Files
Webklar-Kundenbereich/test/financeTodos.test.js
T
2026-10-01 15:42:06 +00:00

454 lines
24 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 { buildTodos } from '../server/services/finance/todos.js'
import { addDays } from '../server/services/finance/dates.js'
import { euroDe } from '../server/services/finance/format.js'
const TODAY = '2026-10-01'
const NOW = new Date('2026-10-01T10:00:00.000Z')
const HOUR = 3_600_000
const ago = (hours) => new Date(NOW.getTime() - hours * HOUR).toISOString()
const connection = (extra = {}) => ({ status: 'aktiv', validUntil: '2027-03-29T00:00:00.000Z', lastSyncAt: ago(1), lastSyncOk: true, lastError: '', ...extra })
const invoice = (extra = {}) => ({
id: 'in_1', number: 'WK-2026-0001', status: 'ueberfaellig', customerName: 'Muster GmbH', createdDate: '2026-08-01', dueDate: addDays(TODAY, -5),
remainingCents: 50000, paidCents: 0, paidDate: null, overdueDays: 5, source: 'projekt', hostedUrl: 'https://pay.example/in_1', ...extra,
})
const inflow = (extra = {}) => ({
$id: 'tx1', status: 'BOOK', bookingDate: '2026-09-28', amountCents: 16879, counterpartyName: 'EWV Software GmbH', remittance: 'Rechnung 0005', category: '', ...extra,
})
const sub = (extra = {}) => ({ customerId: 'c1', status: 'past_due', priceAmount: 2000, productKey: 'hosting', ...extra })
const tax = (anteil = 0.1, extra = {}) => ({ jahr: 2026, kleinunternehmer: { jahrCents: Math.round(anteil * 2500000), grenzeFolgejahrCents: 2500000, grenzeHartCents: 10000000, anteilFolgejahr: anteil, ...extra } })
const recurring = (extra = {}) => ({ key: 'hetzner', name: 'Hetzner', amountCents: 449, interval: 'monat', lastDate: '2026-09-27', count: 4, ...extra })
const todos = (extra = {}) => buildTodos({ today: TODAY, now: NOW, connection: connection(), invoices: [], transactions: [], subscriptions: [], recurring: [], tax: tax(), ...extra })
const only = (list, art) => list.filter((t) => t.art === art)
test('alles ruhig: keine Aufgaben', () => {
assert.deepEqual(todos(), [])
})
test('Standardwerte: fehlende Listen und tax dürfen fehlen', () => {
assert.deepEqual(buildTodos({ today: TODAY, now: NOW, connection: connection() }), [])
assert.deepEqual(buildTodos({ today: TODAY, now: NOW, connection: connection(), invoices: null, transactions: null, subscriptions: null, recurring: null, tax: null }), [])
})
// --- bank-zugang ---------------------------------------------------------------------------------------------
test('bank-zugang: keine Verbindung -> „Bankkonto verbinden“ hoch', () => {
for (const noConnection of [null, undefined]) {
const list = todos({ connection: noConnection })
assert.equal(list.length, 1)
assert.deepEqual(list[0], {
art: 'bank-zugang',
dringlichkeit: 'hoch',
titel: 'Bankkonto verbinden',
text: list[0].text,
datum: TODAY,
aktion: { typ: 'bank-verbinden' },
})
assert.match(list[0].text, /Konto/)
}
})
test('bank-zugang: Zugang gültig bis 20.10. (19 Tage) -> mittel, bis 10.10. (9 Tage) -> hoch', () => {
const mid = only(todos({ connection: connection({ validUntil: '2026-10-20T00:00:00.000Z' }) }), 'bank-zugang')
assert.equal(mid.length, 1)
assert.equal(mid[0].dringlichkeit, 'mittel')
assert.equal(mid[0].titel, 'Bankzugang läuft ab')
assert.equal(mid[0].datum, '2026-10-20')
assert.deepEqual(mid[0].aktion, { typ: 'bank-verbinden' })
assert.match(mid[0].text, /20\.10\.2026/)
assert.match(mid[0].text, /19 Tagen/)
const high = only(todos({ connection: connection({ validUntil: '2026-10-10T00:00:00.000Z' }) }), 'bank-zugang')
assert.equal(high.length, 1)
assert.equal(high[0].dringlichkeit, 'hoch')
})
test('bank-zugang: Grenzen 14/15 und 30/31 Tage, Berliner Kalendertag', () => {
const level = (validUntil) => only(todos({ connection: connection({ validUntil }) }), 'bank-zugang')[0]?.dringlichkeit ?? null
assert.equal(level('2026-10-15T12:00:00.000Z'), 'hoch') // 14 Tage
assert.equal(level('2026-10-16T12:00:00.000Z'), 'mittel') // 15 Tage
assert.equal(level('2026-10-31T12:00:00.000Z'), 'mittel') // 30 Tage
assert.equal(level('2026-11-01T12:00:00.000Z'), null) // 31 Tage
// 23:30 UTC ist in Berlin schon der nächste Tag: 15.10. 23:30 UTC = 16.10. -> 15 Tage
assert.equal(level('2026-10-15T23:30:00.000Z'), 'mittel')
assert.equal(level('2026-10-01T12:00:00.000Z'), 'hoch') // läuft heute ab
})
test('bank-zugang: Status abgelaufen -> „Bankzugang abgelaufen“ hoch, egal was validUntil sagt', () => {
for (const validUntil of ['2027-03-29T00:00:00.000Z', '2026-09-01T00:00:00.000Z', '', undefined]) {
const list = only(todos({ connection: connection({ status: 'abgelaufen', validUntil }) }), 'bank-zugang')
assert.equal(list.length, 1)
assert.equal(list[0].titel, 'Bankzugang abgelaufen')
assert.equal(list[0].dringlichkeit, 'hoch')
assert.deepEqual(list[0].aktion, { typ: 'bank-verbinden' })
}
})
test('bank-zugang: Datum der abgelaufenen Aufgabe ist nie in der Zukunft', () => {
const datum = (validUntil) => only(todos({ connection: connection({ status: 'abgelaufen', validUntil }) }), 'bank-zugang')[0].datum
assert.equal(datum('2027-03-29T00:00:00.000Z'), TODAY)
assert.equal(datum('2026-09-01T10:00:00.000Z'), '2026-09-01')
assert.equal(datum(''), TODAY)
})
test('bank-zugang: Text nennt heute, morgen oder die Tage', () => {
const text = (validUntil) => only(todos({ connection: connection({ validUntil }) }), 'bank-zugang')[0].text
assert.match(text('2026-10-01T12:00:00.000Z'), /\(heute\)/)
assert.match(text('2026-10-02T12:00:00.000Z'), /\(morgen\)/)
assert.match(text('2026-10-04T12:00:00.000Z'), /\(in 3 Tagen\)/)
})
test('bank-zugang: Status aktiv, aber validUntil liegt schon zurück -> abgelaufen hoch', () => {
const list = only(todos({ connection: connection({ validUntil: '2026-09-30T10:00:00.000Z' }) }), 'bank-zugang')
assert.equal(list.length, 1)
assert.equal(list[0].titel, 'Bankzugang abgelaufen')
assert.equal(list[0].dringlichkeit, 'hoch')
assert.match(list[0].text, /30\.09\.2026/)
})
test('bank-zugang: ohne lesbares validUntil keine Aufgabe (aktiv), Status fehler folgt validUntil', () => {
assert.deepEqual(only(todos({ connection: connection({ validUntil: '' }) }), 'bank-zugang'), [])
assert.deepEqual(only(todos({ connection: connection({ validUntil: 'bald' }) }), 'bank-zugang'), [])
assert.deepEqual(only(todos({ connection: connection({ status: 'fehler' }) }), 'bank-zugang'), [])
assert.equal(only(todos({ connection: connection({ status: 'fehler', validUntil: '2026-10-05T00:00:00.000Z' }) }), 'bank-zugang')[0].dringlichkeit, 'hoch')
})
// --- bank-abruf ----------------------------------------------------------------------------------------------
test('bank-abruf: letzter Abruf vor 25 h -> mittel, Aktion bank-aktualisieren', () => {
const list = only(todos({ connection: connection({ lastSyncAt: ago(25) }) }), 'bank-abruf')
assert.equal(list.length, 1)
assert.equal(list[0].dringlichkeit, 'mittel')
assert.equal(list[0].titel, 'Bankabruf veraltet')
assert.equal(list[0].datum, '2026-09-30')
assert.deepEqual(list[0].aktion, { typ: 'bank-aktualisieren' })
assert.match(list[0].text, /30\.09\.2026/)
})
test('bank-abruf: 23 h und genau 24 h sind noch frisch', () => {
assert.deepEqual(only(todos({ connection: connection({ lastSyncAt: ago(23) }) }), 'bank-abruf'), [])
assert.deepEqual(only(todos({ connection: connection({ lastSyncAt: ago(24) }) }), 'bank-abruf'), [])
assert.equal(only(todos({ connection: connection({ lastSyncAt: new Date(NOW.getTime() - 24 * HOUR - 1).toISOString() }) }), 'bank-abruf').length, 1)
})
test('bank-abruf: lastSyncOk false oder fehlender Abruf; Fehlertext erscheint', () => {
const failed = only(todos({ connection: connection({ lastSyncOk: false, lastError: 'Die Bank antwortet nicht.' }) }), 'bank-abruf')
assert.equal(failed.length, 1)
assert.equal(failed[0].titel, 'Bankabruf fehlgeschlagen')
assert.match(failed[0].text, /Die Bank antwortet nicht\./)
assert.equal(failed[0].datum, TODAY) // letzter Abruf vor 1 h = heute
const never = only(todos({ connection: connection({ lastSyncAt: '', lastSyncOk: true }) }), 'bank-abruf')
assert.equal(never.length, 1)
assert.equal(never[0].datum, TODAY)
assert.equal(only(todos({ connection: connection({ lastSyncAt: undefined }) }), 'bank-abruf').length, 1)
assert.equal(only(todos({ connection: connection({ lastSyncAt: 'neulich' }) }), 'bank-abruf').length, 1)
})
test('bank-abruf: nur bei Status aktiv und mit Verbindung', () => {
assert.deepEqual(only(todos({ connection: connection({ status: 'abgelaufen', lastSyncAt: ago(100), lastSyncOk: false }) }), 'bank-abruf'), [])
assert.deepEqual(only(todos({ connection: connection({ status: 'fehler', lastSyncAt: ago(100) }) }), 'bank-abruf'), [])
assert.deepEqual(only(todos({ connection: null }), 'bank-abruf'), [])
})
// --- ueberfaellig --------------------------------------------------------------------------------------------
test('ueberfaellig: 31 Tage Verzug -> hoch, 5 Tage -> mittel', () => {
const late = only(todos({ invoices: [invoice({ dueDate: addDays(TODAY, -31), overdueDays: 31 })] }), 'ueberfaellig')
assert.equal(late.length, 1)
assert.equal(late[0].dringlichkeit, 'hoch')
const soon = only(todos({ invoices: [invoice({ dueDate: addDays(TODAY, -5) })] }), 'ueberfaellig')
assert.equal(soon.length, 1)
assert.equal(soon[0].dringlichkeit, 'mittel')
})
test('ueberfaellig: Grenze bei 30 Tagen, Inhalt und Aktion', () => {
const level = (days) => only(todos({ invoices: [invoice({ dueDate: addDays(TODAY, -days) })] }), 'ueberfaellig')[0].dringlichkeit
assert.equal(level(29), 'mittel')
assert.equal(level(30), 'hoch')
assert.equal(level(1), 'mittel')
const [t] = only(todos({ invoices: [invoice()] }), 'ueberfaellig')
assert.equal(t.titel, 'Rechnung WK-2026-0001 überfällig')
assert.equal(t.datum, '2026-09-26')
assert.deepEqual(t.aktion, { typ: 'rechnung-oeffnen', invoiceId: 'in_1', hostedUrl: 'https://pay.example/in_1' })
assert.ok(t.text.includes('Muster GmbH'))
assert.ok(t.text.includes(euroDe(50000)))
assert.ok(t.text.includes('26.09.2026'))
assert.ok(t.text.includes('5 Tage'))
assert.ok(only(todos({ invoices: [invoice({ dueDate: addDays(TODAY, -1) })] }), 'ueberfaellig')[0].text.includes('1 Tag'))
})
test('ueberfaellig: offene Rechnung mit abgelaufener Fälligkeit zählt, nicht fällige, bezahlte und Entwürfe nicht', () => {
assert.equal(only(todos({ invoices: [invoice({ status: 'offen', dueDate: '2026-09-30' })] }), 'ueberfaellig').length, 1)
assert.deepEqual(only(todos({ invoices: [invoice({ status: 'offen', dueDate: TODAY })] }), 'ueberfaellig'), [])
assert.deepEqual(only(todos({ invoices: [invoice({ status: 'offen', dueDate: '2026-10-14' })] }), 'ueberfaellig'), [])
assert.deepEqual(only(todos({ invoices: [invoice({ status: 'bezahlt', paidCents: 50000, remainingCents: 0 })] }), 'ueberfaellig'), [])
assert.deepEqual(only(todos({ invoices: [invoice({ status: 'entwurf' }), invoice({ id: 'in_2', status: 'storniert' }), invoice({ id: 'in_3', status: 'uneinbringlich' })] }), 'ueberfaellig'), [])
})
test('ueberfaellig: ohne hostedUrl -> hostedUrl null', () => {
const [t] = only(todos({ invoices: [invoice({ hostedUrl: undefined })] }), 'ueberfaellig')
assert.deepEqual(t.aktion, { typ: 'rechnung-oeffnen', invoiceId: 'in_1', hostedUrl: null })
})
// --- zuordnung -----------------------------------------------------------------------------------------------
test('zuordnung: ungeordneter Eingang -> mittel mit transactionId', () => {
const list = only(todos({ transactions: [inflow()] }), 'zuordnung')
assert.equal(list.length, 1)
assert.equal(list[0].dringlichkeit, 'mittel')
assert.equal(list[0].titel, 'Zahlungseingang zuordnen')
assert.equal(list[0].datum, '2026-09-28')
assert.deepEqual(list[0].aktion, { typ: 'zuordnen', transactionId: 'tx1' })
assert.ok(list[0].text.includes(euroDe(16879)))
assert.ok(list[0].text.includes('EWV Software GmbH'))
assert.ok(list[0].text.includes('28.09.2026'))
assert.ok(list[0].text.includes('Rechnung 0005'))
assert.ok(!list[0].text.includes('Vorschlag'))
})
test('zuordnung: nur gebuchte Eingänge ohne Kategorie', () => {
const none = [
inflow({ status: 'PDNG' }),
inflow({ amountCents: -500 }),
inflow({ amountCents: 0 }),
inflow({ category: 'rechnung' }),
inflow({ category: 'privat' }),
inflow({ amountCents: 1.5 }),
inflow({ bookingDate: 'heute' }),
]
assert.deepEqual(only(todos({ transactions: none }), 'zuordnung'), [])
for (const category of [undefined, null, '']) assert.equal(only(todos({ transactions: [inflow({ category })] }), 'zuordnung').length, 1)
})
test('zuordnung: Vorschlag hoch hängt „Vorschlag: Rechnung <Nr.>“ bzw. „Vorschlag: Stripe-Auszahlung“ an', () => {
const invoiceHint = { kind: 'invoice', id: 'in_9', number: 'WK-2026-0009', confidence: 'hoch', reason: 'Betrag und Nummer', amountDiffers: false }
const payoutHint = { kind: 'payout', id: 'po_1', number: '', confidence: 'hoch', reason: 'Betrag und Datum', amountDiffers: false }
const [a] = only(todos({ transactions: [inflow({ vorschlaege: [invoiceHint] })] }), 'zuordnung')
assert.ok(a.text.endsWith('Vorschlag: Rechnung WK-2026-0009'))
const [b] = only(todos({ transactions: [inflow({ vorschlaege: [payoutHint] })] }), 'zuordnung')
assert.ok(b.text.endsWith('Vorschlag: Stripe-Auszahlung'))
assert.deepEqual(b.aktion, { typ: 'zuordnen', transactionId: 'tx1' })
})
test('zuordnung: nur ein Vorschlag mit Sicherheit hoch zählt, der erste davon', () => {
const hint = (extra) => ({ kind: 'invoice', id: 'in_x', number: 'WK-2026-0001', confidence: 'mittel', reason: '', amountDiffers: false, ...extra })
const mid = only(todos({ transactions: [inflow({ vorschlaege: [hint({}), hint({ confidence: 'niedrig' })] })] }), 'zuordnung')[0]
assert.ok(!mid.text.includes('Vorschlag'))
const mixed = only(todos({ transactions: [inflow({ vorschlaege: [hint({}), hint({ confidence: 'hoch', number: 'WK-2026-0002' }), hint({ confidence: 'hoch', number: 'WK-2026-0003' })] })] }), 'zuordnung')[0]
assert.ok(mixed.text.endsWith('Vorschlag: Rechnung WK-2026-0002'))
assert.ok(!only(todos({ transactions: [inflow({ vorschlaege: [] })] }), 'zuordnung')[0].text.includes('Vorschlag'))
})
// --- abo-zahlung ---------------------------------------------------------------------------------------------
test('abo-zahlung: past_due und unpaid -> hoch, Aktion kunde-oeffnen', () => {
const list = only(todos({ subscriptions: [sub(), sub({ customerId: 'c2', status: 'unpaid', priceAmount: 5000 })] }), 'abo-zahlung')
assert.equal(list.length, 2)
for (const t of list) assert.equal(t.dringlichkeit, 'hoch')
assert.deepEqual(list.map((t) => t.aktion), [
{ typ: 'kunde-oeffnen', customerId: 'c1' },
{ typ: 'kunde-oeffnen', customerId: 'c2' },
])
assert.equal(list[0].titel, 'Abo-Zahlung fehlgeschlagen')
assert.equal(list[1].titel, 'Abo unbezahlt')
assert.equal(list[0].datum, TODAY)
assert.ok(list[0].text.includes('hosting'))
assert.ok(list[0].text.includes(euroDe(2000)))
})
test('abo-zahlung: aktive, gekündigte und unbekannte Abos erzeugen nichts; Kundenname und planKey werden genutzt', () => {
assert.deepEqual(only(todos({ subscriptions: [sub({ status: 'active' }), sub({ status: 'canceled' }), sub({ status: 'trialing' })] }), 'abo-zahlung'), [])
const [t] = only(todos({ subscriptions: [sub({ productKey: undefined, planKey: 'wartung', customerName: 'Muster GmbH' })] }), 'abo-zahlung')
assert.ok(t.text.includes('Muster GmbH'))
assert.ok(t.text.includes('wartung'))
const [bare] = only(todos({ subscriptions: [sub({ productKey: undefined, priceAmount: undefined })] }), 'abo-zahlung')
assert.equal(bare.art, 'abo-zahlung')
})
// --- entwurf -------------------------------------------------------------------------------------------------
test('entwurf: vor 4 Tagen -> info, von gestern -> keine Aufgabe, auch 3 Tage noch nicht', () => {
const draft = (days) => invoice({ id: 'in_d', number: '', status: 'entwurf', createdDate: addDays(TODAY, -days), remainingCents: 24000, hostedUrl: null })
const [t] = only(todos({ invoices: [draft(4)] }), 'entwurf')
assert.equal(t.dringlichkeit, 'info')
assert.equal(t.titel, 'Rechnungsentwurf nicht versendet')
assert.equal(t.datum, '2026-09-27')
assert.deepEqual(t.aktion, { typ: 'rechnung-oeffnen', invoiceId: 'in_d', hostedUrl: null })
assert.ok(t.text.includes('Muster GmbH'))
assert.ok(t.text.includes(euroDe(24000)))
assert.ok(t.text.includes('4 Tagen'))
assert.deepEqual(only(todos({ invoices: [draft(1)] }), 'entwurf'), [])
assert.deepEqual(only(todos({ invoices: [draft(3)] }), 'entwurf'), [])
assert.equal(only(todos({ invoices: [draft(40)] }), 'entwurf').length, 1)
})
test('entwurf: andere Status nie, Entwurf ohne Nummer wird nicht mit leerem Namen angezeigt', () => {
assert.deepEqual(only(todos({ invoices: [invoice({ status: 'offen', createdDate: '2026-01-01', dueDate: '2026-12-01' })] }), 'entwurf'), [])
const [t] = only(todos({ invoices: [invoice({ status: 'entwurf', number: '', createdDate: '2026-09-01' })] }), 'entwurf')
assert.ok(!/ {2}|\(\)|undefined|null/.test(t.text))
})
// --- fixkosten-vorschlag -------------------------------------------------------------------------------------
test('fixkosten-vorschlag: erkannte wiederkehrende Ausgabe -> info mit Vorschlag in der Aktion', () => {
const entry = recurring()
const [t] = only(todos({ recurring: [entry] }), 'fixkosten-vorschlag')
assert.equal(t.dringlichkeit, 'info')
assert.equal(t.datum, '2026-09-27')
assert.deepEqual(t.aktion, { typ: 'fixkosten-uebernehmen', vorschlag: entry })
assert.ok(t.text.includes('Hetzner'))
assert.ok(t.text.includes(euroDe(449)))
assert.ok(t.text.includes('monatlich'))
assert.match(t.text, /27\.09\.2026/)
})
test('fixkosten-vorschlag: nur wenn die letzte Buchung höchstens 60 Tage zurückliegt', () => {
const at = (days) => only(todos({ recurring: [recurring({ lastDate: addDays(TODAY, -days) })] }), 'fixkosten-vorschlag').length
assert.equal(at(60), 1)
assert.equal(at(61), 0)
assert.equal(at(0), 1)
assert.equal(only(todos({ recurring: [recurring({ lastDate: 'Mai' })] }), 'fixkosten-vorschlag').length, 0)
})
test('fixkosten-vorschlag: Rhythmus im Text', () => {
const text = (interval) => only(todos({ recurring: [recurring({ interval })] }), 'fixkosten-vorschlag')[0].text
assert.ok(text('quartal').includes('vierteljährlich'))
assert.ok(text('jahr').includes('jährlich') && !text('jahr').includes('vierteljährlich'))
assert.ok(text('monat').includes('monatlich'))
})
// --- kleinunternehmer ----------------------------------------------------------------------------------------
test('kleinunternehmer: Anteil 0.72 -> mittel, 1.05 -> hoch, Aktion einstellungen', () => {
const [mid] = only(todos({ tax: tax(0.72) }), 'kleinunternehmer')
assert.equal(mid.dringlichkeit, 'mittel')
assert.deepEqual(mid.aktion, { typ: 'einstellungen' })
assert.equal(mid.datum, TODAY)
assert.ok(mid.text.includes('72 %'))
assert.ok(mid.text.includes(euroDe(1800000)))
assert.ok(mid.text.includes(euroDe(2500000)))
const [high] = only(todos({ tax: tax(1.05) }), 'kleinunternehmer')
assert.equal(high.dringlichkeit, 'hoch')
assert.ok(high.text.includes(euroDe(2625000)))
})
test('kleinunternehmer: Schwellen 70 % und 100 %, darunter nichts', () => {
const level = (anteil) => only(todos({ tax: tax(anteil) }), 'kleinunternehmer')[0]?.dringlichkeit ?? null
assert.equal(level(0.69), null)
assert.equal(level(0), null)
assert.equal(level(0.7), 'mittel')
assert.equal(level(1750000 / 2500000), 'mittel')
assert.equal(level(0.99), 'mittel')
assert.equal(level(1), 'hoch')
assert.equal(level(2.5), 'hoch')
})
test('kleinunternehmer: über 100.000 Euro nennt die harte Grenze', () => {
const [t] = only(todos({ tax: tax(4.2, { jahrCents: 10500000 }) }), 'kleinunternehmer')
assert.equal(t.dringlichkeit, 'hoch')
assert.ok(t.text.includes(euroDe(10000000)))
assert.ok(t.text.includes(euroDe(10500000)))
assert.equal(
t.text,
`Die Einnahmen 2026 betragen ${euroDe(10500000)} und liegen über der Grenze von ${euroDe(10000000)}. ` +
'Ab dem Umsatz, mit dem die Grenze überschritten wurde, gilt die Kleinunternehmerregelung (§ 19 UStG) nicht mehr – auch im laufenden Jahr. ' +
'Bitte umgehend mit der Steuerberatung klären.'
)
})
test('kleinunternehmer: ohne tax oder mit unlesbarem Anteil keine Aufgabe', () => {
assert.deepEqual(only(todos({ tax: null }), 'kleinunternehmer'), [])
assert.deepEqual(only(todos({ tax: {} }), 'kleinunternehmer'), [])
assert.deepEqual(only(todos({ tax: { kleinunternehmer: { anteilFolgejahr: 'viel' } } }), 'kleinunternehmer'), [])
})
test('kleinunternehmer: ohne jahrCents wird der Betrag aus dem Anteil abgeleitet', () => {
const [t] = only(todos({ tax: { kleinunternehmer: { anteilFolgejahr: 0.72 } } }), 'kleinunternehmer')
assert.ok(t.text.includes(euroDe(1800000)))
})
// --- Sortierung und Sonstiges --------------------------------------------------------------------------------
test('Sortierung: hoch vor mittel vor info, darin nach Datum aufsteigend', () => {
const list = todos({
connection: connection({ validUntil: '2026-10-20T00:00:00.000Z', lastSyncAt: ago(30) }), // mittel (20.10.), mittel (30.09.)
invoices: [
invoice({ id: 'late', dueDate: '2026-08-01' }), // hoch 01.08.
invoice({ id: 'later', dueDate: '2026-08-15' }), // hoch 15.08.
invoice({ id: 'draft', status: 'entwurf', createdDate: '2026-09-20' }), // info 20.09.
invoice({ id: 'mid', dueDate: '2026-09-29' }), // mittel 29.09.
],
transactions: [inflow({ bookingDate: '2026-09-10' })], // mittel 10.09.
subscriptions: [sub()], // hoch heute
recurring: [recurring({ lastDate: '2026-09-15' })], // info 15.09.
tax: tax(0.8), // mittel heute
})
assert.deepEqual(
list.map((t) => [t.dringlichkeit, t.datum, t.art]),
[
['hoch', '2026-08-01', 'ueberfaellig'],
['hoch', '2026-08-15', 'ueberfaellig'],
['hoch', '2026-10-01', 'abo-zahlung'],
['mittel', '2026-09-10', 'zuordnung'],
['mittel', '2026-09-29', 'ueberfaellig'],
['mittel', '2026-09-30', 'bank-abruf'],
['mittel', '2026-10-01', 'kleinunternehmer'],
['mittel', '2026-10-20', 'bank-zugang'],
['info', '2026-09-15', 'fixkosten-vorschlag'],
['info', '2026-09-20', 'entwurf'],
]
)
})
test('Sortierung: bei gleichem Datum bleibt die Eingabereihenfolge', () => {
const list = todos({ invoices: [invoice({ id: 'b', number: 'WK-B', dueDate: '2026-09-20' }), invoice({ id: 'a', number: 'WK-A', dueDate: '2026-09-20' })] })
assert.deepEqual(list.map((t) => t.aktion.invoiceId), ['b', 'a'])
})
test('jede Aufgabe hat Art, Dringlichkeit, Titel, Text, Datum und Aktion', () => {
const list = todos({
connection: connection({ status: 'abgelaufen', lastSyncAt: ago(30) }),
invoices: [invoice(), invoice({ id: 'd', status: 'entwurf', createdDate: '2026-09-01' })],
transactions: [inflow()],
subscriptions: [sub()],
recurring: [recurring()],
tax: tax(1.1),
})
assert.equal(list.length, 7)
for (const t of list) {
assert.deepEqual(Object.keys(t).sort(), ['aktion', 'art', 'datum', 'dringlichkeit', 'text', 'titel'])
assert.ok(['hoch', 'mittel', 'info'].includes(t.dringlichkeit))
assert.match(t.datum, /^\d{4}-\d{2}-\d{2}$/)
assert.ok(t.titel && t.text)
assert.ok(!/undefined|null|NaN/.test(`${t.titel} ${t.text}`))
}
})
test('kaputte Datensätze werden übersprungen statt alles zu kippen', () => {
const list = todos({
invoices: [null, invoice({ dueDate: 'bald' }), invoice({ id: 'x', remainingCents: 1.5 }), invoice({ id: 'ok' })],
transactions: [null, inflow({ $id: 'bad', amountCents: 'x' }), inflow({ $id: 'ok' })],
subscriptions: [null],
recurring: [null, recurring({ lastDate: undefined })],
})
assert.deepEqual(list.map((t) => t.art).sort(), ['ueberfaellig', 'zuordnung'])
})
test('Eingaben werden nicht verändert', () => {
const input = {
today: TODAY,
now: NOW,
connection: connection({ lastSyncAt: ago(30) }),
invoices: [invoice()],
transactions: [inflow({ vorschlaege: [{ kind: 'invoice', id: 'in_1', number: 'WK-1', confidence: 'hoch', reason: '', amountDiffers: false }] })],
subscriptions: [sub()],
recurring: [recurring()],
tax: tax(0.9),
}
const copy = JSON.stringify(input)
buildTodos(input)
assert.equal(JSON.stringify(input), copy)
})