From 7cbd791c5bb7f05193aff3e016f11ae8e60f7902 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:11:39 +0200 Subject: [PATCH] fix: request only enabled TIC enrichment types (unblocks the picker) (#349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) * 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) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- extensions/general/tic/index.ts | 57 ++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/extensions/general/tic/index.ts b/extensions/general/tic/index.ts index e85a8e28..5ff996cd 100644 --- a/extensions/general/tic/index.ts +++ b/extensions/general/tic/index.ts @@ -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 { 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({