feat(import): support Wise balance statements (#1368)

* 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>
This commit is contained in:
Mattsson
2026-08-03 17:55:09 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 0510d4c13f
commit 24911abde0
12 changed files with 741 additions and 23 deletions
+10 -5
View File
@@ -35,7 +35,8 @@ export default function BankFilePreviewStep({
onBack,
}: BankFilePreviewStepProps) {
const { transactions, stats, issues, date_from, date_to } = parseResult
const hasIssues = issues.filter((i) => i.severity === 'error').length > 0
const errors = issues.filter((i) => i.severity === 'error')
const hasIssues = errors.length > 0
const warnings = issues.filter((i) => i.severity === 'warning')
// Wise/camt.053 files can mix currencies per row: the parser-level totals
// sum across currencies, so income/expenses are grouped per currency here.
@@ -190,11 +191,15 @@ export default function BankFilePreviewStep({
<CardContent className="pt-6">
<div className="flex gap-3">
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<div className="min-w-0 space-y-2">
<p className="font-medium text-destructive">Filen innehåller fel som förhindrar import</p>
<p className="text-sm text-muted-foreground mt-1">
Kontrollera felet ovan och försök ladda upp en korrigerad fil.
</p>
<div className="max-h-32 space-y-1 overflow-y-auto">
{errors.map((issue, i) => (
<p key={i} className="text-xs text-muted-foreground">
Rad {issue.row}: {issue.message}
</p>
))}
</div>
</div>
</div>
</CardContent>