Banking | Cine Power Planner

Banking

Phase 4 of the Accounting domain — bank-statement reconciliation, transaction linking, and partial-payment tracking

Visibility (production): The Banking surface (sidebar entry, /accounting/banking route, Settings → Accounting → Banking sub-tab) sits behind the same Pro paywall as the rest of Accounting. Owners and customers on the Accounting plan see it; everyone else gets a locked empty state with an upgrade CTA. Dev / staging builds keep everything visible. This page documents the feature for owners and Accounting subscribers.

Overview

The Banking module lives at /accounting/banking and lets you close the loop between an invoice you sent and the money that landed on your account — without leaving Cine Power Planner. Every transaction is local-first (your bank data lives in IndexedDB + OPFS backup; Supabase sync is opt-in), legally aligned with § 257 HGB / GoBD 10-year retention requirements, § 14 Abs. 4 Nr. 7 UStG Skonto rules, and DATEV / GoBD Z3 export so your tax advisor can pull your bank ledger in their preferred shape.

Three input paths feed the same BankTransaction entity:

  1. Manual entry — blank form. Useful for cash receipts, owner draws, or one-off corrections.

  2. CSV import — drag-and-drop your bank's CSV export. 13 German bank presets ship out of the box (Sparkasse, Volksbank, Commerzbank, Deutsche Bank, DKB, Postbank, comdirect, ING, HypoVereinsbank (HVB), N26, Revolut, Wise, Trade Republic). Unknown formats fall back to a manual column-mapping step.

  3. camt.053 (ISO 20022) XML — the standardized statement format every German bank can produce on demand. Includes built-in closing-balance reconciliation so a mid-statement edit catches itself before you save.

  4. FinTS / HBCI bank sync — connect the bank directly and let it fetch. This shipped in v0.401.0 (the macOS desktop shell in v0.408.0) and lives at Settings → Accounting → Banking. See Bank sync (FinTS) below for where it is available.

This guide said Banking "does NOT ship automated bank sync" and that FinTS was "deferred", right up to 2026-09-07 — a year after it landed. It also told readers to file a feature request for it. If you imported CSVs by hand because of that, you did not have to.

FinAPI (N26 / Revolut / Wise / Trade Republic / Tomorrow / Vivid via paid add-on) is a separate, still-deferred track for banks FinTS does not reach.

Transaction lifecycle

imported / created  ──score──▶  unmatched
                                 │
                                 ├──auto-match (≥ threshold) ──▶  matched
                                 ├──auto-match (< threshold) ──▶  suggested
                                 └──user link────────────────▶  matched

matched / suggested  ──user un-link──▶  unmatched
matched / suggested / unmatched  ──user mark ignored──▶  ignored
ignored  ──user un-ignore──▶  unmatched
any  ──user delete──▶  removed (10-year retention guard applies to imported / linked rows)
  • unmatched — the engine could not find a strong candidate. Action needed: link manually, mark ignored (for fees / withdrawals you don't need to track against an invoice or expense), or accept the default-tax-category SKR04 bookkeeping path if your accountant uses one.
  • suggested — the engine found at least one candidate but didn't auto-link (either score is below the auto-link threshold or you turned auto-link off). Action needed: review and confirm or reject.
  • matched — successfully linked to at least one invoice / expense / Mahnung via the junction table. The linked invoice / expense reflects paid or partially_paid status automatically.
  • ignored — explicitly marked "not relevant" (typical for bank fees, owner draws, internal transfers). Hidden from the default list view but still retained on disk per § 257 HGB.

Status is derived, not stored manually. The engine computes the status from the junction-table row count and your weight settings. Editing it directly is not supported — link / un-link / mark-ignored are the only mutations.

The list view

Open Banking from the sidebar (/accounting/banking). The page is split into four bands:

1. Header CTAs (top right)

  • + Add manually — opens the manual transaction form (see below).
  • Import CSV — opens the 4-step CSV wizard.
  • Import camt.053 — opens the XML import modal.
  • Download CSV — exports the visible (filtered) rows as a plain CSV.
  • Download DATEV — exports the visible rows in DATEV column order (transactions sheet + links sheet) ready for your Steuerberater.

2. Counters strip

Two chips: {n} unmatched and {n} suggested — click either to apply that status filter. The counters reflect the current filter scope (e.g., date range), not the global database.

3. Filter bar

  • Status — multi-select (Unmatched / Suggested / Matched / Ignored). Default: Unmatched + Suggested.
  • Source — multi-select (Manual / CSV / camt.053).
  • Account — single-select your bank accounts.
  • Date range — presets (This month / Last month / This year) + custom date picker.
  • Amount range — min / max input.
  • Search — free-text across counterparty + purpose. Diacritic-insensitive (Müller matches Muller).
  • Quick filters — preset chips: Unmatched > 7 days, Suggested, This month's income. One-click toggles.
  • Clear filters — top-right of the bar; restores the default Unmatched + Suggested view.

4. Table (desktop) / card list (mobile)

Columns: Date · Counterparty · Purpose · Account · Amount (right-aligned, color-coded) · Status (badge) · Linked (chips with the invoice number / expense ID; +N more collapses overflow) · Actions (kebab menu).

Click any row to open the detail drawer (right-side slide-out on desktop, full-screen on mobile). Multi-select via the row checkbox or Cmd / Ctrl-click + range-select with Shift.

5. Bulk action bar

Appears at the bottom when any rows are selected:

  • Accept suggestions — promotes every selected Suggested row to Matched, applying the engine's top candidate for each.
  • Mark ignored — bulk-set Ignored. Reversible via the kebab menu.
  • Re-link — opens the link modal pre-selecting the first row; allows you to re-link a misrouted batch (e.g., a vendor with similar names).
  • Delete — confirmation dialog. Imported rows older than 10 years cannot be deleted per § 257 HGB; the dialog warns and skips the protected rows.
  • Clear — deselects.

Empty states

  • No accounts yet — Banking is opt-in even within the Accounting plan; you create your first account from the empty-state CTA. Most users start with one current account (Girokonto); freelancers with a separate "business" Sparkasse Geschäftskonto add a second.
  • No transactions yet — your account exists but you haven't imported / entered anything.
  • All matched — every visible row is linked.
  • Filtered zero — your filters returned no rows.
  • Paywall locked — Banking is a Pro feature.

Manual entry

Click + Add manually in the header. The form:

FieldRequiredNotes
Booking dateYesDefaults to today. Picker is locale-aware (DE: dd.mm.yyyy / EN: yyyy-mm-dd).
AmountYesSmart-paste handles 1.234,56, (123,45) (parenthesized negatives), EUR 50,00, -15.00 €. Sign toggle controls Income / Expense default.
CurrencyYesDefaults to your account currency. V1 ships 11 currencies (EUR / USD / GBP / CHF / etc.); FX conversion is Phase 3.
CounterpartyNoFree-text. Auto-complete suggests prior counterparties.
Counterparty IBANNoIf supplied, used by the auto-match engine as a strong signal.
PurposeNoUp to 500 characters with a live counter. Matches your bank's Verwendungszweck field.
AccountYesDefaults to your most-recently-used account.
Value dateNoOptional — bank's Wertstellungsdatum. Defaults to booking date when blank.
NotesNoInternal-only — not part of any export.

Duplicate warning. If a transaction with the same amount + counterparty exists on the same date (± 1 day), a soft warning appears: "A transaction with the same amount and counterparty already exists on {date}. Save anyway?" You can save through it or cancel. The warning is information-only — there is no hard block, because legitimate same-day, same-amount duplicates do exist (e.g., two parking fees at €5 in the same garage on the same day).

Save + Add another keeps the modal open and pre-fills the next row with the same account + counterparty, useful for batch-entering a stack of paper deposit slips.

CSV import

Click Import CSV in the header. The wizard has four steps:

Step 1 — Pick a file

Drag-and-drop or click. Max size 10 MB (≈ 50 000 rows; if your statement is larger, split it). Supported encodings: UTF-8, UTF-8-BOM, Windows-1252 (auto-detected). The file is parsed locally — nothing leaves your browser.

Step 2 — Map columns

If your bank's format is in the 13-preset library (Sparkasse, Volksbank, Commerzbank, Deutsche Bank, DKB, Postbank, comdirect, ING, HypoVereinsbank (HVB), N26, Revolut, Wise, Trade Republic), the wizard auto-detects it and skips this step (a banner shows "Detected: {preset}").

For unknown formats, the wizard shows your first 5 rows and asks you to map each required column to a target field via dropdowns:

Target fieldRequiredMaps from
Booking dateYesBuchungstag / Booking date / Posted date
AmountYesBetrag / Amount
CounterpartyYesEmpfänger / Auftraggeber / Counterparty
PurposeNoVerwendungszweck / Memo / Reference
Counterparty IBANNoIBAN / Account
CurrencyNoWährung / Currency
Value dateNoWertstellung / Value date
End-to-end IDNoUsed for cross-bank deduplication

The preset library is opinionated — if you find a bank format we don't yet support, file an issue with two anonymized sample rows and we can add it in a patch release.

Step 3 — Preview

The wizard shows you three counts:

  • {n} new rows that will be imported.
  • {n} already in database that will be skipped (matched by stable dedup hash on bank + amount + date + IBAN + end-to-end ID).
  • {n} parse errors with a per-row breakdown and a Download errors.csv button so you can fix the source and retry just the broken rows.

A Force re-import duplicates toggle is available for power users who know they need to re-import (e.g., the bank issued a corrected export). Don't use it casually — it creates linkable duplicates.

Step 4 — Confirm

Click Import {n} rows. The page shows a toast: "Imported {imported}, skipped {skipped}, auto-matched {matched}, suggested {suggested}" and the list refreshes. Imported rows immediately go through the auto-match engine; high-confidence matches flip to Matched (linking their invoice / expense automatically), medium-confidence become Suggested, and the rest stay Unmatched.

camt.053 (ISO 20022) XML import

Most German banks expose camt.053 in their online banking under "Kontoauszug → XML-Format" or via your tax advisor. The format is standardized, structured, and includes a closing balance that the import validates against — if you accidentally edited a row in your CSV / camt file, the import catches the drift.

Click Import camt.053 in the header. Pick the XML file (max 25 MB). The wizard validates the file structure (BkToCstmrStmt root, correct namespace) and shows:

  • The statement period (FrToDt).
  • The opening + closing balance (Bal blocks).
  • The number of entries (Ntry count).

If the closing balance matches the sum of entries on top of the opening balance, you see a green ✓ Closing balance matches. If it doesn't, you see a red banner: "Closing balance mismatch by €{delta}" with a Continue anyway option (use cautiously — your statement was likely edited or the file is corrupted).

V1 ships camt.053**.001.02** parsing specifically — v04 / v08 (which add SCT Inst, structured remittance, and additional fields) are tracked as Phase 3 follow-ups. If your bank only exports v04 (less common in Germany), file an issue and we'll prioritize.

Bank sync (FinTS)

Connect the bank once and let it fetch statements — no CSV in the loop. Settings → Accounting → Banking, under Bank sync.

Where it is available

The connection speaks HBCI/FinTS to your bank from your own device, so availability follows the build you are running:

PlatformBank sync
macOS desktop app✅ available (shipped v0.408.0)
Android app✅ available
iOS appdeliberately hidden — the gate begins !isIos(), an App Store compliance decision, and it overrides the native-platform branch
Web browser❌ for ordinary accounts — use CSV / camt.053 import instead

This guide listed iOS among the platforms where the connection runs "directly on your device" until 2026-09-07. It is the one platform where the feature is switched off on purpose, so an iPhone user following that line went looking for a control that is not there.

What a connection does

Search for your institution → connect → answer the TAN challenge → pick accounts → transactions are imported and flow into the same list, matching and linking described above. Later you can refresh, reconnect, revoke, or check status from the same section.

TAN methods: chipTAN, photoTAN, pushTAN, and decoupled/app-based approval (DKB-App, Banking-to-go and friends).

Re-authentication. Banks expire an SCA session — 150 days on most, and the app treats ING more conservatively at 75. When it lapses you get a re-TAN prompt in the banking list rather than a silent failure.

Credentials and where the data lives

Connection credentials are encrypted at rest on the device and are never synced. On a native build, a non-owner's banking data is likewise local to that device — the sync engine excludes the banking entity types outright (bankingSyncGate / BANKING_LOCAL_ONLY_TYPES), so it does not reach the cloud at all. Encrypted cloud sync of banking data is a separate, unstarted piece of work.

If your bank refuses the connection

Acceptance is a per-institution decision, not a single global switch: the product registration is live, but each bank allow-lists it on its own schedule. If yours rejects the connection, that is the likely reason — CSV and camt.053 import keep working in the meantime.

Linking transactions to invoices / expenses

Linking is how Banking turns "money landed" into "invoice paid". Click any unmatched / suggested row to open the detail drawer, then Link in the actions bar. The link modal opens with three tabs:

  1. Invoices — your outstanding invoices, ranked by match score.
  2. Expenses — your unbilled / unpaid expenses, ranked by match score.
  3. Mahnungen — invoices in Mahnung stages 1 / 2 / 3 (or Zahlungserinnerung stage 0) — linking here cancels the scheduled next-stage Mahnung.

Each candidate shows:

  • The invoice number / expense name + counterparty + open amount.
  • A score chip (0–100) summarising why the engine ranked it where it did.
  • An IBAN warning if the transaction's IBAN differs from both the contact's current IBAN AND the invoice's finalized IBAN.

Allocated amount

Below the candidate list is an Allocate amount input. By default it's set to either:

  • The transaction's full amount (if it's ≤ the candidate invoice's open amount), OR
  • The invoice's open amount (if the transaction is larger — typical when one bank transfer pays multiple invoices, see below).

You can override it to allocate a partial amount. The remaining amount stays on the transaction for additional linking.

Auto banners

The modal shows one of these context banners based on what the link will do:

  • "Linking will mark this invoice as paid." — full allocation closes the invoice.
  • "Linking will mark this invoice as partially paid (€{remaining} remaining)." — partial allocation moves it to the new partially_paid status.
  • "Linking will mark {invoiceNumber} as paid and cancel scheduled stage-{n} Mahnung." — for Mahnung-tab links.
  • Account mismatch warning — IBAN differs from both contact + invoice. You can still link, but review carefully (might be a wire from a different bank or a related-but-separate entity).

Skonto detection

If the linked invoice has a Skonto policy (2 % within 7 days) and the transaction was booked within the Skonto window AND the amount is consistent with the discounted total (within €0.01), the modal surfaces: "Skonto detected — link with €{discountAmount} discount?" with a Link with Skonto primary action. This automatically writes off the discount portion to the SKR04 Skonto account (4731 — Gewährte Skonti) and marks the invoice paid in full.

The detection respects § 14 Abs. 4 Nr. 7 UStG — Skonto only applies when the payment landed within the discount window. Out-of-window payments fall back to literal-amount partial matching (so a 98% payment after the window becomes partially_paid, NOT auto-discount).

One transaction, many invoices (1:N)

Many bank transfers pay multiple invoices in one wire (typical when a client batches their AP runs). After your first link, the transaction stays in the detail drawer with a positive remaining amount; click Link again to add a second invoice. The junction table tracks each link with its own allocatedAmount, and the transaction status stays Matched as long as it has at least one link AND its total allocation matches its amount (within €0.01).

Creating a new invoice from a transaction

If the money landed for work you forgot to invoice, click + Create new invoice from this transaction at the bottom of the link modal. This opens the standard invoice editor with the transaction's counterparty + amount pre-seeded; finalize as normal and the link snaps in automatically once the new invoice has a number.

Transaction detail drawer

Click any row to open the drawer. It shows all 11 fields (booking date / value date / amount / currency / counterparty / IBAN / purpose / source / account / notes / end-to-end ID), with editing disabled when the transaction is linked to a paid invoice (a banner explains: "This transaction is locked because it's linked to a paid invoice. Un-link to edit.").

Linked Bank Transactions section

If linked, the drawer shows each link with its allocated amount, the target invoice / expense / Mahnung, and a per-link Unlink button. Unlinking reduces the target's paidAmount, which may flip its status back from paidpartially_paidsent.

Actions

  • Link / Re-link / Unlink — see above.
  • Mark ignored — moves to Ignored status, hides from default list.
  • Delete — soft delete (10-year retention guard for imported / linked rows).
  • Attach receipt — upload a PDF / image attachment, useful for cash withdrawals that need a paper trail.

"Why this match?"

For Suggested / Matched rows, the Why this match? expander shows a per-criterion breakdown:

CriterionWeightHitContribution
Amount exact40 %✓ (€1,234.56 = €1,234.56)40 %
Reference match25 %✓ (purpose contains "INV-2026-42")25 %
Counterparty20 %~ (75 % similarity)15 %
Date window10 %✓ (within 30 days)10 %
IBAN match5 %0 %
Total90 %

This is your audit trail — useful when an external auditor asks "why did you book this payment against that invoice".

Raw source viewer

For CSV-imported rows, you can see the original CSV line. For camt.053-imported rows, you can see the <Ntry> XML fragment. This is your paper trail per GoBD if a Betriebsprüfer asks for original document data.

Reversal pairing (Rücklastschriften)

When a SEPA direct debit bounces or a wire gets recalled, your bank books a reversal — a positive entry that cancels the original negative entry (or vice versa). Banking automatically detects this pairing:

  • Sign-flip (one positive, one negative).
  • Amount equality (exact or within €0.01).
  • Fuzzy match on counterparty + purpose (≥ 0.92 similarity).

When detected, both rows show a notice banner:

  • On the original: "This transaction was reversed."
  • On the reversal: "This is a reversal."

Either banner has an Un-pair action if the detection was wrong (rare but possible — e.g., a coincidental same-amount, same-counterparty refund that's actually a separate event).

When a reversal pairs to a linked-and-paid invoice, the system asks you (via toast) whether to un-link the original payment automatically — your accountant typically wants this so the invoice flips back to sent / overdue, but it's not automatic to avoid silent surprises.

Partial-payment status (partially_paid)

When a transaction's allocated amount is positive but less than the invoice's total (e.g., €500 paid against a €1 000 invoice), the invoice transitions to a new status:

sent  ──link partial──▶  partially_paid  ──link remainder──▶  paid
                              │
                              └──un-link────────────────────▶  sent  (or overdue if past dueDate)

In the invoice list, partially_paid shows as an info-tone badge (between sent/overdue's amber and paid's green). The Mahnung badge automatically suppresses when paidAmount >= total × 0.999 (covering rounding + FX micro-deltas).

Verzugszinsen on partial-paid invoices

If you issue a Mahnung against an invoice that's already partially paid, the § 288 BGB Verzugszinsen is computed pro-rata on the open balance (total - paidAmount) at the time of the Mahnung. Once the Mahnung is issued, that Verzugszinsen amount is historical-locked — additional payments after issuance reduce only the principal, and the principal reduction shows on the NEXT Mahnung's recomputation.

This matches how German Steuerberater handle the case: each Mahnung is a frozen snapshot; new partial payments don't retroactively change historical Mahnung amounts.

Settings — Banking sub-tab

Settings → Accounting → Banking is where you tune the engine and manage your bank accounts.

Bank Accounts

The accounts list shows display name, IBAN (optional, format-validated against country-specific rules), BIC (optional), currency, and opening balance + as-of date. Add / edit / archive from here. Archiving an account hides it from new transactions but preserves history.

Duplicate-IBAN warning: if another active account already has the IBAN you're entering, a soft warning appears — useful to catch typos, but you can continue if you genuinely have two accounts at the same bank with cleared identical-but-different IBANs (rare but possible).

Five sliders (0–100, must sum to 100; the UI normalizes on save):

  • Amount — default 40
  • Reference — default 25 (matches purpose against your invoice numbers + customer references)
  • Counterparty — default 20
  • Date — default 10
  • IBAN — default 5

The defaults are tuned against a freelancer-payment-heavy dataset. If you primarily get paid by a small group of repeat clients, you may want to bump Counterparty to 30 and drop Reference to 15. Restore defaults resets all five.

A single Auto-link enabled switch. When on, transactions matching ≥ 80 % score auto-flip to Matched at import time. When off, every transaction goes to Suggested instead, and you confirm each one manually. The engine still computes scores either way — the toggle only affects the auto-promotion step.

Important: Turning auto-link off does NOT disable the engine. Scores still show in the "Why this match?" panel and in the Suggested rankings. This makes it useful as a power-user mode if you want to review every match before it lands.

Match rules

Settings → Accounting → Banking → Custom match rules is a working editor (REQ-BANK-294). Add, edit and delete your own rules — "if counterparty contains 'Dropbox' and amount = €9.99, auto-link to expense category 6815" — and they take effect on the next match run. Rules are stored as a synced entity, so they follow you to every signed-in device.

This section read "Coming soon — power-user rules editor" until 2026-09-10. The editor shipped; the guide had not caught up.

Dashboard widget

The Accounting Dashboard (/accounting) shows a Banking tile alongside the outstanding-invoices tile:

  • Unmatched — count of rows needing your attention.
  • Inflow this month — sum of positive amounts in the current calendar month.
  • Outflow this month — sum of negative amounts in the current month.

Click the tile to jump to the filtered Banking page (Unmatched + current month for the first metric; current month + sign filter for the other two).

Quick Switcher (Cmd+K)

Bank transactions are searchable from the Quick Switcher. Type a partial counterparty, an amount, or an invoice number you remember linking — matching transactions show up in the result list with a small banknote icon. Hit Enter to jump directly to the detail drawer.

Exports

DATEV CSV

Two CSVs for your Steuerberater:

  • transactions.csv — the raw transaction ledger in DATEV column order (Buchungstext / Belegfeld 1 / Belegfeld 2 / etc.).
  • links.csv — the junction-table contents (transaction ID, invoice / expense ID, allocated amount, link date, link user).

GoBD Z3 export

Banking transactions are included in the GoBD Z3 export ZIP. There is no settings page for it — it is a panel, Settings → Security & Privacy → Privacy & Legal → GoBD Z3 export, where you pick the period and it builds a tax-audit ZIP (an index.xml plus six CSV tables, per BMF GoBD § 147 AO). Don't confuse it with Download tax records (GoBD) in the Danger Zone's backup block — that one is the whole-account dump offered before you delete your account, not a period export. The ZIP includes a bank_transactions.csv table per the BMF GDPDU DTD. If you're going through a Betriebsprüfung, hand this to the prüfer and they'll have everything they need.

Privacy + retention

  • PII redaction. When you anonymize a contact (Settings → Security & Privacy → Privacy & Legal → Anonymize, scoped to a contact), Banking redacts the counterparty + IBAN + purpose on every linked bank transaction. The redacted rows stay on disk (10-year retention) but show as [anonymized] in the UI and exports.
  • 10-year retention. Per § 257 HGB + § 147 AO, imported bank transactions are retention-locked for 10 years from the booking date. You cannot delete them before that. The trigger is enforced both client-side (delete button hidden / warns) AND server-side (Postgres trigger rejects).
  • GDPR exports. The standard data-export ZIP includes a banking/ folder with your transactions + accounts + links + match events.

Paywall

Banking is gated behind the same isAccountingEnabled(uid) check as the rest of Accounting (Invoices, Expenses). Owners and Accounting subscribers see the full surface; everyone else gets a locked empty state at /accounting/banking with an upgrade CTA. Edge functions also re-validate the entitlement so a bypass attempt at the URL level fails with a clean 403, not a silent data leak.

Limitations + roadmap

V1 is the manual + import path. The bigger picture:

  • FinTS / HBCI 3.0 auto-sync — shipped, see Bank sync (FinTS). Not a roadmap item any more.
  • E2E-encrypted cloud sync of banking data — not started. Today a non-owner's banking data on a native build stays local to that device; it is deliberately excluded from the sync outbox.
  • FinAPI add-on (deferred, only on demand) — for N26 / Revolut / Wise / Trade Republic / Tomorrow / Vivid, i.e. banks FinTS does not reach.
  • Multi-currency FX conversion + project P&L — Phase 3.
  • xlsx import, camt.053 v04 / v08, saved-filter management, running-balance column, month-end reconciliation report, bank-fee / interest auto-categorization, per-user weight learning from match events, rules editor UI, recurring-payment detection — Phase 3 or separate SDD cycles.

Everything above that is still open is demand-driven — file a feature request if one of them is blocking you. Do not file one for FinTS auto-sync; it is already there.

  • Invoices — for status machine, Mahnwesen, Storno
  • Accounting — for the broader Accounting domain + Expense Tracking + Bewirtungsbeleg
  • Data Storage — for the local-first IDB + OPFS + Supabase triple-store model

Last Updated: 2026-09-10 Version: 0.790.2