Import ING and Kontist statements behind a reviewed column mapping

CSV import is now mapping-driven: N26, ING (metadata preamble, Windows-1252,
German decimals) and Kontist exports are recognized locally, and any other
layout can have its columns proposed by the configured model from a sample in
which letters are replaced by x and digits by 0. Proposals are untrusted: every
column must name a supplied header, money must come from one signed column or
one debit/credit pair, and formats must be from a closed list.

Uploading no longer imports. /api/import is replaced by prepare/confirm/cancel:
prepare parses, deduplicates and previews the exact facts, and only confirming
at the reviewed revision writes them. ING and AI-mapped facts carry no
transaction reference, because repeating SEPA mandate references must never
become a transaction identity.
This commit is contained in:
Lars Nolden
2026-09-11 17:49:03 +02:00
parent 6f791b1277
commit dc767799bc
16 changed files with 2182 additions and 301 deletions
+15 -3
View File
@@ -2,7 +2,7 @@
A self-hosted personal finance dashboard with a **Go backend**, **React frontend**, and **DuckDB analytics**. Human-readable `.finance` journals are the source of truth; DuckDB is a disposable index.
Imported bank facts are separate from editable merchant, category, and tag classifications. Imports work without AI. Optional OpenRouter enrichment uses restrictive provider routing and omits amounts by default.
Imported bank facts are separate from editable merchant, category, and tag classifications. N26, ING, and Kontist CSV imports and bank synchronization work without AI. Optional OpenRouter enrichment uses restrictive provider routing and omits amounts by default, and can map the columns of an unrecognized CSV layout from a redacted sample.
> **There is no application login.** Keep Finance Duck behind your VPN. The default service and Docker Compose port bindings are loopback-only. Setting a hostname does not provide authentication or firewall protection.
@@ -18,7 +18,7 @@ go build -o bin/finance-duck ./cmd/finance-duck
./bin/finance-duck -data ./finance
```
Open **http://localhost:8080**. For CSV imports, create a local account in **Accounts**, then import an N26 statement. For automatic bank imports, follow the setup below; authorization discovers the bank's accounts for you.
Open **http://localhost:8080**. For CSV imports, create a local account in **Accounts**, then import an N26, ING, or Kontist statement. For automatic bank imports, follow the setup below; authorization discovers the bank's accounts for you.
Build React before building Go: its production assets are embedded in the binary. `shell.nix` supplies Go, Node.js, and the CGO toolchain needed by DuckDB. Node.js is not needed at runtime.
@@ -129,6 +129,8 @@ Stop the old process before restarting; only one process may write a finance dir
For Docker, use the container-side key path and the configuration in [Docker Compose](#docker-compose).
### 5. Authorize from the UI
**Before continuing:** link every account you want to use in Finance Duck through the Enable Banking control panel. The **Connect your bank** flow cannot connect an account until it has been linked to the Enable Banking application there.
Open **Accounts → Connect your bank**:
@@ -166,6 +168,16 @@ Renewal preserves local account identities and existing transaction history. It
Reconnecting renews bank consent, not your application registration. Correct certificate registration in the Enable Banking control panel and update the matching private key in **Settings** (or your optional environment-managed setup).
## Import a CSV statement
Open **Accounts → Import a statement**, choose the account, select the export, and click **Review statement**. Uploading imports nothing: it parses the file and opens a review dialog showing the detected export, the column mapping, how many records are new or already imported, and a sample of the parsed transactions with their dates, descriptions, counterparties, and signed amounts. **Import N transactions** commits exactly those records; **Cancel**, a reload, or a journal change in between commits nothing.
**N26**, **ING** (Umsatzanzeige, including its metadata preamble and Windows-1252 encoding), and **Kontist** exports are recognized on your own machine, with no AI involved. Comma, semicolon, and tab separators, UTF-8 with or without BOM, CRLF, quoted multiline descriptions, ISO and German dates, and both decimal separators are accepted. Use the bank's original export rather than a spreadsheet-reformatted copy. Uploads are limited to **2 MiB**, and a prepared statement expires after **one hour**.
Any other layout needs a saved OpenRouter key and model, which maps the **columns** rather than reading the transactions: the request carries the delimiter, the column names, and up to four sample rows in which every letter is replaced by `x` and every digit by `0`. Descriptions, counterparties, references, IBANs, and amounts are never sent. The proposal must name existing columns, choose exactly one money convention (one signed amount column, or a debit and credit pair), and use a supported date and decimal format; anything else is rejected instead of guessed. Because a proposed mapping can still be wrong, check the sample's dates, signs, and currency before confirming.
Reimporting the same statement adds nothing: the review dialog reports the overlap as already imported. A statement whose currency conflicts with the account, or whose records cannot be parsed, is rejected whole rather than imported in part.
## Deployment options
| Option | Best fit | Included support |
@@ -361,7 +373,7 @@ The key is stored separately from preferences and journals in `state/openrouter.
For administrator-managed startup configuration, `OPENROUTER_API_KEY` in the server environment or Compose `.env` remains an optional fallback **only while no saved credential file exists**. A UI-saved key takes precedence; **Remove key** persists an explicit disable, so a restart cannot silently restore the environment key. Environment-only changes require a restart/recreate.
Bank imports do **not** require this key. Without AI, explicit merchant-default rules still work; unresolved transactions remain unclassified and editable.
Bank synchronization and recognized N26, ING, and Kontist CSV imports do **not** require this key; only mapping an unrecognized CSV layout does. Without AI, explicit merchant-default rules still work; unresolved transactions remain unclassified and editable.
Every AI classification requests `provider.data_collection = "deny"`, `provider.zdr = true`, and `provider.require_parameters = true`. Unsupported private routing fails rather than falling back to a less restrictive provider. Amount sharing is off by default. Keep OpenRouter account prompt logging disabled as well. Automatic redaction minimizes data; it is not a guarantee that arbitrary transaction prose is anonymous.