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:
co-authored by
Claude Opus 4.7
parent
adf58a51c0
commit
f3a3d07ed3
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('next/cache', () => ({
|
||||
revalidatePath: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/company/context', () => ({
|
||||
setActiveCompany: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { createCompanyFromTicRole } from '../actions'
|
||||
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
|
||||
|
||||
const mockCreateClient = vi.mocked(createClient)
|
||||
|
||||
type CapturedCall = { table: string; method: string; args: unknown[] }
|
||||
|
||||
/**
|
||||
* Builds a chainable Supabase mock that records every method call, allows
|
||||
* per-table result seeding, and returns a capture log the test can assert on.
|
||||
*
|
||||
* - `results[table][method]` (optional) is returned when the chain ends on
|
||||
* that method. Chains otherwise resolve to `{ data: null, error: null }`.
|
||||
* - Unknown methods on the chain no-op and return the chain so callers can
|
||||
* keep chaining freely.
|
||||
*/
|
||||
function buildSupabase(opts: {
|
||||
user: { id: string } | null
|
||||
results?: Record<string, Record<string, { data?: unknown; error?: unknown }>>
|
||||
rpcResults?: Record<string, { data?: unknown; error?: unknown }>
|
||||
}) {
|
||||
const calls: CapturedCall[] = []
|
||||
const { user, results = {}, rpcResults = {} } = opts
|
||||
|
||||
function makeChain(table: string) {
|
||||
const record = (method: string, args: unknown[]) => {
|
||||
calls.push({ table, method, args })
|
||||
}
|
||||
const chain: Record<string, unknown> = {}
|
||||
const methods = ['select', 'eq', 'is', 'in', 'order', 'limit', 'maybeSingle', 'single', 'insert', 'upsert', 'delete', 'update']
|
||||
for (const m of methods) {
|
||||
chain[m] = (...args: unknown[]) => {
|
||||
record(m, args)
|
||||
const canTerminate = results[table]?.[m]
|
||||
if (canTerminate) {
|
||||
return Promise.resolve({
|
||||
data: canTerminate.data ?? null,
|
||||
error: canTerminate.error ?? null,
|
||||
})
|
||||
}
|
||||
return chain
|
||||
}
|
||||
}
|
||||
chain.then = (resolve: (v: unknown) => void) => resolve({ data: null, error: null })
|
||||
return chain
|
||||
}
|
||||
|
||||
const supabase = {
|
||||
auth: {
|
||||
getUser: vi.fn().mockResolvedValue({ data: { user } }),
|
||||
},
|
||||
from: vi.fn().mockImplementation((table: string) => makeChain(table)),
|
||||
rpc: vi.fn().mockImplementation((name: string) => {
|
||||
const result = rpcResults[name]
|
||||
if (result) {
|
||||
return Promise.resolve({ data: result.data ?? null, error: result.error ?? null })
|
||||
}
|
||||
return Promise.resolve({ data: null, error: null })
|
||||
}),
|
||||
}
|
||||
|
||||
return { supabase, calls }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('createCompanyFromTicRole', () => {
|
||||
it('returns Unauthorized when no user session', async () => {
|
||||
const { supabase } = buildSupabase({ user: null })
|
||||
mockCreateClient.mockResolvedValue(supabase as never)
|
||||
|
||||
const result = await createCompanyFromTicRole({
|
||||
teamId: 'team-1',
|
||||
orgNumber: '5566778899',
|
||||
legalName: 'Acme AB',
|
||||
legalEntityType: 'AB',
|
||||
lookup: null,
|
||||
})
|
||||
|
||||
expect(result.error).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it('rejects unmappable entity types before any DB work', async () => {
|
||||
const { supabase, calls } = buildSupabase({ user: { id: 'user-1' } })
|
||||
mockCreateClient.mockResolvedValue(supabase as never)
|
||||
|
||||
const result = await createCompanyFromTicRole({
|
||||
teamId: 'team-1',
|
||||
orgNumber: '969696-1212',
|
||||
legalName: 'Beta HB',
|
||||
legalEntityType: 'Handelsbolag',
|
||||
lookup: null,
|
||||
})
|
||||
|
||||
expect(result.error).toMatch(/manuellt/i)
|
||||
// Entity-type rejection should short-circuit — no table writes.
|
||||
const writes = calls.filter((c) => ['insert', 'upsert', 'delete', 'update'].includes(c.method))
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('refuses to guess when TIC lookup is missing (prevents silent ML 17 kap violation)', async () => {
|
||||
const { supabase, calls } = buildSupabase({ user: { id: 'user-1' } })
|
||||
mockCreateClient.mockResolvedValue(supabase as never)
|
||||
|
||||
const result = await createCompanyFromTicRole({
|
||||
teamId: 'team-1',
|
||||
orgNumber: '5566778899',
|
||||
legalName: 'Acme AB',
|
||||
legalEntityType: 'AB',
|
||||
lookup: null,
|
||||
})
|
||||
|
||||
expect(result.error).toBe('lookup_missing')
|
||||
// Must not have provisioned anything with a guessed VAT status.
|
||||
const writes = calls.filter((c) => ['insert', 'upsert', 'delete', 'update'].includes(c.method))
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('provisions with sensible defaults for a VAT-registered aktiebolag', async () => {
|
||||
const lookup: CompanyLookupResult = {
|
||||
companyName: 'Acme Konsult AB',
|
||||
isCeased: false,
|
||||
address: { street: 'Storgatan 1', postalCode: '11122', city: 'Stockholm' },
|
||||
registration: { fTax: true, vat: true },
|
||||
bankAccounts: [],
|
||||
email: null,
|
||||
phone: null,
|
||||
sniCodes: [],
|
||||
}
|
||||
|
||||
const { supabase, calls } = buildSupabase({
|
||||
user: { id: 'user-1' },
|
||||
results: {
|
||||
// Seed an enrichment row so the cleanup branch runs and the test
|
||||
// can verify it fires.
|
||||
extension_data: {
|
||||
maybeSingle: { data: { id: 'enrichment-1', value: {} } },
|
||||
},
|
||||
},
|
||||
rpcResults: {
|
||||
create_company_with_owner: { data: 'new-company-id' },
|
||||
seed_chart_of_accounts: { data: null },
|
||||
},
|
||||
})
|
||||
mockCreateClient.mockResolvedValue(supabase as never)
|
||||
|
||||
const result = await createCompanyFromTicRole({
|
||||
teamId: 'team-1',
|
||||
orgNumber: '5566778899',
|
||||
legalName: 'Acme Konsult AB',
|
||||
legalEntityType: 'AB',
|
||||
lookup,
|
||||
})
|
||||
|
||||
expect(result.companyId).toBe('new-company-id')
|
||||
expect(result.error).toBeUndefined()
|
||||
|
||||
// The settings upsert on company_settings should reflect our derived defaults.
|
||||
const settingsUpsert = calls.find((c) => c.table === 'company_settings' && c.method === 'upsert')
|
||||
expect(settingsUpsert).toBeDefined()
|
||||
const settings = (settingsUpsert!.args[0] as Record<string, unknown>)
|
||||
expect(settings.entity_type).toBe('aktiebolag')
|
||||
expect(settings.company_name).toBe('Acme Konsult AB')
|
||||
expect(settings.org_number).toBe('5566778899')
|
||||
expect(settings.f_skatt).toBe(true)
|
||||
expect(settings.vat_registered).toBe(true)
|
||||
expect(settings.moms_period).toBe('quarterly')
|
||||
expect(settings.accounting_method).toBe('accrual')
|
||||
expect(settings.address_line1).toBe('Storgatan 1')
|
||||
expect(settings.postal_code).toBe('11122')
|
||||
expect(settings.city).toBe('Stockholm')
|
||||
|
||||
// The enrichment row must be cleaned up by the one-click path so the
|
||||
// picker doesn't re-offer this company on a return visit.
|
||||
const enrichmentDelete = calls.find(
|
||||
(c) => c.table === 'extension_data' && c.method === 'delete',
|
||||
)
|
||||
expect(enrichmentDelete).toBeDefined()
|
||||
})
|
||||
|
||||
it('defaults enskild firma to kontantmetoden (K1), leaves moms_period null when non-VAT', async () => {
|
||||
const lookup: CompanyLookupResult = {
|
||||
companyName: 'Liten EF',
|
||||
isCeased: false,
|
||||
address: null,
|
||||
registration: { fTax: true, vat: false },
|
||||
bankAccounts: [],
|
||||
email: null,
|
||||
phone: null,
|
||||
sniCodes: [],
|
||||
}
|
||||
|
||||
const { supabase, calls } = buildSupabase({
|
||||
user: { id: 'user-1' },
|
||||
rpcResults: {
|
||||
create_company_with_owner: { data: 'new-company-id' },
|
||||
seed_chart_of_accounts: { data: null },
|
||||
},
|
||||
})
|
||||
mockCreateClient.mockResolvedValue(supabase as never)
|
||||
|
||||
await createCompanyFromTicRole({
|
||||
teamId: 'team-1',
|
||||
orgNumber: '8001011234',
|
||||
legalName: 'Liten EF',
|
||||
legalEntityType: 'Enskild firma',
|
||||
lookup,
|
||||
})
|
||||
|
||||
const settingsUpsert = calls.find((c) => c.table === 'company_settings' && c.method === 'upsert')
|
||||
const settings = settingsUpsert!.args[0] as Record<string, unknown>
|
||||
expect(settings.entity_type).toBe('enskild_firma')
|
||||
expect(settings.vat_registered).toBe(false)
|
||||
expect(settings.moms_period).toBeNull()
|
||||
// EF entities default to cash per K1/BFNAR 2013:2; AB must use accrual (K2/K3).
|
||||
expect(settings.accounting_method).toBe('cash')
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,9 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { setActiveCompany } from '@/lib/company/context'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { computeFiscalPeriod } from '@/lib/company/compute-fiscal-period'
|
||||
import { mapEntityType } from '@/lib/company-lookup/entity-type-map'
|
||||
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
|
||||
|
||||
export async function switchCompany(companyId: string): Promise<{ error?: string }> {
|
||||
const supabase = await createClient()
|
||||
@@ -146,3 +149,127 @@ export async function createCompanyFromOnboarding(params: {
|
||||
revalidatePath('/')
|
||||
return { companyId: newCompanyId }
|
||||
}
|
||||
|
||||
/**
|
||||
* One-click company setup from a TIC/Bolagsverket company role.
|
||||
*
|
||||
* The picker page at /select-company passes a `CompanyLookupResult` already
|
||||
* fetched from `/api/extensions/ext/tic/lookup`, plus the `EnrichmentCompanyRole`
|
||||
* minimums (org number, legal name, legal entity type). This action derives
|
||||
* sensible defaults (accrual, quarterly moms for VAT-registered, Jan-Dec
|
||||
* fiscal year), reads SPAR address from `extension_data` as a fallback, and
|
||||
* then delegates to `createCompanyFromOnboarding` so the provisioning path is
|
||||
* identical to the manual wizard. On success it clears the enrichment row
|
||||
* consumed by this path — the manual wizard leaves it intact so a returning
|
||||
* BankID user can still reach `/select-company` and pick another directorship.
|
||||
*
|
||||
* Requires `lookup` to be non-null: if TIC `/lookup` is unreachable, the client
|
||||
* must route to the manual wizard instead. Silently defaulting `vat_registered`
|
||||
* to false for a momsregistrerat bolag would violate ML 17 kap (invoices
|
||||
* without moms), so we refuse to guess.
|
||||
*/
|
||||
export async function createCompanyFromTicRole(params: {
|
||||
teamId: string
|
||||
orgNumber: string
|
||||
legalName: string
|
||||
legalEntityType: string
|
||||
lookup: CompanyLookupResult | null
|
||||
}): Promise<{ companyId?: string; error?: string }> {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
const entityType = mapEntityType(params.legalEntityType)
|
||||
if (!entityType) {
|
||||
return { error: 'Den här företagsformen måste sättas upp manuellt.' }
|
||||
}
|
||||
|
||||
// If the TIC lookup failed we don't know the company's VAT/F-skatt status.
|
||||
// Refuse to silently guess — the caller routes to the manual wizard so the
|
||||
// user can confirm these fields themselves.
|
||||
if (!params.lookup) {
|
||||
return { error: 'lookup_missing' }
|
||||
}
|
||||
|
||||
// SPAR address fallback if the TIC lookup didn't include one.
|
||||
const { data: enrichmentRow } = await supabase
|
||||
.from('extension_data')
|
||||
.select('id, value')
|
||||
.eq('user_id', user.id)
|
||||
.eq('extension_id', 'tic')
|
||||
.eq('key', 'bankid_enrichment')
|
||||
.maybeSingle()
|
||||
|
||||
const spar = (enrichmentRow?.value as { spar?: Record<string, string | undefined> } | null)?.spar
|
||||
const sparStreet = spar?.Folkbokforingsadress_SvenskAdress_Utdelningsadress1
|
||||
const sparPostal = spar?.Folkbokforingsadress_SvenskAdress_PostNr
|
||||
const sparCity = spar?.Folkbokforingsadress_SvenskAdress_Postort
|
||||
|
||||
const addressStreet = params.lookup.address?.street ?? sparStreet ?? null
|
||||
const addressPostal = params.lookup.address?.postalCode ?? sparPostal ?? null
|
||||
const addressCity = params.lookup.address?.city ?? sparCity ?? null
|
||||
|
||||
const fTax = params.lookup.registration.fTax
|
||||
const vatRegistered = params.lookup.registration.vat
|
||||
|
||||
// moms_period: Skatteverket assigns the actual reporting period from
|
||||
// annual beskattningsunderlag (≤1 MSEK → yearly, ≤40 MSEK → quarterly,
|
||||
// >40 MSEK → monthly). TIC /lookup doesn't expose turnover, so we pick the
|
||||
// middle-tier default. The user must verify it matches their Skatteverket
|
||||
// assignment in /settings/tax — a mismatch causes late-filing penalties
|
||||
// under SFL.
|
||||
const momsPeriod = vatRegistered ? 'quarterly' : null
|
||||
|
||||
// EF ≤3 MSEK may use kontantmetoden under K1/BFNAR 2013:2; above that
|
||||
// threshold, BFNAR 2017:3 requires bokföringsmässiga grunder. We default
|
||||
// to cash because the vast majority of EF users are small; users above
|
||||
// the threshold can switch in /settings/bookkeeping. Aktiebolag must use
|
||||
// accrual under K2/K3.
|
||||
const accountingMethod = entityType === 'enskild_firma' ? 'cash' : 'accrual'
|
||||
|
||||
const settings: Record<string, unknown> = {
|
||||
entity_type: entityType,
|
||||
company_name: params.legalName,
|
||||
org_number: params.orgNumber.replace(/[\s-]/g, ''),
|
||||
f_skatt: fTax,
|
||||
vat_registered: vatRegistered,
|
||||
moms_period: momsPeriod,
|
||||
accounting_method: accountingMethod,
|
||||
fiscal_year_start_month: 1,
|
||||
address_line1: addressStreet,
|
||||
postal_code: addressPostal,
|
||||
city: addressCity,
|
||||
}
|
||||
|
||||
const periodResult = computeFiscalPeriod(settings)
|
||||
if (periodResult.error) {
|
||||
return { error: 'Kunde inte beräkna räkenskapsår.' }
|
||||
}
|
||||
|
||||
const result = await createCompanyFromOnboarding({
|
||||
teamId: params.teamId,
|
||||
settings,
|
||||
fiscalPeriod: {
|
||||
startDate: periodResult.startStr,
|
||||
endDate: periodResult.endStr,
|
||||
name: periodResult.periodName,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.error || !result.companyId) {
|
||||
return { error: result.error ?? 'Kunde inte skapa företag. Försök igen.' }
|
||||
}
|
||||
|
||||
// One-time use: drop the enrichment row now that the user has committed to
|
||||
// a TIC-suggested company. The manual wizard intentionally does NOT do this
|
||||
// so a user with multiple directorships can still reach /select-company
|
||||
// afterwards and provision another one.
|
||||
if (enrichmentRow?.id) {
|
||||
await supabase.from('extension_data').delete().eq('id', enrichmentRow.id)
|
||||
}
|
||||
|
||||
return { companyId: result.companyId }
|
||||
}
|
||||
|
||||
@@ -138,16 +138,29 @@ export async function updateSession(request: NextRequest) {
|
||||
// their account without being trapped on /onboarding forever.
|
||||
const isNoCompanyAllowed =
|
||||
pathname.startsWith('/onboarding') ||
|
||||
pathname.startsWith('/select-company') ||
|
||||
pathname.startsWith('/settings/account') ||
|
||||
pathname.startsWith('/api/account/') ||
|
||||
pathname.startsWith('/api/company')
|
||||
|
||||
// No companies — redirect to onboarding, but allow the escape-hatch routes
|
||||
// No companies — redirect to the picker if we have BankID enrichment for
|
||||
// this user, otherwise the manual wizard. Either way, allow the escape-hatch
|
||||
// routes to pass through.
|
||||
if (!companyId) {
|
||||
if (isNoCompanyAllowed) {
|
||||
return supabaseResponse
|
||||
}
|
||||
return NextResponse.redirect(new URL('/onboarding', request.url))
|
||||
|
||||
const { data: enrichmentRow } = await supabase
|
||||
.from('extension_data')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('extension_id', 'tic')
|
||||
.eq('key', 'bankid_enrichment')
|
||||
.maybeSingle()
|
||||
|
||||
const destination = enrichmentRow ? '/select-company' : '/onboarding'
|
||||
return NextResponse.redirect(new URL(destination, request.url))
|
||||
}
|
||||
|
||||
// Set company cookie on the response so downstream requests have it
|
||||
|
||||
Reference in New Issue
Block a user