Finanzen: Vorschau, Steuer und Aufgaben genauer (Fixkosten ohne ESt, verspaetete Auszahlungen, Abos nicht ladbar)

M6: fixkosten30Cents zaehlt nur Fixkosten, die ESt-Vorauszahlung bleibt sicherer Posten.
M7: verspaetete Auszahlung (Ankunftstag vorbei, noch nicht bezahlt) kommt heute.
M8: ohne Stripe sind Kleinunternehmer-Zaehler und Anteil unbekannt (null), keine Warnung daraus.
M12: Verbindungsdokument ohne Sitzung zaehlt als keine Verbindung.
M13: Buchungen privat/fixkosten gehen nicht in die Fixkosten-Vorschlaege.
M14: Vorjahresumsatz ueber 25.000 EUR ist eine hohe Aufgabe.
M15: scheitert das Laden der Abos, meldet abos.fehler statt die ganze Uebersicht zu kippen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 15:42:06 +00:00
1 parent 83ee9d5a25
commit 039fdcaf27
6 files changed
+295 -20

No files matched your search

+134 -1
View File
@@ -446,7 +446,10 @@ test('GET /uebersicht: Stripe 500 -> 200, Bankteil, Kopf und Aufgaben aus Bank u
assert.equal(body.kopf.kontostandCents, 10893)
assert.equal(body.kopf.verfuegbarCents, 4176)
assert.deepEqual(body.vorschau.posten.map((p) => p.label), ['Hetzner', 'Porkbun', 'Abo-Verlängerung'])
assert.equal(body.steuer.kleinunternehmer.jahrCents, 0)
// Kleinunternehmer-Zaehler ohne Stripe-Zahltage waere zu niedrig: unbekannt statt 0 (M8)
assert.equal(body.steuer.kleinunternehmer.jahrCents, null)
assert.equal(body.steuer.kleinunternehmer.anteilFolgejahr, null)
assert.equal(body.steuer.kleinunternehmer.grenzeFolgejahrCents, 2500000)
assert.deepEqual(body.rechnungen.altersgruppen.map((g) => g.anzahl), [0, 0, 0, 0])
assert.deepEqual(body.rechnungen.entwuerfe, { anzahl: 0, summeCents: 0 })
@@ -487,6 +490,136 @@ test('buildOverview: Stripe antwortet rechtzeitig -> ok, die Wartezeit haelt den
assert.equal(body.stand.stripe.ok, true)
})
test('GET /uebersicht: ohne Stripe kein Kleinunternehmer-Zähler und keine Warnung daraus, mit Stripe schon (M8)', async () => {
seedConnection()
seedSettings()
// 19.000 EUR "Rechnung ausserhalb von Stripe" im laufenden Jahr = 76 % der Grenze
aw.seed(config.collections.bankTransactions, tx('x1', { amountCents: 1_900_000, category: 'rechnung-extern', counterpartyName: 'GROSSKUNDE AG' }))
const ok = await uebersicht()
assert.equal(ok.steuer.kleinunternehmer.jahrCents, 1_900_000)
assert.equal(ok.steuer.kleinunternehmer.anteilFolgejahr, 0.76)
assert.equal(ok.aufgaben.filter((t) => t.art === 'kleinunternehmer').length, 1)
invalidateFinanceCache()
stripeDown()
const down = await uebersicht()
assert.equal(down.stand.stripe.ok, false)
assert.equal(down.steuer.kleinunternehmer.jahrCents, null)
assert.equal(down.steuer.kleinunternehmer.anteilFolgejahr, null)
assert.deepEqual(down.aufgaben.filter((t) => t.art === 'kleinunternehmer'), [])
// Gewinn und Ruecklage kommen aus der Bank und bleiben
assert.equal(down.steuer.einnahmenCents, 1_900_000)
assert.equal(down.steuer.ruecklageCents, ok.steuer.ruecklageCents)
})
test('GET /uebersicht: Verbindungsdokument ohne Sitzung -> wie keine Verbindung, Aufgabe "Bankkonto verbinden" (M12)', async () => {
aw.seed(config.collections.bankConnections, { $id: 'finom', authState: 'abc123', authStartedAt: new Date().toISOString() })
const body = await uebersicht()
assert.equal(body.stand.bank, null)
assert.equal(body.kopf.kontostandCents, null)
assert.equal(body.kopf.verfuegbarCents, null)
assert.deepEqual(ART(body), ['bank-zugang'])
assert.equal(body.aufgaben[0].titel, 'Bankkonto verbinden')
// mit Sitzung (auch bei einer neuen Freigabe in Arbeit) bleibt es ein Zugang
await deleteDocument(config.collections.bankConnections, 'finom')
seedConnection({ authState: 'neu', authStartedAt: new Date().toISOString() })
const connected = await uebersicht()
assert.equal(connected.stand.bank.status, 'aktiv')
assert.ok(!ART(connected).includes('bank-zugang'))
})
test('GET /uebersicht: Buchungen "privat" und "fixkosten" zählen nicht für Fixkosten-Vorschläge (M13)', async () => {
seedConnection()
const hetzner = (id, days, over = {}) => tx(id, { bookingDate: at(days), amountCents: -1000, counterpartyName: 'HETZNER ONLINE GMBH', ...over })
const seedThree = (prefix, over = {}) => [-58, -28, 0].forEach((days, i) => aw.seed(config.collections.bankTransactions, hetzner(`${prefix}${i}`, days, over)))
seedThree('open')
const suggested = (await uebersicht()).aufgaben.filter((t) => t.art === 'fixkosten-vorschlag')
assert.equal(suggested.length, 1)
assert.match(suggested[0].text, /HETZNER ONLINE GMBH/)
for (const id of ['open0', 'open1', 'open2']) await deleteDocument(config.collections.bankTransactions, id)
seedThree('privat', { category: 'privat' })
assert.deepEqual((await uebersicht()).aufgaben.filter((t) => t.art === 'fixkosten-vorschlag'), [])
for (const id of ['privat0', 'privat1', 'privat2']) await deleteDocument(config.collections.bankTransactions, id)
seedThree('fix', { category: 'fixkosten', fixedCostId: 'fk1' })
assert.deepEqual((await uebersicht()).aufgaben.filter((t) => t.art === 'fixkosten-vorschlag'), [])
for (const id of ['fix0', 'fix1', 'fix2']) await deleteDocument(config.collections.bankTransactions, id)
// zwei private und eine offene: ohne den Filter waeren es drei gleiche Abbuchungen
aw.seed(config.collections.bankTransactions, hetzner('m0', -58, { category: 'privat' }))
aw.seed(config.collections.bankTransactions, hetzner('m1', -28, { category: 'privat' }))
aw.seed(config.collections.bankTransactions, hetzner('m2', 0))
assert.deepEqual((await uebersicht()).aufgaben.filter((t) => t.art === 'fixkosten-vorschlag'), [])
})
test('GET /uebersicht: Vorjahresumsatz über 25.000 € aus den Einstellungen wird eine hohe Aufgabe (M14)', async () => {
seedConnection()
seedSettings({ previousYearRevenueCents: 2_600_000 })
const body = await uebersicht()
const todo = body.aufgaben.find((t) => t.titel === 'Vorjahresumsatz über 25.000 €')
assert.ok(todo, JSON.stringify(body.aufgaben))
assert.equal(todo.art, 'kleinunternehmer')
assert.equal(todo.dringlichkeit, 'hoch')
assert.match(todo.text, /26\.000,00/)
// hoch steht vor den mittleren Bank-Aufgaben
assert.equal(body.aufgaben[0].dringlichkeit, 'hoch')
await deleteDocument(config.collections.financeSettings, 'main')
seedSettings({ previousYearRevenueCents: 2_500_000 })
assert.ok(!(await uebersicht()).aufgaben.some((t) => t.titel === 'Vorjahresumsatz über 25.000 €'))
})
test('GET /uebersicht: Abos nicht ladbar -> 200 mit abos.fehler, der Rest bleibt, nichts aus Abos in Vorschau und Aufgaben (M15)', async () => {
seedAll()
seedSub('sub_c', { customerId: 'c1', status: 'past_due' })
const error = mock.method(console, 'error', () => {})
aw.failNext({ collection: config.collections.subscriptions, method: 'GET', status: 500, message: 'Appwrite kaputt 10.0.0.9' })
const res = await rawGet(`${base}/uebersicht`, admin)
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(res.body.abos, {
monatlichCents: null,
zahlendeKunden: null,
gefaehrdetCents: null,
liste: [],
fehler: 'Abos konnten nicht geladen werden.',
})
assert.ok(!JSON.stringify(res.body).includes('10.0.0.9'))
assert.equal(res.body.stand.bank.balanceBookedCents, 10893)
assert.equal(res.body.stand.stripe.ok, true)
assert.ok(!ART(res.body).includes('abo-zahlung'))
assert.ok(!res.body.vorschau.posten.some((p) => p.label === 'Abo-Verlängerung'))
assert.ok(res.body.vorschau.posten.some((p) => p.label === 'Porkbun'), 'Fixkosten bleiben')
assert.ok(ART(res.body).includes('ueberfaellig'))
assert.equal(error.mock.callCount(), 1)
assert.deepEqual(error.mock.calls[0].arguments, ['[finanzen] Abos laden fehlgeschlagen:', 'Error', 500, 500])
// der naechste Aufruf laedt wieder und hat kein fehler-Feld
mock.restoreAll()
const again = await uebersicht()
assert.equal(again.abos.fehler, undefined)
assert.equal(again.abos.liste.length, 3)
})
test('buildOverview: ESt-Vorauszahlung bleibt ein sicherer Posten, ist aber nicht in fixkosten30Cents und verfuegbarCents (M6)', async () => {
seedConnection()
seedSettings({ estPrepaymentCents: 50_000, minBufferCents: 0 })
aw.seed(config.collections.financeFixedCosts, { $id: 'fk1', name: 'Hetzner', amountCents: 449, interval: 'monat', nextDate: '2026-11-25', counterpartyMatch: '', active: true })
const body = await buildOverview({ now: new Date('2026-11-20T10:00:00Z') })
assert.deepEqual(body.vorschau.posten.map((p) => [p.date, p.label, p.amountCents]), [
['2026-11-25', 'Hetzner', -449],
['2026-12-10', 'ESt-Vorauszahlung', -50_000],
])
assert.equal(body.vorschau.fixkosten30Cents, 449)
assert.equal(body.kopf.aufschluesselung.fixkosten30Cents, 449)
// 10.893 - Ruecklage 0 - Fixkosten 449 - Puffer 0: die ESt ist Sache der Ruecklage, nicht noch einmal abgezogen
assert.equal(body.kopf.verfuegbarCents, 10893 - 449)
assert.equal(body.vorschau.endeSicherCents, 10893 - 449 - 50_000)
})
test('GET /uebersicht: nur die Auszahlungen scheitern -> Rechnungen bleiben, Stripe-Block meldet den Fehler', async () => {
seedAll()
st.failNext({ method: 'GET', path: '/v1/payouts', status: 400, message: 'Geheimer Stripe-Text' })