fix: request only enabled TIC enrichment types (unblocks the picker) (#349)

* chore: translate TIC enrichment errors into actionable hints

Adds self-diagnosing log output for known TIC failure shapes:
- 'Session not completed' → tenant doesn't have enrichment enabled
  (the BankID consent-to-enrich dialog only fires when SPAR/CompanyRoles
  are configured on the tenant; without it, TIC rejects enrichment
  requests with this misleadingly-worded error)
- 'not enabled' variants → explicit tenant disable
- 'too old' → >30min between auth and enrichment request

No behaviour change — purely diagnostic. Saves a future developer from
re-discovering that "Session not completed" means "contact support@tic.io"
rather than "retry the session."

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

* fix: stop requesting disabled 'SPAR' enrichment type

Root cause of the 'Session not completed' failures: TIC renamed 'SPAR'
to 'Address' and our tenant has Address *disabled* (only CompanyRoles is
on — verified via GET /api/v1/enrichment/types). Requesting an unknown
or disabled type causes TIC to reject the whole enrichment with a
misleading 'Session not completed' error rather than ignoring the unknown
type or returning PartiallyCompleted.

- Enrichment request now asks only for ['CompanyRoles'], which is what
  the /select-company picker actually needs. This is enough to stop the
  'Session not completed' errors and make the picker work in prod.
- If Address is enabled on TIC later, add it to the array (and wire up
  the address pre-fill paths in WelcomeOnboarding + createCompanyFromTicRole
  — both already look for a `.spar` field that TIC may have renamed).
- Updated the diagnostic hint for 'Session not completed' to lead with
  the type-mismatch cause and point at the /enrichment/types curl for
  verification, since that's the first thing to check.

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

* chore: clean up stale SPAR references in tic extension (PR review)

Greptile P2 nits — all cosmetic, no runtime impact:

- fetchAndStoreEnrichment JSDoc updated from "SPAR + CompanyRoles" to
  just CompanyRoles, with a note about where to add Address back if/when
  it's enabled on the tenant.
- hasSpar log field removed from the 'enrichment data shape' snapshot.
  enrichmentData.spar was permanently undefined after the SPAR → nothing
  swap; logging a constantly-false field would have misled a future
  developer chasing missing address data.
- Inline comment on the signup-path call to fetchAndStoreEnrichment now
  matches the login-path comment (CompanyRoles, not SPAR + CompanyRoles).

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 16:11:39 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 1583e302da
commit 7cbd791c5b
+46 -11
View File
@@ -30,10 +30,14 @@ import crypto from 'crypto'
const log = createLogger('tic/bankid')
/**
* Request SPAR + CompanyRoles enrichment for a completed BankID session and
* cache the result in `extension_data` so /select-company and the onboarding
* wizard can pre-fill from it. Non-blocking: any failure is logged and
* swallowed — BankID auth must still succeed even if enrichment is down.
* Request CompanyRoles enrichment for a completed BankID session and cache
* the result in `extension_data` so /select-company can pre-fill the picker.
* Non-blocking: any failure is logged and swallowed — BankID auth must still
* succeed even if enrichment is down.
*
* Only types currently enabled on the TIC tenant are requested — see the
* block comment inside the function. If Address (formerly SPAR) is enabled
* later, add it here to restore address pre-fill in the manual wizard.
*/
async function fetchAndStoreEnrichment(
sessionId: string,
@@ -41,7 +45,19 @@ async function fetchAndStoreEnrichment(
supabase: SupabaseClient,
): Promise<void> {
try {
const enrichment = await requestEnrichment(sessionId, ['SPAR', 'CompanyRoles'])
// IMPORTANT: only request types that are actually enabled on the TIC
// tenant. Requesting an unknown/disabled type (e.g. 'SPAR', which TIC
// has renamed to 'Address' and which our tenant currently has off)
// makes TIC reject the whole enrichment with
// `error: 'Session not completed'` — a misleading error that took a
// round of debugging to trace. Verified via GET /api/v1/enrichment/types:
// { type: 'CompanyRoles', enabled: true } ← we want this
// { type: 'Address', enabled: false } ← formerly SPAR, off
//
// If 'Address' gets enabled later, add it here (and wire up the
// address pre-fill in WelcomeOnboarding and createCompanyFromTicRole
// — both already look for a `.spar` field that TIC may have renamed).
const enrichment = await requestEnrichment(sessionId, ['CompanyRoles'])
log.info('enrichment request returned', {
status: enrichment.status,
requestedTypes: enrichment.requestedTypes,
@@ -51,8 +67,7 @@ async function fetchAndStoreEnrichment(
// Case-insensitive status comparison: TIC has been observed returning
// lowercase values ('completed', 'failed') in addition to the docs' canonical
// capitalized form. Accept both fully and partially completed runs — if the
// tenant only has SPAR enabled (not CompanyRoles) we still want the address.
// capitalized form. Accept both fully and partially completed runs.
const statusLower = String(enrichment.status ?? '').toLowerCase()
const isCompleted = statusLower === 'completed' || statusLower === 'partiallycompleted'
const usable = isCompleted && enrichment.secureUrl
@@ -61,17 +76,37 @@ async function fetchAndStoreEnrichment(
// so we can diagnose why a real-user enrichment comes back non-usable.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { secureUrl: _omit, ...responseDiagnostic } = enrichment
log.warn('enrichment not usable — inspect response for diagnostic fields', responseDiagnostic)
// Interpret common failure shapes into actionable hints so developers
// don't have to re-trace this every time. TIC returns these as body
// fields with HTTP 200, not as errors — see TIC_AUTH.md §enrichment.
const errField = (enrichment as { error?: string }).error ?? ''
let hint: string | undefined
if (errField === 'Session not completed') {
// Two distinct causes produce this identical error:
// 1. We requested a type not enabled on the tenant (most common —
// verify via GET /api/v1/enrichment/types)
// 2. The BankID session genuinely never went through the
// consent-to-enrich dialog
hint = 'Likely cause: a requested enrichment type is not enabled on the TIC tenant. Run `curl -H "X-Api-Key: $KEY" https://id.tic.io/api/v1/enrichment/types` to verify which types have `enabled: true` and adjust the requestEnrichment call to match.'
} else if (errField.toLowerCase().includes('not enabled')) {
hint = 'Enrichment explicitly disabled on TIC tenant — contact support@tic.io.'
} else if (errField.toLowerCase().includes('too old')) {
hint = '>30 min between auth completion and enrichment call — check for slow server-side work between /bankid/complete and fetchAndStoreEnrichment.'
}
log.warn('enrichment not usable', { ...responseDiagnostic, hint })
return
}
const enrichmentData = await fetchEnrichmentData(enrichment.secureUrl)
// Log a PII-free snapshot so we can debug the role filter in production.
// Raw personnummer/names are deliberately omitted.
// Raw personnummer/names are deliberately omitted. `spar`/`address` not
// logged — we don't request those types currently (see block comment
// on requestEnrichment above), so they'd always be absent.
const firstRole = enrichmentData.companyRoles?.[0]
log.info('enrichment data shape', {
hasSpar: !!enrichmentData.spar,
companyCount: enrichmentData.companyRoles?.length ?? 0,
firstRoleStatuses: firstRole
? {
@@ -733,7 +768,7 @@ export const ticExtension: Extension = {
)
}
// Enrichment (SPAR + CompanyRoles) — pre-fills /select-company picker.
// Enrichment (CompanyRoles) — pre-fills /select-company picker.
await fetchAndStoreEnrichment(sessionId, userId, supabase)
return NextResponse.json({