feat: one-click company setup from BankID directorships (#309)

* feat: one-click company setup from BankID directorships

After BankID auth, surface Bolagsverket companies where the user is a
director and provision a fully-configured gnubok company with one click
instead of walking the 4-step wizard. Also exposed via CompanySwitcher's
"Lägg till företag" for returning users.

- New /select-company route merges gnubok memberships with TIC
  CompanyRoles; cards flag already-registered org numbers.
- createCompanyFromTicRole server action derives entity_type, f-skatt,
  VAT, moms_period, and SPAR address defaults, then delegates to
  createCompanyFromOnboarding for consistent provisioning.
- TIC /bankid/complete now requests enrichment on login too, so
  returning users see fresh CompanyRoles in the picker.
- Middleware routes zero-membership users to /select-company when
  enrichment is available, /onboarding otherwise.
- Inline enrichment picker removed from WelcomeOnboarding (wizard is
  now the manual fallback); SPAR address pre-fill preserved.
- Unit tests for mapEntityType helper and createCompanyFromTicRole
  defaults (VAT-AB, non-VAT EF, unmappable, unauth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback on BankID company picker

Greptile P1 + swedish-compliance bot findings:

- Move enrichment row cleanup out of createCompanyFromOnboarding and
  into createCompanyFromTicRole. The manual wizard also goes through
  createCompanyFromOnboarding, and was wiping the enrichment row before
  the returning-user "Lägg till företag" flow could use it.
- Refuse to provision when TIC /lookup is missing. Silently defaulting
  vat_registered to false for a momsregistrerat bolag would create a
  company that issues invoices without moms (ML 17 kap violation). The
  picker now routes to the manual wizard with org_number pre-filled
  when the lookup fails, so the user confirms VAT/F-skatt manually.
- Default accounting_method by entity type: enskild firma → cash
  (K1/kontantmetoden per BFNAR 2013:2), aktiebolag → accrual (K2/K3).
- Document that moms_period='quarterly' is a provisional middle-tier
  default; Skatteverket's assigned period depends on turnover and the
  user can correct it in /settings/tax.
- Fix the misleading "re-fetch from BankID" comment — /select-company
  only reads the cached enrichment row; it's refreshed only on the next
  BankID auth.
- Extend test coverage: lookup-missing refusal, EF kontantmetoden default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: tighten entity-type mapping and clarify K1 threshold

Second round of PR review fixes (swedish-compliance bot):

- mapEntityType now uses explicit allow-lists instead of substring
  matches. "Enskild stiftelse" / "Enskild näringsverksamhet utan firma"
  no longer false-match as enskild_firma (would have provisioned with
  K1/kontantmetoden — ML/BFL risk). Regression guard test added.
- Publikt aktiebolag explicitly included (same K2/K3 regime as private
  AB); Bankaktiebolag / Försäkringsaktiebolag excluded (FFFS regime).
- Remove misleading claim that onboarding UI flags moms_period as
  provisional — no such UI exists by design (approved one-click UX).
- Expand accounting_method comment to cite the 3 MSEK K1→K3 threshold
  (BFNAR 2013:2 vs 2017:3) so the EF→cash default is honest about its
  scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-22 13:21:55 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent adf58a51c0
commit f3a3d07ed3
10 changed files with 1081 additions and 114 deletions
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest'
import { mapEntityType } from '../entity-type-map'
describe('mapEntityType', () => {
it('maps the exact AB codes and labels to aktiebolag', () => {
expect(mapEntityType('AB')).toBe('aktiebolag')
expect(mapEntityType('ab')).toBe('aktiebolag')
expect(mapEntityType('Aktiebolag')).toBe('aktiebolag')
expect(mapEntityType('Publikt aktiebolag')).toBe('aktiebolag') // same K2/K3 regime
expect(mapEntityType(' Aktiebolag ')).toBe('aktiebolag') // whitespace tolerant
})
it('maps the exact EF codes and labels to enskild_firma', () => {
expect(mapEntityType('EF')).toBe('enskild_firma')
expect(mapEntityType('ef')).toBe('enskild_firma')
expect(mapEntityType('Enskild firma')).toBe('enskild_firma')
expect(mapEntityType('Enskild näringsidkare')).toBe('enskild_firma')
})
it('returns null for unsupported entity types', () => {
expect(mapEntityType('HB')).toBeNull()
expect(mapEntityType('Handelsbolag')).toBeNull()
expect(mapEntityType('KB')).toBeNull()
expect(mapEntityType('Kommanditbolag')).toBeNull()
expect(mapEntityType('Stiftelse')).toBeNull()
expect(mapEntityType('Ekonomisk förening')).toBeNull()
expect(mapEntityType('Bostadsrättsförening')).toBeNull()
})
it('does not false-match strings that merely contain "enskild" or "aktiebolag"', () => {
// Regression guard: a loose substring match would misclassify these and
// provision them with K1/kontantmetoden defaults (ML/BFL risk).
expect(mapEntityType('Enskild stiftelse')).toBeNull()
expect(mapEntityType('Enskild näringsverksamhet utan firma')).toBeNull()
// Bank- and försäkringsaktiebolag follow FFFS, not K2/K3 — not a safe
// one-click provision.
expect(mapEntityType('Försäkringsaktiebolag')).toBeNull()
expect(mapEntityType('Bankaktiebolag')).toBeNull()
})
it('returns null for empty or nullish input', () => {
expect(mapEntityType('')).toBeNull()
expect(mapEntityType(null)).toBeNull()
expect(mapEntityType(undefined)).toBeNull()
})
})
+34
View File
@@ -0,0 +1,34 @@
import type { EntityType } from '@/types'
/**
* Explicit allow-lists for TIC/Bolagsverket `legalEntityType` → gnubok
* EntityType. Strict (not substring) matching avoids misclassifications like
* "Enskild stiftelse" → enskild_firma, which would provision with K1/
* kontantmetoden defaults — an ML/BFL correctness risk.
*
* Publikt aktiebolag is included because the bookkeeping regime (K2/K3) and
* VAT treatment are identical to a privat AB. Specialized AB forms
* (Bankaktiebolag, Försäkringsaktiebolag) are deliberately excluded — they
* follow FFFS and need manual setup.
*
* Extend only with values whose bookkeeping regime is known to match.
*/
const AKTIEBOLAG_VALUES = new Set<string>([
'ab',
'aktiebolag',
'publikt aktiebolag',
])
const ENSKILD_FIRMA_VALUES = new Set<string>([
'ef',
'enskild firma',
'enskild näringsidkare',
])
export function mapEntityType(ticType: string | null | undefined): EntityType | null {
if (!ticType) return null
const normalized = ticType.trim().toLowerCase()
if (AKTIEBOLAG_VALUES.has(normalized)) return 'aktiebolag'
if (ENSKILD_FIRMA_VALUES.has(normalized)) return 'enskild_firma'
return null
}