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:
co-authored by
Claude Fable 5
parent
c0a106e591
commit
93f81f03e8
@@ -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<ArcimProvider>([])
|
||||
// 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<ArcimProvider>(['wint'])
|
||||
|
||||
const PROVIDER_LOGOS: Record<ArcimProvider, string> = {
|
||||
fortnox: '/logos/fortnox.svg',
|
||||
@@ -261,6 +267,7 @@ const PROVIDER_LOGOS: Record<ArcimProvider, string> = {
|
||||
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({
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">SIE-import krävs först</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
<Link
|
||||
href="/import?mode=sie"
|
||||
@@ -500,24 +507,34 @@ function ConnectStep({
|
||||
|
||||
// BL uses server-side client credentials: only needs company ID, no API key
|
||||
const isClientCredentials = provider === 'bjornlunden'
|
||||
// WINT has no API keys: the "token" is the user's WINT login (e-post +
|
||||
// lösenord), exchanged server-side for ett tokenpar; lösenordet sparas aldrig.
|
||||
const isWintLogin = provider === 'wint'
|
||||
const needsApiToken = !isClientCredentials
|
||||
// Briox: the account ID is the `clientid` half of the token exchange
|
||||
const needsCompanyId = provider === 'bokio' || provider === 'bjornlunden' || provider === 'briox'
|
||||
// Briox: the account ID is the `clientid` half of the token exchange;
|
||||
// WINT reuses the same field for the login e-mail.
|
||||
const needsCompanyId = provider === 'bokio' || provider === 'bjornlunden' || provider === 'briox' || provider === 'wint'
|
||||
const companyIdLabel = provider === 'briox'
|
||||
? 'Konto-ID'
|
||||
: provider === 'bjornlunden'
|
||||
? 'Företagsnyckel (User-Key)'
|
||||
: 'Företags-ID'
|
||||
: provider === 'wint'
|
||||
? 'E-postadress'
|
||||
: 'Företags-ID'
|
||||
|
||||
const tokenDescription = isClientCredentials
|
||||
? `Ange din företagsnyckel (User-Key) från Björn Lundén. ${branding.appName.toLowerCase()} ansluter automatiskt via sin integrationspartner-åtkomst.`
|
||||
: provider === 'briox'
|
||||
? `Ange ditt konto-ID och din applikationstoken från Briox för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata.`
|
||||
: `Ange din API-nyckel från ${providerName} för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata.`
|
||||
: isWintLogin
|
||||
? `Logga in med dina WINT-uppgifter för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata. Lösenordet används en gång för att skapa anslutningen och sparas aldrig.`
|
||||
: provider === 'briox'
|
||||
? `Ange ditt konto-ID och din applikationstoken från Briox för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata.`
|
||||
: `Ange din API-nyckel från ${providerName} för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata.`
|
||||
|
||||
const tokenHelpText = isClientCredentials
|
||||
? `Företagsnyckeln (User-Key) är ett GUID som du hittar i Lundify under Integrationer → kugghjulet vid integrationen, eller i aktiveringsmejlet från Björn Lundén.`
|
||||
: provider === 'bokio'
|
||||
: isWintLogin
|
||||
? `Använd samma e-postadress och lösenord som när du loggar in på app.wint.se. Kräver ditt WINT-konto BankID-inloggning kan anslutningen inte skapas ännu: be i så fall WINT om en SIE-fil och importera den manuellt.`
|
||||
: provider === 'bokio'
|
||||
? `Du hittar din API-nyckel i ${providerName} under Inställningar \u2192 Integrationer \u2192 API. Ditt företags-ID är det GUID som syns i URL:en när du är inloggad, t.ex. https://app.bokio.se/ditt-företags-id/settings-r/private-integrations.`
|
||||
: provider === 'briox'
|
||||
? `Skapa din applikationstoken i Briox under Admin \u2192 Anv\u00e4ndare \u2192 kugghjulet vid din anv\u00e4ndare \u2192 Applikationstoken. Ditt konto-ID \u00e4r det l\u00e5nga numret inom parentes bredvid f\u00f6retagsnamnet under "Ditt konto" i menyn till h\u00f6ger.`
|
||||
@@ -609,38 +626,50 @@ function ConnectStep({
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tokenHelpText}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{/* WINT is a login form: e-mail reads above password (CSS order;
|
||||
the button keeps its place). Other token providers keep
|
||||
token-first order. */}
|
||||
<div className={cn('space-y-3', isWintLogin && 'flex flex-col gap-3 space-y-0')}>
|
||||
{needsApiToken && (
|
||||
<div>
|
||||
<div className={cn(isWintLogin && 'order-2')}>
|
||||
<label htmlFor="apiToken" className="text-sm font-medium">
|
||||
{provider === 'briox' ? 'Applikationstoken' : 'API-nyckel'}
|
||||
{provider === 'briox' ? 'Applikationstoken' : isWintLogin ? 'Lösenord' : 'API-nyckel'}
|
||||
</label>
|
||||
<Input
|
||||
id="apiToken"
|
||||
name="apiToken_nocomplete"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder={provider === 'briox' ? 'Klistra in din applikationstoken' : 'Klistra in din API-nyckel'}
|
||||
placeholder={
|
||||
provider === 'briox'
|
||||
? 'Klistra in din applikationstoken'
|
||||
: isWintLogin
|
||||
? 'Ditt lösenord hos WINT'
|
||||
: 'Klistra in din API-nyckel'
|
||||
}
|
||||
value={apiToken}
|
||||
onChange={(e) => setApiToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{needsCompanyId && (
|
||||
<div>
|
||||
<div className={cn(isWintLogin && 'order-1')}>
|
||||
<label htmlFor="companyId" className="text-sm font-medium">
|
||||
{companyIdLabel}
|
||||
</label>
|
||||
<Input
|
||||
id="companyId"
|
||||
name="companyId_nocomplete"
|
||||
type={isWintLogin ? 'email' : 'text'}
|
||||
autoComplete="new-password"
|
||||
placeholder={
|
||||
isClientCredentials
|
||||
? 'Företagsnyckel, t.ex. 1f0e2d3c-4b5a-...'
|
||||
: provider === 'briox'
|
||||
? 'Det långa numret inom parentes, t.ex. 35649125'
|
||||
: 'GUID från URL:en, t.ex. 14ccad83-67f6-49bd-...'
|
||||
: isWintLogin
|
||||
? 'namn@foretaget.se'
|
||||
: 'GUID från URL:en, t.ex. 14ccad83-67f6-49bd-...'
|
||||
}
|
||||
value={companyId}
|
||||
onChange={(e) => setCompanyId(e.target.value)}
|
||||
@@ -648,7 +677,7 @@ function ConnectStep({
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="min-h-11"
|
||||
className={cn('min-h-11', isWintLogin && 'order-3')}
|
||||
onClick={() => onTokenSubmit(apiToken, companyId)}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user