feat(providers): WINT migration provider behind WINT_MIGRATION_ENABLED (#1446)

* feat(providers): WINT migration provider behind WINT_MIGRATION_ENABLED

Adds WINT (wint.se) as a sixth migration provider, built against the
OpenAPI specs WINT's own API host serves publicly. Tier A scope: only the
partner-facing v1 endpoints are used; the general ledger is fetched as
vouchers/accounts and rendered as SIE 4E by our own sie-builder, with
opening balances for earlier years derived backward from the current-year
Ib anchor. Auth is the user's WINT login exchanged once for a JWT pair;
the password is never stored.

Ships dark: the wizard shows a disabled "Kommer snart" card, and the
server-side /connect gate rejects WINT until WINT_MIGRATION_ENABLED=true.
Live verification against a real WINT account is still outstanding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(providers): harden WINT provider per PR #1446 review findings

Addresses CodeRabbit and Swedish accounting review feedback in one pass:

- Ib anchor selection now uses WINT's unfiltered fiscal-year list, so an
  active year outside the allowed import window can never silently anchor
  the wrong year; the voucher chain is extended through the anchor and a
  per-year fetch failure fails that year loudly instead of sinking the
  whole migration.
- Auth token exchange is strict: only LoginState Success with a complete
  access+refresh pair mints a consent (a pair without a refresh token is
  unrefreshable and would break days later).
- WintApiError no longer retains full response bodies (bounded 300-char
  diagnostic; bodies can carry customer data and errors get logged).
- sie-builder refuses to render structurally invalid vouchers (missing
  account number or booking date) and documents deleted-voucher gaps in a
  #PROSA record per BFL 5 kap 6-7 §.
- Account classification: 20xx is equity, 83xx is financial income.
- SIE validator accepts EUBAS97 as BAS-based (standard kontoplanstyp; it
  previously produced a false non-BAS warning on every WINT/Bollbok file).
- New tests: resolveConsent WINT refresh flow, credential upsert payload
  (no mail/password persisted), WINT fetch failure path, EUBAS97 warning
  regression, builder invalid-data rejection, vi.clearAllMocks hygiene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(import): pin EUBAS97 acceptance to the exact SIE spec value

Review follow-up on PR #1446: match EUBAS97 exactly instead of any
EUBAS* prefix, so the non-BAS kontoplan warning stays pinned to the four
kontoplanstyp values the SIE 4B spec enumerates (BAS95, BAS96, EUBAS97,
NE2007) rather than silently accepting unknown future variants.

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-07 11:07:14 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent c0a106e591
commit 93f81f03e8
27 changed files with 2400 additions and 29 deletions
+10
View File
@@ -1099,6 +1099,16 @@ describe('parseSIEFile: tab-separated fields (Bollbok export shape)', () => {
)
})
it('does not warn about a non-BAS kontoplan for `#KPTYP` EUBAS97', () => {
// EUBAS97 is a standard SIE kontoplanstyp (the spec routes every BAS2xxx
// chart through it): flagging it as non-BAS would put a false warning on
// every WINT-rendered and Bollbok file.
const parsed = parseSIEFile(BOLLBOK_TAB_2025_SHAPE)
const validation = validateSIEFile(parsed)
expect(parsed.header.kontoPlanType).toBe('EUBAS97')
expect(validation.warnings.filter((w) => w.includes('inte BAS-baserad'))).toEqual([])
})
it('parses tab-separated #KONTO into accounts (2025 shape)', () => {
const result = parseSIEFile(BOLLBOK_TAB_2025_SHAPE)
expect(result.accounts.map((a) => a.number)).toEqual(
+5 -1
View File
@@ -1072,7 +1072,11 @@ export function validateSIEFile(parsed: ParsedSIEFile): ValidationResult {
// Warn if non-BAS kontoplan declared: mapping logic assumes BAS number ranges
if (parsed.header.kontoPlanType) {
const planType = parsed.header.kontoPlanType.toUpperCase()
const isBAS = planType.startsWith('BAS') || planType === 'EUBAS' || planType === 'EU-BAS'
// EUBAS97 is one of the four kontoplanstyp values the SIE 4B spec
// enumerates (BAS95, BAS96, EUBAS97, NE2007), and the spec routes every
// BAS2xxx chart through it. Matched exactly, not by prefix, so this stays
// pinned to the spec's own table.
const isBAS = planType.startsWith('BAS') || planType === 'EUBAS97' || planType === 'EUBAS' || planType === 'EU-BAS'
if (!isBAS) {
warnings.push(
`Kontoplanstyp "${parsed.header.kontoPlanType}" är inte BAS-baserad. Automatisk kontomappning kan bli felaktig: granska alla mappningar manuellt i nästa steg.`