From 93f81f03e847dbc4860ce2645452688251600cef Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:07:14 +0200 Subject: [PATCH] feat(providers): WINT migration provider behind WINT_MIGRATION_ENABLED (#1446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 --------- Co-authored-by: Claude Fable 5 --- DECISIONS.md | 1 + app/(dashboard)/import/page.tsx | 1 + .../general/ArcimMigrationWorkspace.tsx | 63 ++- extensions/general/arcim-migration/index.ts | 21 +- .../lib/__tests__/provider-client.test.ts | 131 +++++- .../lib/__tests__/sie-fetcher.test.ts | 166 ++++++++ .../arcim-migration/lib/provider-client.ts | 74 +++- .../arcim-migration/lib/sie-fetcher.ts | 166 +++++++- .../general/arcim-migration/manifest.json | 1 + extensions/general/arcim-migration/types.ts | 6 +- lib/import/__tests__/sie-parser.test.ts | 10 + lib/import/sie-parser.ts | 6 +- .../__tests__/resolve-consent.test.ts | 48 +++ lib/providers/oauth-config.ts | 11 +- lib/providers/provider-data-fetcher.ts | 31 ++ lib/providers/resolve-consent.ts | 5 + lib/providers/types.ts | 8 +- lib/providers/wint/__tests__/client.test.ts | 104 +++++ lib/providers/wint/__tests__/mapper.test.ts | 161 ++++++++ lib/providers/wint/__tests__/oauth.test.ts | 137 +++++++ .../wint/__tests__/sie-builder.test.ts | 275 +++++++++++++ lib/providers/wint/client.ts | 168 ++++++++ lib/providers/wint/config.ts | 56 +++ lib/providers/wint/mapper.ts | 246 ++++++++++++ lib/providers/wint/oauth.ts | 153 +++++++ lib/providers/wint/sie-builder.ts | 376 ++++++++++++++++++ public/logos/wint.svg | 4 + 27 files changed, 2400 insertions(+), 29 deletions(-) create mode 100644 lib/providers/wint/__tests__/client.test.ts create mode 100644 lib/providers/wint/__tests__/mapper.test.ts create mode 100644 lib/providers/wint/__tests__/oauth.test.ts create mode 100644 lib/providers/wint/__tests__/sie-builder.test.ts create mode 100644 lib/providers/wint/client.ts create mode 100644 lib/providers/wint/config.ts create mode 100644 lib/providers/wint/mapper.ts create mode 100644 lib/providers/wint/oauth.ts create mode 100644 lib/providers/wint/sie-builder.ts create mode 100644 public/logos/wint.svg diff --git a/DECISIONS.md b/DECISIONS.md index fb3fd4ff..4becac2e 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -815,6 +815,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-06] Sandbox ledger history marked no_doc_required instead of seeding receipt documents: the history represents books kept before the company arrived in Accounted, so its underlag sits in the previous system. Same rationale and same sidecar table the SIE-import opt-in uses. Without it the demo's first screen read "Verifikat utan underlag: 39". [2026-08-06] Sandbox payroll takes skatteavdrag from FALLBACK_TAX_TABLES_2026 rather than a flat schablon: the draft run ships calculated, so its live "Beräkna om" would have jumped ~4 600 kr away from the sibling booked run, and a wrong skatteavdrag would show unlabelled in the payslip, the 2710 line and the AGI figures. [2026-08-06] Added guardSandbox to /api/salary/runs/[id]/payslips/send: it was the only send path without one, and seeding a booked salary run put "Skicka lönebesked" one click from an anonymous visitor with live Resend behind it. +[2026-08-06] WINT provider built Tier A only (partner-facing v1 endpoints, SIE rendered by our own sie-builder from /api/Voucher + /api/Account): their native SIE export and IncomingInvoice live in the internal Full spec and are deliberately unused until WINT grants partner access. Ships dark behind WINT_MIGRATION_ENABLED. Auth is credential pass-through (mail+password exchanged once for a JWT pair; password never stored) because WINT has no OAuth or API keys. [2026-08-06] Login credentials error says "Fel e-postadress eller lösenord", not "Fel lösenord": GoTrue returns one invalid_credentials code for unknown-email and wrong-password alike (anti-enumeration), so a "wrong password" claim would be both unknowable and an account-existence leak. Clarity comes from inline placement + reset link instead. [2026-08-06] Empty SIE file (0 parsed vouchers AND no raw #VER declaration) finalizes as completed no-op, not failed: Fortnox exports an empty file for a not-yet-booked fiscal year and failing it aborted the whole migration wizard (CashLeads case). The failed-downgrade now fires only when the file contained vouchers that could not be imported; the raw-content #VER cross-check must stay, since a separator/encoding mismatch can swallow every #VER block with only a warning-severity parse issue and would otherwise masquerade as a legitimate empty year. The balance-only continuation-guard scenario rides along as no-op since re-running the same file can never produce a different outcome. [2026-08-06] Bucket A defaults pass commits the /pending Godkänn pill directly for low/medium risk and keeps the ConfirmationDialog only for high risk: the Granskning row already states source, title, risk and offers Detaljer, so the dialog's second Godkänn restated the row (the audit's expert lens called double-Godkänn the thing professionals do not tolerate). The chat-side "Godkänn alla N" was DEFERRED, not built: ApprovalCard owns its whole state machine internally (commit fetch, account-activation retry, typed high-risk confirm) and a bulk commit from AgentChat would leave committed cards rendering as pending; that is assistant-redesign seam 8.8 (approval batching) and needs the state lifted, not a button. diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index cbc4bf7d..d5dc579b 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -2135,6 +2135,7 @@ export default function ImportPage() { + } disabled={isSandbox} diff --git a/components/extensions/general/ArcimMigrationWorkspace.tsx b/components/extensions/general/ArcimMigrationWorkspace.tsx index f5c8432d..c2a3222b 100644 --- a/components/extensions/general/ArcimMigrationWorkspace.tsx +++ b/components/extensions/general/ArcimMigrationWorkspace.tsx @@ -40,18 +40,21 @@ import { } from 'lucide-react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' -type ArcimProvider = 'fortnox' | 'visma' | 'briox' | 'bokio' | 'bjornlunden' +type ArcimProvider = 'fortnox' | 'visma' | 'briox' | 'bokio' | 'bjornlunden' | 'wint' // `sieViaApi`: the provider serves its general ledger as SIE over the API: // no manual SIE upload needed. Deliberately duplicated from // extensions/general/arcim-migration/types.ts (core code must not import from // @/extensions/: CI enforces it). Keep both lists in sync. +// WINT is env-gated server-side (WINT_MIGRATION_ENABLED): the wizard renders +// whatever GET /providers returns, so no client-side gate is needed here. const ARCIM_PROVIDERS: { id: ArcimProvider; name: string; authType: 'oauth' | 'token'; sieViaApi: boolean }[] = [ { id: 'fortnox', name: 'Fortnox', authType: 'oauth', sieViaApi: true }, { id: 'visma', name: 'Visma', authType: 'oauth', sieViaApi: false }, { id: 'bokio', name: 'Bokio', authType: 'token', sieViaApi: false }, { id: 'bjornlunden', name: 'Björn Lundén', authType: 'token', sieViaApi: true }, { id: 'briox', name: 'Briox', authType: 'token', sieViaApi: true }, + { id: 'wint', name: 'WINT', authType: 'token', sieViaApi: true }, ] /** @@ -253,7 +256,10 @@ interface ConnectionStatus { } } -const COMING_SOON_PROVIDERS = new Set([]) +// WINT shows as a disabled "Kommer snart" card until the integration is +// verified against a live WINT account. Launch = remove it here AND set +// WINT_MIGRATION_ENABLED=true (the server-side /connect gate). +const COMING_SOON_PROVIDERS = new Set(['wint']) const PROVIDER_LOGOS: Record = { fortnox: '/logos/fortnox.svg', @@ -261,6 +267,7 @@ const PROVIDER_LOGOS: Record = { bokio: '/logos/bokio.png', bjornlunden: '/logos/bjornlunden.png', briox: '/logos/Briox_logo.png', + wint: '/logos/wint.svg', } function ProviderStep({ @@ -291,7 +298,7 @@ function ProviderStep({

SIE-import krävs först

- Bokio och Visma hämtar endast kunder, leverantörer och fakturor via API:et. Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil först. Gäller inte Fortnox, Briox och Björn Lundén: där hämtar vi SIE direkt via API:et. + Bokio och Visma hämtar endast kunder, leverantörer och fakturor via API:et. Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil först. Gäller inte Fortnox, Briox, Björn Lundén och WINT: där hämtar vi bokföringen direkt via API:et.

{tokenHelpText}

-
+ {/* WINT is a login form: e-mail reads above password (CSS order; + the button keeps its place). Other token providers keep + token-first order. */} +
{needsApiToken && ( -
+
setApiToken(e.target.value)} />
)} {needsCompanyId && ( -
+
setCompanyId(e.target.value)} @@ -648,7 +677,7 @@ function ConnectStep({
)}