Commit Graph
5 Commits
Author SHA1 Message Date
MattssonandClaude Fable 5.1 b56da5d6c5 feat(api): expose bank-connection freshness in MCP and v1 REST (#2124)
* feat(api): expose bank-connection freshness in MCP and v1 REST

gnubok_connect_bank now returns last_synced_at, consent_expires and
error_message per connection, and its instructions tell the agent to
flag stale or expiring connections. New read-only endpoint
GET /api/v1/companies/{companyId}/bank-connections exposes the same
fields to API-key integrations (scope companies:read).

Background: a user's PSD2 feed died silently in July; bookkeeping
looked complete while three weeks stale, and nothing on the API/MCP
surface could reveal it. Sync stays cron-driven; an agent-triggerable
sync was considered and deferred (see DECISIONS.md).

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

* fix(api): address skeptic findings on bank-connection freshness

- Map the bank-connections group into skills/accounted-api (apiskill:check
  crashed on the unmapped group; regenerated skill files included).
- Gate the v1 route on the bank_sync capability, mirroring the MCP twin:
  a lapsed entitlement now answers with a capability error instead of
  status=active with a frozen last_synced_at.
- Reword MCP instructions + v1 pitfalls: null last_synced_at right after
  connecting is normal, staleness threshold aligned to the UI's 36 hours,
  and re-authorisation is only advised for expired/error/consent-out, not
  for stale-but-active connections (lapsed subscription or deselected
  accounts are the usual causes there).

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

* fix(mcp): keep gnubok_connect_bank schema under the tools/list token ceiling

The enriched outputSchema plus the worked examples that landed on main
(#2100) pushed the projected tools/list payload 20 tokens over the
61.6K context-budget ceiling. Drop the per-property descriptions from
the new freshness fields; the instructions string (runtime output, not
catalog payload) already explains them.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 20:54:55 +02:00
d8244ecaff feat(mcp): connect_migration: one-click card into the previous-system wizard (#1960)
* feat(mcp): gnubok_connect_migration: one-click connect card into the previous-system wizard

'Jag hade Fortnox' now gets the same feel as Skatteverket: the tool
returns the migration-wizard link for the named provider and renders
the connect-card widget (new migration branch: 'Hämta från Fortnox',
button opens the wizard that logs into the old system and fetches all
fiscal years plus invoices, customers, suppliers and documents). For
visma/bokio (no API export) the instructions order the SIE drop card
first and this card as the complement. Scope companies:read; skill
step 3 points at the tool instead of raw wizard links; ceiling 62.4K
to 63K documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): 5-minute freshness hint on tools/list, widgets and prompts: the catalog changes with every deploy

The stateless-client CacheableResult hint on tools/list, resources/read
(widget HTML) and prompts/list was 1 hour. Claude.ai honors it, so for
up to an hour after a deploy the connector served a pre-deploy catalog:
a freshly shipped tool flapped in and out of the tool list depending on
which fetch hit the client cache, and two E2E runs dead-ended on
'tool does not exist' for a tool that was live server-side. These
payloads are static only within one deploy; 5 minutes bounds the stale
window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 19:19:24 +02:00
6f0f4d1eea fix(mcp): flow-order signals in tool results: ask the bank, import history first (#1955)
E2E #5: the agent skipped the bank question (generic picker link) and
went straight to connect cards for a company whose fiscal year started
13 months ago (history import should come first). The skill says both,
but the agent follows tool results more reliably than skill prose, so
the rules now live in the results themselves:

- gnubok_lookup_company still_to_ask gains two flow questions: which
  bank (for the bank= deep link) and whether the bookkeeping lived in a
  previous system (SIE import BEFORE bank).
- gnubok_create_company's confirmed result computes days of history in
  the fiscal period: past 90 days it emits history_note (PSD2 cannot
  reach it; run gnubok_sie_preflight first) and reorders the remaining-
  setup message to (1) SIE import, (2) bank, (3) Skatteverket.
- gnubok_connect_bank called without bank= now opens its instructions
  with a nudge to ask for the bank and re-call for the direct consent
  link.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:35:12 +02:00
ec9cab24cc feat(mcp): ask the bank first: connect link deep-starts the named bank's consent (#1951)
* feat(mcp): ask the bank first: connect link deep-starts the named bank's consent

The connect card used to open the generic picker page; the user then chose
the bank there. The agent now asks 'vilken bank har företaget?' among the
opening questions and passes it to gnubok_connect_bank, whose connect_url
becomes /import?mode=psd2&bank=<name>. BankSelector resolves the name
(exact, then unique prefix, then unique substring: ambiguous names fall
back to the prefilled picker rather than guessing an institution) and
auto-starts that bank's consent through the same onConnect handler, so
the duplicate-pending and renew-instead guards stay fully interactive.
The param is stripped via history.replaceState after the one-shot so an
aborted bank flow plus back-navigation does not silently relaunch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): first-year suggestion keeps the AB vs enskild firma distinction

Compliance-review finding on #1949: the suggestion text collapsed both
forms onto a 31 December end. Only an enskild firma's first year MUST end
31 December; an AB may pick any end within BFL 3 kap 3 §'s 18-month cap,
with 31 December as the common default. The lookup tool's still_to_ask
line and the skill now say so explicitly, and first-year-defaults
documents that fiscalYear-null is a strong-not-perfect filed-report
signal that must only ever feed confirm-question suggestions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:31:45 +02:00
31e0cd6e05 feat(onboarding): company setup from the conversation and POST /api/v1/companies (#1814 PR 3) (#1864)
* feat(onboarding): company setup from the conversation and POST /api/v1/companies

Third PR of agent-first onboarding (#1814). Once connected, the agent can
now set up a company end to end without the web wizard, and partner
platforms can provision companies over REST.

- create_company_for_user: service-role-only SECURITY DEFINER twin of
  create_company_with_owner taking the owner explicitly (service clients
  have no auth.uid()). pg-real test covers creation, role gating, unknown
  owner and foreign team.
- lib/company/create-company.ts: the wizard's creation sequence (org
  number, TIC snapshot, BAS chart, settings, first fiscal period, tax
  deadlines, rollback) extracted into createCompanyCore; the Server
  Action delegates to it, behaviour unchanged.
- lib/company/onboarding-input.ts: one Zod schema + planner for the
  agent/API paths; a VAT-registered company without moms_period is
  refused (a missing period silently yields zero VAT deadlines).
- MCP: gnubok_create_company (two-phase: preview, then confirm=true;
  companies:write, company-independent), gnubok_connect_bank and
  gnubok_connect_skatteverket (status + the browser link, gated on
  bank_sync / skatteverket, search-only in the catalog), the
  "onboarding" skill, and initialize instructions pointing at it.
- Consent page pre-ticks companies:write for an account with no company
  yet, so the setup does not dead-end on insufficient scope after signup.
- POST /api/v1/companies (companies:write, dry-run aware) on the same
  core; scope map, registry, spec snapshot and the generated API skill
  updated.
- tools/list payload ceiling raised 59.95K -> 60.4K for the one new
  default-catalog tool (documented in the guard).

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

* fix(onboarding): explicit f_skatt, org number when VAT-registered, EF first year ends 31 Dec

Review findings on #1864 (Swedish compliance review):
- f_skatt is required, never defaulted to approved (SE-R-005 risk).
- org_number is required when vat_registered: the invoice
  momsregistreringsnummer derives from it (ML 17 kap 24 §).
- An enskild firma's first fiscal year must end on 31 December and its
  start month is forced to 1 even with first_fiscal_year set, mirroring
  the wizard's own rule text (BFL 3 kap. 1 §).
- POST /api/v1/companies no longer claims Idempotency-Key support (the
  wrapper only honours it on company-scoped routes).
- pg-real: createCompanyCore's chart seed runs under the real
  service_role, which the unit tests could not prove.

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

* test(pg): starter chart has 41 accounts, assert non-empty

The service_role chart-seed proof passed the part that mattered (no
42501 from seed_chart_of_accounts) and failed on a wrong row-count
guess: the seeded chart is a curated starter set, not the full BAS list.

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

* fix(migrations): move create_company_for_user to 20260825120000

main gained 20260824170000_bulk_book_transactions_service_actor.sql with
the same version while this branch was open; two files on one version
abort every Supabase branch apply and the prod auto-apply.

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

* chore(api): refresh spec snapshot and generated skill after rebasing onto main

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

* fix(mcp): flat create_company result, refuse localhost connect links, test hygiene

CodeRabbit on #1864: the confirmed-create result was wrapped in the
{ data, next } envelope while its outputSchema promised top-level
fields; it now returns the fields with next as a sibling. The two
connect-link tools refuse to build a link when NEXT_PUBLIC_APP_URL is
unset instead of handing a remote user a localhost URL. Tests clear
mocks and the event bus in beforeEach. Not changed: the rollback
already survives user_preferences.active_company_id (that FK is ON
DELETE SET NULL since 20260331010000), and v1 error details stay in the
surface's English developer convention.

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 12:41:02 +02:00