fix(providers): name the real Björn Lundén connect failure (integration not activated, not bad credentials) (#2322)

* fix(providers): name the real Björn Lundén connect failure: integration not activated, not bad credentials

Every Björn Lundén connect in prod has failed with "Leverantören avvisade
autentiseringen" (10 consents since June; only BL's own sandbox company ever
received tokens). Live-verified against a real customer User-Key today: BL
answers 403 "<service>:READ is out of allowed scope for service provider
Arcim" on every read endpoint. The key is right and binds the company; the
company has simply never activated our integration, and it cannot until BL
moves the listing out of sandbox. The generic 403 mapping told the user to
re-check what they pasted, which can never help.

- BjornLundenClient: isBjornLundenScopeError / isBjornLundenUnknownKeyError,
  matching the verbatim live 403 and 500 bodies.
- submitProviderToken: 403-with-scope-body -> ProviderTokenInvalidError kind
  'integration-not-activated'; 500/404 -> 'company-key-not-found'; 401 (our
  own client_credentials token refused) rethrows as a generic submit failure
  instead of blaming the pasted key.
- New 422 structured errors BL_INTEGRATION_NOT_ACTIVATED and
  BL_COMPANY_KEY_NOT_FOUND with Swedish/English copy that names the fix
  (activate under Integrationer in Lundify, else SIE) and where the GUID is.
- Wizard copy for BL moved to i18n keys and reordered: activate first, then
  paste the key; the key only works once the integration is activated.
- Tests: route mapping for both kinds, probe classification incl. the
  captured live bodies, registry entries pinned to 422.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY

* fix(providers): drop the unknown-key body matcher, the live BL 500 body is not stable

Verifying through BjornLundenClient against apigateway.blinfo.se, a made-up
User-Key answered 500 with a Spring BeanCreationException for
databaseConnector, not the null getCurrentUser() message captured earlier.
The unknown-key verdict already keys on the status alone in
submitProviderToken; keep only the 403 scope matcher, whose body IS stable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-05 17:20:08 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 41a5728ca7
commit 473b1fd2eb
12 changed files with 264 additions and 12 deletions
+1
View File
@@ -1604,3 +1604,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-05] Supplier and customer pages: the register fills the row's own contact fields (e-mail, phone, postal address, VAT number) when they are empty or still carry what the register said last time, marked "från SCB" by equality with the registry fact, never a value a person typed. Chosen over a read-only fallback because the row is what payment files and documents use; provenance by equality instead of a source column because it needs no schema and a person's edit ends it by itself. Företagsuppgifter keeps only what the register alone knows (status line, industry, seat, size). Agents get the party read-only first: ?expand=party on v1 supplier/customer detail, party_id on list rows, and gnubok_get_party in MCP; the parties resource (suggest, promote, enrich) comes as its own v1 surface next.
[2026-09-05] Invoice PDF statutory notices follow the document language: the export notice (stored in Swedish on invoices.reverse_charge_text at create time) is matched against the shared EXPORT_NOTICE_SV constant and rendered from LABELS, so existing invoices are fixed too, and the English footer reads "Approved for F-tax (Godkänd för F-skatt)". Rejected translating at create time (would leave every existing export invoice Swedish) and deriving purely from vat_treatment (would override custom or v1-supplied text). The old Swedish-only F-skatt footer cited Peppol SE-R-005, which governs the UBL file, not the PDF; the swedish-invoice-compliance skill (invoice-rules.md §4) states ML has no language requirement for invoice text, and the literal Swedish phrase stays on the English PDF in parentheses. All notice boxes (proforma, quote, VAT notice, notes) now share one neutral style; custom invoice layouts are a separate feature request.
[2026-09-05] Utlägg becomes an answer, not a page: the Underlag pane asks "Vem betalade?" (Företaget / Jag, privat / En anställd / Ingen ännu) and books a privately paid receipt in place through POST /api/expense-claims; the person owed surfaces as a Betala row in Att göra (lib/worklist expense_payout, one item per person) and the Utlägg nav row is gated on existing claims like Körjournal. Chosen over a fourth item in the Bokföring split button (that menu is three ways to type one verifikat, not a list of document kinds) and over keeping the two-step wizard as the entry point: a kvitto paid with a private card differs from any other purchase only in the credit account, and 93 percent of companies on prod are owner-only, for whom a module for that one bit is the wrong shape. Phase 2 (bank-driven repayment, open items shared with leverantörsfakturor, via lön) and phase 3 (retire the wizard, per-person list under Löner) are filed as follow-ups.
[2026-09-05] Björn Lundén connect: a 403 whose body says "out of allowed scope for service provider" is mapped to its own BL_INTEGRATION_NOT_ACTIVATED verdict (the key is right, the company never activated the integration) instead of the generic "leverantören avvisade autentiseringen"; live-verified against a real customer key, where every read endpoint answered exactly that while a made-up key answered 500. Root cause of every failed BL connect in prod (10 consents, only BL's own sandbox company ever got tokens): the integration is still a sandbox listing at BL, so no real company can activate it. Chose a message that names the fix (activate in Lundify, else SIE) over hiding the provider state; the Lundify activation-redirect flow and document/line-level fetching are filed as follow-ups rather than built blind before BL releases the integration.