* feat(import): support Wise balance statements
* fix(import): fail closed on ambiguous Wise rows
* fix(import): guard Wise statement netted-fee assumption with running-balance continuity check
Swedish accounting review asked whether balance-statement Total fees is
netted into Amount. It is: Running Balance moves by exactly the signed
Amount per row, so a separate fee row would double-count the cost. Codify
the assumption with a pairwise continuity warning (order-agnostic, chain
resets across skipped rows) and document the decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(api): cap bank-import validation payload and harden issue assertion
CodeRabbit review: bound the VALIDATION_ERROR issues array to 20 entries
with issue_count carrying the full total, so a large malformed file cannot
balloon the response or log sink. Gate stays format-agnostic on purpose:
error severity means do-not-ingest for every parser, and no non-Wise parser
emits per-row errors alongside parsed transactions today.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(import): add Wise (TransferWise) CSV import format
Wise exports a single multi-currency transaction history (one row per balance
movement). Add it as a bank-file format plugin so it flows through the existing
upload -> preview -> confirm -> execute wizard.
- lib/import/bank-file/formats/wise.ts: quote-aware parse (dates contain a
space), Direction IN/OUT drives the sign, booked on the moved side (target
for IN, source for OUT). Native currency preserved; SEK conversion is left to
the downstream FX/booking pipeline (Riksbanken).
- Non-zero Wise fees become their own negative "Wise avgift" row (source and
target), so the fee books separately and the balance ties out.
- Only COMPLETED rows import. external_id keys on the stable Wise ID
(TRANSFER-/PLAN_ORDER-, -fee suffix for fee rows) via a new 'wise' branch in
generateExternalId, so re-imports dedup exactly.
- Register the format (types, parser list), add it to the manual-format picker
and the v1 /imports/bank format enum.
Tests cover detection, IN/OUT signing + currency, fee splitting, stable
external_id, and COMPLETED-only filtering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>
* fix(import): harden Wise parser against malformed rows (CodeRabbit #1018)
- Strict amount parsing: reject "12abc"/"1,234" instead of parseFloat coercing
them to 12/1 and silently corrupting the imported amount.
- Require Status to be exactly COMPLETED: a blank/missing status no longer
slips through the completed-only filter.
- Fail hard on an unsupported Direction: a blank or non-IN/OUT value (e.g.
NEUTRAL for a balance conversion) throws instead of being guessed as income;
the parse route surfaces it as BANK_FILE_PARSE_FAILED. Proper conversion
support is tracked in #1019.
- Never invent currencies: a missing movement currency skips the row with a
warning (no SEK default), and a fee with no currency of its own is dropped
with a warning rather than inheriting the movement currency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>
---------
Signed-off-by: Alexander Reinthal <email@reinthal.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>