Commit Graph

11 Commits

Author SHA1 Message Date
Jakob Wennberg f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

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

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00
Jakob Wennberg 18cbc4c30a fix(security): audit remediation 2026-09-01: api_keys identity, viewer gates, OAuth binding, XSS, MFA gate (#2155)
* fix(security): bind api_keys to the caller, lock hash-as-bearer RPCs and provider token tables

Security audit 2026-09-01, critical items.

- api_keys INSERT requires user_id = auth.uid() again (an admin could
  forge a key for any co-member and act as them in every company they
  belong to); SELECT is own-keys-or-admin; a BEFORE trigger freezes the
  identity and credential columns against user-session UPDATEs.
- rotate_mcp_refresh_token and validate_and_increment_api_key become
  service_role only: they match rows by a presented SHA-256, so a hash
  readable by co-members was a bearer credential.
- validate_and_increment_api_key fails closed when the key's user is no
  longer a member of the key's company.
- provider_consent_tokens and provider_otc: the DELETE policies collapsed
  to "caller has any team row" (correlated subquery on a non-existent
  team_members.company_id). All member policies dropped; service_role
  only, matching every existing code path.

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

* fix(security): role gates, ownership guards and posting integrity in the database

Security audit 2026-09-01, high items at the database layer.

- One table-level guard, enforce_company_writer_role(), blocks the
  read-only viewer role on 55 company-scoped tables including through
  the 15 membership-only SECURITY DEFINER writers. Keyed on the JWT role
  claim so it fires inside definer bodies; no-op for service_role and
  trigger cascades.
- company_members user_id/company_id immutable from user sessions;
  invitations can never grant owner; team_members gains a transition
  guard (admins keep non-owner role moves); companies team_id and
  archiving are owner-only and team attachment needs team membership.
- Direct statements (current_user = authenticated) can no longer insert
  posted headers, add lines under posted verifikat, or post a draft with
  a voucher number the sequence never issued. Sanctioned RPCs run as the
  definer and are untouched; the engine's own draft-then-post shapes
  still pass.
- create_document_version refuses viewers and foreign storage paths;
  validate_version_chain needs membership and loses anon EXECUTE;
  match_documents / match_booking_templates lose anon; cron maintenance
  RPCs become service_role only; the production-only
  seed_asset_categories is dropped.

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

* build: pin tsx as an exact devDependency instead of fetching it with npx at build time

prebuild ran "npx tsx" with no lockfile entry, so every Vercel, Docker
and CI build downloaded tsx@latest and its transitive tree from the
registry with no integrity check, inside the build environment.

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

* fix(security): refuse the viewer role on API-key and MCP write paths

The v1 wrapper and the MCP company routing checked company membership
but never role, and both run as service role, so a read-only viewer
holding an API key could post vouchers and change settings through the
API. Mutating methods and non-read scopes now return 403 ROLE_READ_ONLY
for viewers on v1; MCP write tools refuse viewers the same way.

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

* fix(security): stop serving uploaded SVG, XML and HTML as executable content on the app origin

Uploads persisted the browser-declared mime type and the inline proxy
served it verbatim, sandboxing only text/html; the storage proxy
forwarded the uploader's Content-Type. Any writer, or any Peppol sender,
could plant a scripted SVG or XHTML that executed on app.gnubok.se.

- inline route: allow-list of natively safe types (PDF, raster images)
  served as before; everything else gets the opaque sandbox CSP.
- storage proxy: octet-stream + attachment + sandbox unless the DB
  mime for the key is on the allow-list.
- document-service: the stored mime is the magic-byte validated type.
- logo upload: magic-byte validation, SVG refused.

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

* fix(security): byrå brand logo upload decides the type by magic bytes and drops SVG

Same pattern as the company logo route: the logos bucket is public, so a
scripted SVG (or anything declared as an image) must never land there.
The upload pickers stop advertising SVG.

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

* fix(security): bind Enable Banking, Stripe and WooCommerce callbacks to the initiating user

The callbacks resolved the pending row by oauth_state alone, so a
victim who completed an attacker-initiated consent had their bank
account, merchant account or store attached to the attacker's company.
requireFlowInitiator() now requires the cookie session of the user who
started the flow: no session redirects to login with the callback URL
preserved, a different user is refused and nothing is exchanged.

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

* fix(security): guard tenant-controlled outbound fetches and surface the disabled rate limiter

WooCommerce and Shopify syncs fetched a member-editable store URL with
plain fetch() and redirect following under the service role, and the
invoice PDF renderer fetched company_settings.logo_url unguarded. All
three go through a new safeFetch() (public-IP validation via url-guard,
https only, redirect: 'manual', body size cap) and re-normalise the
stored host at use time. checkRateLimit() keeps failing open on hosted
but logs one error per process when Upstash is not configured and
exports isRateLimiterConfigured().

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

* fix(security): decide the API MFA gate from server-authenticated factors, not the session cookie

getAuthenticatorAssuranceLevel() without arguments derives nextLevel
from session.user.factors, which comes from the unsigned sb-*-auth-token
cookie. Deleting factors from the cookie made an enrolled account look
like it had nothing to step up to, on every /api route and in
requireAuth. Both gates now read factors from the getUser() result or
listFactors() and the level from the verified JWT claim, and fail closed
on errors. Page-branch gate hardened the same way.

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

* fix(security): bind Fortnox/Visma, Gmail and Skatteverket callbacks to the initiating user

The arcim-migration callback exchanged the provider code onto whatever
consent the one-time state named, with no check of who completed the
flow and no org-number comparison, so a phished Fortnox admin handed
their ledger to the attacker's company. provider_otc now records the
initiating user (migration 20260902100000); the callback requires that
session and, after the exchange, refuses a provider company whose org
number differs from the consent's company. The Gmail and Skatteverket
callbacks enforce the same initiator check.

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

* fix(security): BankID signup confirms the email before linking the identity

Signup created an email-confirmed, MFA-exempt account for any address
the caller typed and returned a magic link, so an attacker could
pre-register a victim's email and keep a permanent BankID login into the
account the victim later adopted. The user is now created unconfirmed,
the identity carries email_verified_at NULL (migration 20260902101000),
bankid_linked is not set until the mailed confirmation is clicked, and
BankID login of a pending identity is refused with the confirmation
re-sent.

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

* fix(security): bind MCP OAuth redirect URIs to the consenting user and cap scopes

A user-registered redirect URI was allowlisted globally, the consent page
named no client, and all scopes were pre-checked, so one phishing link
handed an attacker a full-scope key for the victim's company. Registered
URIs now resolve only for the registrant or a colleague sharing a
company; the consent page shows the client identity and redirect host;
non-built-in clients default to read-only pre-checks; scopes are capped
by the user's role (viewer: read only) at consent and at /token.

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

* fix(auth): client follow-ups for BankID confirmation, callback mismatch copy and decision log

- register client handles the new confirmation_sent response from BankID
  signup with the existing inbox screen instead of calling verifyOtp.
- BankID login surfaces the email_unconfirmed explanation.
- WooCommerce settings map woocommerce_error=wrong_user to its own copy.
- Logo help text no longer advertises SVG.
- DECISIONS.md records the audit remediation choices.

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

* fix(mcp-oauth): literal SoD columns in the api_keys insert so the phantom-column scanner resolves them

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

* test(logo): type the upload fixtures as Uint8Array<ArrayBuffer> so they are valid BlobParts

Fixes the typecheck ratchet on PR #2155 and ratchets the baseline down
by the one legacy error the change removed.

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:38:30 +02:00
Jakob Wennberg 8ddc77fdfd feat(mcp): org-number-first onboarding: gnubok_lookup_company prefills the company from the registry (#1940)
The onboarding flow now mirrors the web wizard: ask for the
organisationsnummer first, look the company up in the public registry (one
TIC Lens call through the extracted extensions/general/tic/lib/lookup.ts,
shared with the /lookup HTTP route), and present the facts for confirmation
instead of interrogating the user.

The new gnubok_lookup_company tool (companies:read, company-independent,
default catalog) returns the registry facts, a prefilled
suggested_create_company_input, and a still_to_ask list that encodes the
same fact-vs-question rules as lib/onboarding-journey/reducer.ts: F-skatt
is a fact both ways, VAT is a fact only when positively registered (ML 17
kap 24 paragraf), moms period and accounting method are always asked, an
enskild firma's verksamhetsnamn is the user's choice, and a known fiscal
year becomes a confirm question. Registry outages degrade to the full
question list instead of failing onboarding.

The onboarding skill and the plugin's /accounted:setup command are updated
to the orgnr-first flow (plugin 1.2.0). tools/list ceiling bumped 61.2K to
61.5K with the reason documented in the bench.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 13:03:02 +02:00
Mattsson edfdbe2d2a fix(auth): move the BankID flow into a signed, user-gated, single-use cookie (#1625)
* fix(auth): move the BankID flow into a signed, single-use, confirm-on-resume cookie

A user's BankID signup identified successfully four times and created no
account. His screenshots show four tabs, one on the finished "Verifierad med
BankID, ange e-post" step, and the tab he was looking at showing the idle
button. Prod agreed: no bankid_identities row, no auth.users row.

On iOS outside plain Safari the BankID return URL is handed to the OS, which
opens a NEW tab. The session lived in per-tab sessionStorage, so that tab
started empty and rendered the start button while the completed flow sat
stranded. Login hid it (self-finishing, cookie-backed session); signup waits
for a human to type an e-mail into the stranded tab, so it dies there.

The session id is no longer handed to the browser. It lives in a signed
__Host- HttpOnly cookie set at /start; /poll, /complete, /link and /cancel
read it. Cookies are shared by every tab of the origin, which is what the
handoff needed. The id had to leave the client because it is an
unauthenticated bearer credential: /poll was skipAuth and returned
user.personalNumber, and /complete with mode 'login' returns a tokenHash that
verifyOtp turns into a session, MFA skipped for bankid_linked accounts.

A completed identification must never be consumed by whoever merely opens the
page. A shared cookie plus a shared machine means the tab that finds a
completed flow cannot prove the person at it is the one who made it, and no
client-side token can prove otherwise: nothing survives an iOS same-tab reload
yet dies on reopen-closed-tab / session restore / tab duplication. So a resume
is never automatic. The mount probe routes any found live flow to a confirm
card ("Fortsätt bara om det var du") that reveals no name, and only that click
polls and consumes. Auto-consume happens only inside the live component
instance that called startSession (desktop QR; the pre-navigation mobile
launch), which by construction is the originator. Cost: one tap after
returning from the BankID app on iOS, exactly where the reported bug lives;
desktop and Android never hit the resume path.

The rest is defence the four review rounds proved load-bearing:
- __Host- with Path=/ and unconditional Secure, so a script cannot plant the
  same name at a longer path; readBankIdFlow fails closed on duplicates and on
  a malformed percent-escape.
- Single-use is a unique index (bankid_consumed_sessions), claimed before
  generateLink, not a Set-Cookie. Fail-closed on any non-23505 error, so the
  migration MUST be applied before the code.
- A link flow requires auth at /start and pins userId; /link rejects a flow
  owned by anyone else, before any TIC call. mode is pinned and /poll rejects a
  body mode that does not match, so a login session cannot finish through the
  signup panel. /poll withholds the holder name from a probe. The 900s
  verified-step window is capped by MAX_TOTAL_LIFE from a signed startedAt.
  /poll never clears the cookie (an untargeted Set-Cookie would delete a newer
  flow); only /cancel and terminal /complete + /link exits clear. Avbryt holds
  a 'cancelling' state until /cancel resolves so a new /start cannot race the
  clear. Session id is logged only as an 8-char prefix.

The launch is untouched: iOS keeps its return URL, Android keeps redirect=null
(#194 closed that path deliberately).

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

* fix(auth): bind BankID actions to the resumed flow

* docs: record BankID staging migration drift

* fix(auth): address BankID PR review

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:07:29 +02:00
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
Mattsson f63d3e3100 Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects

We never sent auth_method to Enable Banking, so it fell back to the ASPSP's
visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate*
PSUs the redirect flow does not support Mobile BankID, so authorization failed
right after the user approved in the BankID app. Mobile BankID at Handelsbanken
is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses
when requested explicitly.

Resolve the bank's preferred auth method before /auth: query the ASPSP's
auth_methods and pick the DECOUPLED (Mobile BankID) method when present,
otherwise leave auth_method unset so banks that already work are untouched.
The method name is read dynamically per psu_type, so it is robust across
sandbox/production naming.

- api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods
  field name (was available_auth_methods, never populated), add
  getPreferredAuthMethod(), thread optional authMethod through startAuthorization
- index: resolve authMethod in /connect and pass it on both fresh + reconnect
- tests: cover method selection and request-body shaping

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

* refactor(invoice-inbox): clean up bulk-selection toolbar UI

Redesign the selection toolbar shown when inbox items are checked:
one solid primary "Bokför valda" button with outlined secondary
actions ("Fråga assistenten", "Ta bort") and a plain selection
count. Removes the redundant "Avmarkera" button (users uncheck the
still-visible box), fixes label clipping, and gives the toolbar more
breathing room.

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

* chore(entitlements): bypass paywall in local development

Add isPaywallBypassed() so all gated capabilities are testable locally
without a subscription. Fires only on NODE_ENV=development (npm run dev)
or an explicit DISABLE_PAYWALL=true escape hatch — production builds run
under NODE_ENV=production and the entitlement suite runs under 'test',
so both keep exercising the real gate.

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

* fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer

TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand.

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

* feat(transactions): implement categorize core for bank transaction categorization

- Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations.
- Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing.
- Implemented fiscal period validation and duplicate booking detection.
- Enhanced logging and error handling for transaction categorization.

feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata

- Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken.
- Outputs metadata for business and personal PSU types, including default authentication methods.

fix(migrations): increase statement timeout for SIE bulk delete operations

- Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports.

feat(migrations): add bulk book inbox items to pending operations

- Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`.
- Supports bulk booking of matched inbox items against bank transactions.

test(pg): add tests for replace_period_opening_balance_link RPC

- Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow.
- Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries.

* fix(sie-export): update journal entries and lines handling in SIE export tests

* fix(migrations): resolve version collision on 20260629160000

The SIE bulk-delete statement_timeout migration shared version
20260629160000 with journal_entries_list_series_filter (merged from
main via #798/#823), causing a schema_migrations_pkey duplicate key
error on apply. Rename the branch's migration to 20260629160100.

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

* fix(compliance): resolve compliance-swarm + review findings

- opening-balance/correct: compensating rollback for the non-atomic
  storno+rebook so a mid-sequence failure never leaves two posted OB
  entries (ASVS V2.3); durable audit event on every failure path
  (V16); reference the original verifikationsnummer in the corrected
  entry per BFL 5 kap 5§; document that requireWrite already enforces
  write-role + membership (V8.2.1 was a false positive)
- reports sources routes: validate the cursor date component as ISO
  (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2),
  applied to both the VAT-declaration and trial-balance routes
- AgentSessionList: await the rename PATCH, revert the optimistic
  title and toast on failure (ASVS V4.5)
- bank booking: exclude same-batch siblings from the booking-time
  duplicate guard so bulk-booking distinct same-(date,amount)
  transactions no longer false-positives; pre-existing duplicate
  detection is preserved
- BulkBookInboxDialog: drop the unsafe currency-based reverse_charge
  default, add an omvänd skattskyldighet advisory, and type VAT
  options to the backend VatTreatment union
- OpeningBalanceRowEditor: hold onChange in a ref (synced in effect,
  not during render) so an unstable callback can't cause a render loop

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:13:00 +02:00
Jakob Wennberg c74b19df1b Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances

Two related fixes to bank reconciliation correctness:

1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
   an existing voucher previously advanced only the invoice — the bank
   transaction that paid it kept sitting in the Transactions inbox with a null
   journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
   call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
   links the bank transaction to the same verifikat when exactly one unbooked
   line matches it. Best-effort and post-commit: a failure here never fails the
   link. The result surfaces reconciledTransactionId; the inbox row leaves the
   list and the UI shows link_success_tx_reconciled.

2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
   matching RPCs identify a cash account's ingående balans solely by
   journal_entries.source_type='opening_balance'. Companies migrated from other
   systems often booked the bank IB as an ordinary voucher (source_type
   'import' or 'manual'), so it was never excluded and surfaced as a phantom
   reconciliation difference equal to the opening balance. Adds:
   - migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
     immutability trigger plus a SECURITY DEFINER RPC that validates the entry
     (balance-sheet lines only, dated on a fiscal-period boundary), flips the
     source_type, and writes an audit row — no blanket data sweep.
   - POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
   - BankReconciliationView action to trigger it from the IB diff.

The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.

Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.

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

* chore: rebrand gnubok → Accounted and prune swarm agent skills

Product rebrand and skills housekeeping. No runtime behaviour change.

Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).

Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:52:01 +02:00
Jakob Wennberg f53725b20a Agent v1 bundle: TIC v2 onboarding, in-app assistant gating, sidebar nav, MCP fixes (#584)
* fix(sie-import): accept tab as field separator (Bollbok exports)

The SIE 4 spec allows either space or tab between fields, but
splitSIELine() only treated space (0x20) as a separator. Bollbok
exports tab-separated lines for every record except #RAR, which
silently swallowed all #IB / #UB / #KONTO / #KTYP / #VER / #TRANS
records — imports appeared empty even though the file was well-formed.

Also adds a parser-side diagnostic that emits a warning when raw #IB
or #VER lines are present in the input but parsing produced none. The
previous silent failure is how this bug stayed hidden; the warning
gives the import preview something visible to surface next time.

Verified against two real reproducer files (Sean / Erik Hellqvist):
  erik h 2025.SE (UTF-8): 166 accounts, 66 IB, 4 UB, 11 RES, 95 vouchers, 198 TRANS.
  erik h 2026.SE (CP437): 166 accounts, 66 IB, 4 UB, 0 vouchers.
Both now parse with zero warnings/errors.

Tests:
  + 8 Bollbok-shape tab-separated fixtures (2025 + 2026 quoting variants).
  + 4 silent-failure diagnostic-warning tests.
  All 74 sie-parser tests pass; 155/155 in lib/import; 64/64 downstream callers.

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

* fix(sie-import): address PR #513 review — strip #KTYP quotes, suppress redundant aggregate warning

Two non-blocking P2 findings from Greptile review on PR #513:

1. #KTYP handler stored fields[2] directly, so Bollbok 2026 exports
   (#KTYP\t1510\t"T") stored '"T"' with literal quotes instead of 'T'.
   Latent defect — accountType is unused downstream today, but my tab-
   separator fix made the quoted-value path reachable. Now routes through
   parseStringField so both Bollbok 2025 (unquoted T) and 2026 (quoted "T")
   land as 'T'.

2. The aggregate "kontrollera fältavskiljare och teckenkodning" warning
   fired alongside per-record 'error'-severity issues for malformed #IB /
   #VER records, producing a misleading hint when the parser had already
   pinpointed the structural problem. Now suppressed when an error-severity
   issue with the same tag already exists.

Test coverage:
  + accountType asserted to be 'T' (not '"T"') in both 2025 + 2026 shapes.
  + VER aggregate-warning test now uses #VER lines without { } blocks
    (silent loss, no per-record error) — the canonical case the diagnostic
    is designed for.
  + New suppression test: bare #VER produces per-record errors AND the
    aggregate warning is absent.

75/75 sie-parser tests pass; 156/156 in lib/import.

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

* wip: agent chat + composer + memory + document extraction

In-progress work on this branch beyond the SIE-import fixes:
- Specialized accountant agent (composer + intents + chat loop)
- Persistent agent_conversations/messages, agent_profiles, agent_memory
- /chat surface + /onboarding/agent + /settings/agent-memory
- document-extraction extension with status hooks
- MCP server staging refactor + new skills (atoms, bank reconciliation,
  customer onboarding, kreditfaktura)
- pending_operations rejection feedback (category + reason) + realtime
- TIC company profile cached snapshot on companies
- 17 migrations (all additive — see prior conversation analysis)

Parked while branch waits for review/merge. Migrations are already
applied to prod.

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

* refactor(tic): migrate company-data client from api-core v1 to Lens v2

Swaps the seven TIC company-data endpoints we call from the api-core
paths (`/datasets/companies/{companyId}/...`, `/search/companies`) to
the Lens equivalents (`/companies/{id}/...`, `/search-public/companies`).
Hard cutover; proxy pattern preserved.

Schema shifts handled inside the extension so consumers (TicWorkspace,
Step2CompanyDetails) don't need changes:

- `/companies/{id}/bank-accounts` now returns Bankgirot only — map to
  the existing `{ type, accountNumber, bic }` shape, drop terminated.
- `/companies/{id}/industries` returns a discriminated array — filter
  to `companyIndustryCodeType === 'sni2007'` to preserve v1 behavior.
- `/companies/{id}/phone-numbers` renamed the field to
  `phoneNumberFormatted` (fall back to `e164PhoneNumber`).
- `/companies/{id}/documents` replaces `/financial-report-summaries`;
  filter `type === 'annualReport'` and read nested
  `financialReportMetadata` to rebuild the legacy summary shape.
- `isCeased` is now a top-level boolean; `activityStatus` is an enum.
  Translate enum -> 'ceased' for the workspace's existing check.

BankID identity flow (id.tic.io) is untouched — separate TIC product.

Note: deploy gated on the TIC proxy being flipped to lens-api.tic.io
with an `x-api-key` Lens key.

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

* feat(tic): expose v2 onboarding & workspace data

Adds six new Lens (v2) fetchers on top of the migration that already
landed in this branch, surfacing the data through /lookup and /profile.

New fetchers in lib/tic-client.ts:
- getFiscalYears          /companies/{id}/fiscal-years
- getAccountingPeriods    /companies/{id}/accounting-periods
- getPayrolls             /companies/{id}/payrolls
- getSignatory            /companies/{id}/signatory
- getRepresentatives      /companies/{id}/representatives
- getCompanyStatus        /companies/{id}/status

/lookup gains a fiscalYear field (current fiscal-year configuration)
so onboarding Step 2 can skip manual MM-DD entry. CompanyLookupResult
extended with optional fiscalYear; consumers without it keep working.

/profile gains five new sections on TICCompanyProfile:
- fiscalYear + fiscalYearHistory   current + deduped period list
- signatory                        firmateckning descriptions
- board + representatives          board-composition summary + active
                                   officers (positionEnd in future)
- payrolls                         payroll2 array newest-first, with
                                   deviation vs annual-report
- statuses                         current+historical status entries
                                   with red/yellow/green/neutral color

TicWorkspace renders the new data as four cards (Status, Fiscal year +
Signatory, Board + Representatives, Payroll history) plus a Badge
mapping for the traffic-light status color.

Tests: 52 -> 60 passing. Added unit tests for the new fetchers' v2
paths, fiscal-year auto-fill in /lookup, and full v2 profile coverage.

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

* feat(onboarding,agent): lean on TIC v2 to skip Steps 1 & 3 and sharpen Opus

Three small wins that unlock more of the v2 cutover. No new endpoints — the
data was already in the snapshot, just not flowing where it should.

Step 1 (entity_type) — deep-link path only:
- /lookup now returns `legalEntityType` and `registrationDate` (added to
  CompanyLookupResult).
- /onboarding/page.tsx does a server-side /lookup prefetch when
  ?org_number= is present (BankID picker path), maps "AB"/"EF" to the
  EntityType enum, and seeds Step 1's radio. Falls through silently for
  unsupported codes (HB, KB, …) and on TIC errors.
- WelcomeOnboarding hydrates ticLookup state from the server prefetch so
  Step 2's debounced client fetch and Step 3's first-year inference both
  have data on first render — no flash.

Step 3 (is_first_fiscal_year) — every path:
- deriveFirstYearDefaults() parses ticLookup.registrationDate and returns
  { isFirstFiscalYear, firstYearStart } when registered <12 months ago.
  Step 3's initialData picks it up; the user only confirms the end date.
- Settings value wins when present so existing users with a saved choice
  don't get overridden.

Composer prompt:
- redactTic allowlist was the bottleneck — it stripped beneficialOwners,
  signatory, board, representatives, payrolls, statuses, fiscalYear
  before Opus ever saw the JSON. Existing filterRedundantQuestions
  ownership logic was effectively dead because the data path was severed.
  Expanded allowlist to include those v2 sections; kept bankAccounts/
  email/phone/fiscalYearHistory/financialReports out (token cost > signal).
- SYSTEM_PROMPT now documents each v2 section and the rules Opus should
  apply: payroll signal switches from "registration.payroll" to "actual
  payrolls[] filings" (kills the false-positive swedish-payroll selection
  for newly registered employers); beneficialOwners[] becomes the
  authoritative ownership source (single owner → FMB modifier; multiple →
  multi-owner); statuses[] isCeased/red triggers an uncertainty_note.

Tests: 4112 unchanged. Build: green. No schema or migration changes.

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

* fix(agent): onboarding polish + composer signal fixes from first-run feedback

UX:
- AgentOnboarding: drop the 10s "Hoppa över — fortsätt med standardval"
  escape hatch. The fallback path runs automatically on timeout; the
  manual skip just teased users into a degraded build.
- ReviewCard step 2 title: "Stämma av detaljerna" → "Stäm av detaljerna"
  (imperative form matches the rest of the steps).
- Drop em-dashes from user-visible Swedish strings in AgentOnboarding +
  ReviewCard (fallback labels, subtitles, placeholder, error message,
  final CTA). Em-dashes survive in code comments only.
- "Fråga min revisor" → "Fråga min assistent" everywhere it surfaced:
  AgentTrigger, AgentSparkleButton, ReviewCard preview, ReviewCard
  fallback comment, general.help intent buttonLabel + prompt text.
- AgentTrigger / AgentSparkleButton / EmptyState.AgentHelpLink /
  TransactionInboxCard ask-button all gated on identity.isVerified.
  Pre-onboarding users no longer see the floating FAB or per-page
  Sparkle buttons. AgentSheetProvider.identity gained an isVerified
  field; (dashboard)/layout.tsx selects agent_profiles.verified_at and
  passes it through.

TIC verksamhetsbeskrivning:
- tic/index.ts /profile: /companies/{id}/purposes returns every
  historical verksamhetsföremål filing. Picking [0] was returning the
  oldest "äga och förvalta" holding-company boilerplate for companies
  whose later filings narrowed the purpose ("tillhandahålla
  företagskrediter och finansiella teknologilösningar"). Sort the
  array by lastUpdatedAtUtc desc and take the most recent non-empty
  purpose.

Composer banking signal:
- loadBankingSummary now reads journal_entry_id alongside
  description/amount/date and returns per-counterparty `direction`
  ('in' | 'out' | 'mixed') and `has_unbooked` (any row not yet booked).
  Aggregate `unbooked_count` accompanies the rollup.
- buildUserPrompt emits each counterparty as
  `Name: 12 345 kr (ut, OBOKFÖRD)` so Opus can tell income from cost
  on sight and tell which counterparties are still open questions.
- SYSTEM_PROMPT now explicitly forbids verification questions about
  counterparties whose direction is unambiguous AND status is 'bokförd'.
  Should kill the regressions from the first agent build:
  * "Konsult, J 98 565 kr — intäkt eller kostnad?" when the amount is
    clearly negative.
  * "ALMI AB 493 000 kr — lån eller bidrag?" when the transaction is
    already categorized.

Tests: 4112 unchanged. Build: green.

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

* fix(agent,ui): representation needs deltagare+syfte, drop duplicate doc icon

Representation booking:
- transaction-categorization prompt now requires the agent to capture
  participants (name + company) AND purpose before staging a
  representation categorization. SKV's representationsregler + ML 8 kap
  require the verifikation to document who attended and what the
  meeting was about; without that the avdrag is denied and the post
  should be booked as non-deductible / personalkostnad.
- The agent confirms back in plain text (audit trail in the chat),
  writes the deltagare + syfte to gnubok_remember_fact (long-term),
  THEN stages. Saknas deltagare/syfte: explicitly tell the user the
  avdrag won't go through and offer the non-deductible alternative.
- Known gap (followup, not this commit): the staged op's journal entry
  description doesn't yet carry the deltagare text. Until we add a
  `notes` field to gnubok_categorize_transaction, the audit trail
  lives in chat + agent_memory only.

TransactionInboxCard duplicate attachment indicator:
- Drop the FileCheck2 "open document" button from the trailing slot.
  TransactionAttachmentIndicator (Paperclip) next to the description
  already opens the underlag on click. Two icons doing the same thing
  was noise. Cleaned up the unused state (isOpeningDoc, hasAttachment,
  handleOpenAttachment) and dropped now-unused imports (FileCheck2,
  useToast).

Tests: 4112. Build: green.

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

* feat(agent,nav): notes on verifikation + redesigned sidebar

Audit-trail notes for representation:
- gnubok_categorize_transaction gains an optional `notes` string.
  Threaded through stagePendingOperation → commitCategorizeTransaction →
  createTransactionJournalEntry, which now appends notes to the entry's
  description (capped at 500 chars). The verifikation an external auditor
  reads now carries deltagare + syfte directly — not just chat history /
  agent_memory.
- transaction-categorization prompt updated: representation flow now
  REQUIRES the agent to pass deltagare+syfte via the notes parameter.
  Without it the booking is non-deductible / personalkostnad per SKV.

DashboardNav redesign:
- Top section: flat, no header — Hem (/chat), Underlag (was
  Dokumentinkorg), Transaktioner, Granskning. Always visible; the inline
  badge on /pending shows the count when there are pending ops.
- Mid section: four collapsible dropdowns (Försäljning, Inköp,
  Redovisning, Personal). Each auto-expands when the active route lives
  inside it. KPI moved from main to Redovisning. Extension nav items
  (TIC workspace, etc.) fold into Redovisning.
- Bottom-left: new account popover (DropdownMenu, opens upward) holding
  CompanySwitcher, Inställningar, Hjälp, Support, Logga ut. Replaces
  the old top company-switcher card + the bottom Support/Logout block.
- Mobile drawer mirrors the new structure: top items as flat list,
  same four dropdown groups, separate "Tillägg" section when
  extensions exist, "Mitt konto" section at the bottom.
- i18n: invoice_inbox label renamed "Dokumentinkorg" → "Underlag"
  ("Documents" in en). New keys: mitt_konto, group_extensions.

Tests: 4112. Build: green.

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

* fix(nav): unhide Leverantörer under Inköp

The /suppliers entry existed in navItems but was marked hidden — leftover
from when the supplier list lived elsewhere in the IA. Removing the
hidden flag puts Leverantörer in the Inköp dropdown alongside
Leverantörsfakturor.

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

* fix(nav): CompanySwitcher back to top-left, user account moves bottom-left

The previous pass collapsed both concepts into the bottom popover. They
mean different things: the company is the org context everything below
operates against (top-of-sidebar, scannable); the user is the
account-holder (bottom-of-sidebar, where settings/logout live).

- (dashboard)/layout.tsx: fetch profiles.full_name alongside the
  existing identity queries; pass userName + userEmail into
  DashboardNav.
- DashboardNav: restore CompanySwitcher at the top of the sidebar
  (pre-redesign placement). Bottom-left popover trigger now shows the
  signed-in user's name + single-letter initial (accountInitial helper
  falls back to email's first char, then "?"). Popover header carries
  full name + email; items unchanged (Inställningar, Hjälp, Support,
  Logga ut). CompanySwitcher removed from inside the popover — nested
  dropdowns were awkward and the top placement is where it belongs.

Tests: 4112. Build: green.

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

* fix(pending): trim the agent context strip

The row-level AgentContextStrip on /pending was rendering the model
name (eu.anthropic.claude-sonnet-4-6) and the full atoms array
(horizontal/swedish-vat, vertical/konsult-it, …) inline, which made
each row 60–80 chars of mostly-the-same metadata. Reviewers never
scan that text; they scan amounts and decide approve/reject.

Now the strip shows only the conversation deep-link
(Konversation #<short id>) — the one piece that's actually useful for
diving into context. Model + atoms remain available in agent_metadata
for debugging surfaces; they're just not in the list view.

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

* fix(agent): shared ground rules + paragraph breaks after tool calls

Two regressions surfaced in real usage. Both are systemic.

Shared agent ground rules:
- /chat surface (general.help) was happily inventing four-digit BAS
  account numbers ("Debet 6212 - Molntjänster…", "Kredit 2614 - Ingående
  moms…") and proposing booking decisions on invoices it had never
  seen, with no follow-up questions about currency/scope/etc.
- transaction-categorization had those rules baked into its prompt;
  general-help / bokslut-step / invoice-draft / supplier-invoice-review
  / verifikation-draft / vat-review never inherited them.
- Extracted lib/agent/intents/shared-rules.ts with five cross-cutting
  rules: underlag first (check inbox + ask user to upload to
  Dokumentinkorgen when missing), ask follow-ups when ambiguous, never
  write four-digit BAS account numbers in chat (category names only),
  cite atoms / load skills (don't guess), check counterparty history
  before proposing.
- Injected renderAgentGroundRules() into all six intents above.
  transaction-categorization left alone — it has more detailed inline
  rules tied to its specific underlag-flow.

Paragraph break after tool calls:
- text_delta from the model often resumes after a tool call without a
  leading newline ("kategoriseras." → gnubok_query_journal runs → "Inget
  historik hittades…" appended directly). Markdown rendered the
  concatenation as one paragraph.
- AgentChat text_delta handler now inserts \n\n when (a) the buffer
  ends with text content, (b) the incoming delta starts with text
  content, (c) at least one tool call has run, and (d) the buffer
  doesn't already end with a blank line.

Tests: 4112. Build: green.

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

* fix(nav): default-open dropdown groups; closing is per-user

Dropdowns started collapsed which meant first-time users had to open
each group to discover what's inside. Inverted the state: default open,
user can collapse, active route still forces a group open.

- manualExpanded → manualCollapsed (semantics flip)
- toggleGroup unchanged externally; flips the bit
- isGroupExpanded returns !manualCollapsed[g] || hasActiveChild

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

* feat(agent): rate-safe v1→v2 TIC upgrade, counterparty defaults, profile settings

Three pre-ship quality wins.

Rate-limit-safe TIC v2 upgrade:
- The /profile endpoint fans out to ~13 Lens calls; the account has a
  ~3000/mo ceiling. Force-refreshing every pre-v2 (v1) snapshot across
  the customer base would blow the budget.
- ensureTicSnapshot gains an `upgradeV1` flag. A cached snapshot still
  inside the 7-day window is re-fetched only when (a) the caller passes
  upgradeV1 AND (b) the snapshot is v1-shaped (missing the v2-only
  `statuses` key). Gated to the two agent-onboarding call sites — a
  deliberate, once-per-company action and the only consumer of the v2
  sections. Workspace + signup keep the natural 7-day staleness, so the
  v1→v2 migration is lazy and bounded to companies actually building an
  agent.

Known-counterparty defaults (shared-rules):
- Agent now proposes a sensible default for well-known counterparties
  instead of asking the same question monthly: Almi → lån, Tillväxtverket/
  Vinnova/EU-stöd → bidrag, Skatteverket → skatt/avgift or återbäring,
  Bolagsverket → avgift, Försäkringskassan → ersättning, EF private
  withdrawal → eget uttag. Stated as an assumption the user can correct,
  not a hard rule — underlag/history still wins.

Företagsprofil settings page:
- New /settings/agent-profile (Företagsprofil / "Company profile"):
  view + edit the agent's company profile after onboarding — assistant
  name + avatar, the profile summary the agent reasons from, and a
  read-only chip view of loaded specialities (atoms). Backed by the
  existing GET/PATCH /api/agent/profile.
- New GET /api/agent/atom-titles?ids= resolves atom slugs → human titles
  for the chips (registry is globally-readable reference data).
- Added to SettingsSidebar; i18n keys agent_profile (sv "Företagsprofil"
  / en "Company profile").

Note: /chat already redirects unverified users to / (chat layout guard),
and / renders WelcomeGate → /onboarding/agent. No redirect work needed.
AgentSetupBanner.tsx is orphaned dead code (WelcomeGate superseded it).

Tests: 4112. Build: green. Both new routes compile.

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

* feat(nav,agent): Hem=Översikt + separate Assistent button; memory dedup

Nav restructure:
- "Hem" now points to / (Översikt dashboard) again, not /chat. The agent
  chat gets its own top-level nav entry "Assistent" (Sparkles icon) → /chat.
  Mobile bottom nav mirrors this (Hem / Assistent / Transaktioner).
- / restored to render DashboardContent (the Översikt) for built-agent
  users instead of redirecting to /chat. Users who haven't built their
  assistant yet still get WelcomeGate (the build-agent checklist); once
  verified, / shows the dashboard. Chat is reachable anytime via its nav
  entry. Restored main's dashboard data-fetch; added an agent_profiles
  verified_at probe to drive the WelcomeGate branch.
- i18n: nav.assistant ("Assistent" / "Assistant").

agent_memory dedup (gnubok_remember_fact):
- The agent re-remembers the same fact constantly (e.g. "Vercel = omvänd
  skattskyldighet" on every Vercel categorization), which would bloat
  agent_memory with paraphrases over months.
- Before insert, compare the incoming fact against the 300 most-recent
  active memories by word-set Jaccard similarity (lowercased, punctuation-
  stripped, stopwords dropped). A near-duplicate (≥0.82) is treated as
  already-known: bump its relevance toward the new score + refresh
  updated_at instead of writing a new row. Embedding-free, zero added
  latency beyond one bounded SELECT.

Tests: 4112. Build: green.

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

* fix(agent,nav): företagsprofil=Bolagsuppgifter, avatar nav icon, dedupe greeting

Företagsprofil settings page (the right content this time):
- Replaced the agent atoms/summary panel with CompanyProfileView — a
  read-only "Bolagsuppgifter" view of the cached TIC company snapshot
  (name, org-nr, form, address, F-skatt/Moms/Arbetsgivare, SNI, bank,
  verksamhet, employees, latest financials, status traffic-lights,
  fiscal year, firmateckning, företrädare). Server component reads the
  companies.tic_snapshot column directly — no extension import, stays
  inside the core-build boundary.
- Route renamed /settings/agent-profile → /settings/company-profile.
  Removed the old AgentProfilePanel + the now-unused /api/agent/atom-titles
  endpoint.

"Assistent" nav icon = the agent's chosen avatar:
- DashboardNav reads agent identity from AgentSheetProvider and renders
  the onboarding-chosen avatar for the /chat ("Assistent") entry across
  desktop sidebar, mobile drawer, and mobile bottom nav. Falls back to
  the Sparkles glyph pre-onboarding (no avatar yet).

Nav cleanup:
- Dropped the beta badge from Underlag.
- Filtered the TIC workspace (/e/general/tic, "Företagsprofil") out of
  the nav — the same Bolagsuppgifter now lives under Inställningar →
  Företagsprofil, so it shouldn't appear in two places.

Doubled intake greeting fix:
- /chat/intake fires an invoke with no conversation_id, then swaps the
  URL to /chat/[id] the instant the `conversation` event lands — which
  can beat the greeting being persisted. /chat/[id] then hydrated with 0
  messages and, because the auto-fire guard keyed on (id && messages>0),
  fired a SECOND invoke on the same conversation → two greetings.
  Guard now keys on conversation-id presence alone: a set id means
  resume, never bootstrap. Closes the race.

Tests: 4112. Build: green.

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

* fix(agent): paragraph-break-after-tool split words mid-stream

The earlier "insert \n\n when text resumes after a tool call" heuristic
re-evaluated on EVERY text_delta (any delta not starting/ending with
whitespace, once a tool had run). Streaming deltas arrive in sub-word
chunks, so it injected breaks between fragments of the same word:
"minnes\n\nno\n\nterna", "kund\n\nrep\n\nresentation".

Replace the per-delta heuristic with a consume-once ref:
- tool_use sets breakBeforeNextTextRef = true
- the next text_delta consumes it: prepends \n\n exactly once (only when
  the buffer has content, doesn't already end in whitespace, and the
  delta doesn't start with whitespace), then clears the flag

So the break fires once per tool→text resume, never mid-word.

Build: green.

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

* fix(agent): much shorter replies, representation headcount + VAT cap, dot separator

Brevity (system-prompt Svarsformat — affects every reply):
- Hard "korthet är regel nummer ett": aim for 2-4 sentences, lead with
  the answer/action, no warm-up ("Här är vad som gäller…"), don't derive
  VAT in prose, don't restate what the approval card shows, one question
  at a time. The agent was writing textbook-length essays.

Representation rule now in shared-rules (so verifikation-draft, vat-review,
etc. all get it — previously only transaction-categorization had it, which
is why the verifikation flow guessed 25% VAT and skipped the cap):
- Require ANTAL deltagare (headcount), not just one name — the moms
  deduction is per person (underlag cap 300 kr/person ex moms).
- Use the receipt's ACTUAL VAT rate (usually 12% on food), never assume
  25%.
- Meal representation isn't income-tax deductible (post-2017); whole cost
  booked as non-deductible representation.

Verifikation description separator:
- createTransactionJournalEntry appended notes with an em-dash
  ("Utlägg Eatnam — Deltagare:…"), violating house style. Switched to a
  middle dot " · ". journal_entries has no separate notes column — the
  description IS the BFL verifikationstext / audit field, so deltagare +
  syfte correctly live there.

Tests: 4112. Build: green.

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

* fix(settings): tidy Bolagsuppgifter — no status colours, clean firmateckning

From first-look feedback on the Företagsprofil page:

- Status: dropped the coloured traffic-light badges (red/yellow/green).
  Per the design system semantic colour is data-only, never chrome, so
  status now renders as plain label + date. Also filtered to dated
  entries only — Bolagsverket emits flags like "Har aldrig varit verksam"
  with no date that read as noise next to the real status. Ceased status
  gets muted destructive text (the one chrome colour the system keeps).

- Firmateckning: the source text carries ">" list markers and crams
  several rules onto one line, and repeats "Firman tecknas av styrelsen"
  across rows. cleanSignatory() strips the markers, normalises whitespace,
  splits run-on "Firman tecknas …" clauses onto separate lines, and the
  render dedupes — so each rule reads as its own sentence.

Build: green.

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

* fix(mcp): inbox items expose all terminal links + processed flag

The Eatnam receipt was booked against its bank transaction (so the inbox
row had matched_transaction_id + created_journal_entry_id set), yet the
agent reported it as loose/unmatched and a duplicate risk. Root cause:
gnubok_list_inbox_items only selected and returned matched_supplier_id +
created_supplier_invoice_id — the supplier-invoice path. The
transaction-match and direct-journal-entry paths were invisible, so any
receipt cleared via /transactions looked unprocessed.

- list_inbox_items now selects + returns matched_transaction_id and
  created_journal_entry_id alongside the supplier fields, plus a derived
  `processed` boolean (true when ANY of the three terminal links is set).
- New unprocessed_only=true input filters to items with no terminal link
  — the "what still needs handling" view that prevents the agent from
  flagging already-booked docs as duplicates. (Fetches a wider window
  then filters client-side so limit applies post-filter.)
- Description updated to document the processed semantics, within the
  280-char tool-description budget.

The DB linkage itself already worked: /transactions attach-document sets
matched_transaction_id, and commitCategorizeTransaction stamps
created_journal_entry_id. This was purely a read/surface gap.

Tests: 4112 (+ MCP description guard). Build: green.

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

* fix(mcp): repair stage-but-never-commit tools + consolidate tool surface

- post_annual_depreciation AND reverse_entry were never in the pending_operations operation_type CHECK, so both staged then died with check_violation at INSERT. Add the CHECK migration, a commitPostAnnualDepreciation executor (reusing commitAnnualPostings), risk tier, and the PendingOperationType union member.
- Salary tools de-risked: calculate_salary_run calls runSalaryCalculation() directly (no self-fetch/forged cookie); create_salary_run uses a transactional create-run helper with compensating delete; generate_agi actually generates + persists the declaration.
- import_sie parses + validates at stage time with a content-rich preview (company, fiscal year, voucher/account counts, balance) instead of a blind byte count.
- batch-match-invoices passed user.id where companyId was expected (silently matched zero).
- VAT report+widget merged behind render_ui; gnubok_search_tools ranks by relevance; gnubok_feedback readOnlyHint corrected; tools/list instruction text fixed; income decision-tree + GL/query_journal cross-refs added.

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

* feat(agent): load skill atom bodies from the DB so they survive the build

Skill bodies were read from disk at runtime (.claude/skills/**/SKILL.md); on Vercel the dynamic readFile path isn't traced into the lambda and on Docker .claude/ is excluded, so atoms loaded EMPTY in production — a despecialized agent. Inline the bodies into agent_atom_registry instead:
- Migration adds body + mcp_exposed columns; a build-time generator (scripts/generate-skill-bodies.ts) emits a deterministic dollar-quoted seed migration with a content-hash manifest + --check CI guard.
- Read sites (mcp-server atoms.ts, chat system-prompt.ts, composer prewarm) read body from the DB, with a dev-only disk fallback. mcp_exposed curates which atoms the MCP exposes (swarm-* never become atoms).
- The seed script + generator share scripts/lib/atom-discovery.ts; estimated_tokens now reflects SKILL.md only.

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

* feat(agent): safe the in-app assistant — gating, FAB de-confliction, rate limit, friendly errors

- Hide all agent entry points until verified_at: the Assistent nav tab (sidebar + mobile) and the agent-memory settings tab now match the floating FAB's gate.
- FAB de-confliction: /kpi -> kpi.explain and /bookkeeping/year-end -> bokslut.step so the floating button opens the SAME assistant as the page button (no two-agents-on-one-page).
- Generous per-user rate limit (30/min, 1000/day) on /api/agent/invoke, /onboarding/stream, /composer via a new agent_rate_counters table + check_and_increment_agent_quota RPC; fails open. Bounds runaway Bedrock spend without touching normal users.
- Friendly errors: Bedrock 429/timeout/5xx normalized to Swedish (friendlyModelError) in run-turn + the invoke route; the chat client surfaces the server's friendly message instead of a raw HTTP status.
- /chat/new validates ?intent= against the registry so bad deep-links fall back to general.help instead of rendering a broken-looking error.

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

* fix(agent): keep /chat read-only — redirect categorization + swap the "categorize" suggestion for a VAT-report question

general.help (the /chat assistant) is read-only, but it still gave per-transaction bokföringsförslag in prose and asked "godkänner du dessa?" — an analysis the user can't act on (no write tool, no per-tx underlag). Strengthen the prompt to redirect categorization/bokföring to the per-transaction flow (open the transaction -> "Fråga om denna transaktion", where the agent sees the underlag and stages a real ApprovalCard); a short overview is still allowed. Add a guard test locking in no-write-tools + the redirect language. Swap the /chat empty-state "Hjälp mig kategorisera" chip (which lured users into exactly this dead-end) for a VAT-report question the read-only assistant can actually answer.

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

* refactor(pending): declutter the review queue rows + header

Fold the conversation deep-link onto the actor label (drop the separate
"Konversation #xxxx" strip and its icon), hide the quick-pick when there's
only one operation type (it duplicated "Markera alla"), and drop the "(0)"
from the disabled bulk-approve button.

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

* feat(vat): enhance VAT handling by integrating document validation and improving error messaging

* feat(settings): add assistant knowledge surface + consolidate settings tabs

Expose the agent's skill atoms (agent_atom_registry) in a read-only surface
beside the existing memory view, and tighten the settings tab bar from 14 to
10 tabs.

- New GET /api/agent/skills + AgentSkillsPanel: lists active, mcp_exposed
  atoms grouped by tier (Kärnkompetens / bransch / bolagssituation), flags
  which are active for the company from agent_profiles, and lazy-loads each
  SKILL.md body on expand.
- New /settings/assistant tab with a Minne/Kompetens toggle (?view=skills);
  /settings/agent-memory and /settings/agent-skills redirect into it.
- Merge Företagsprofil (TIC snapshot) into the Företag tab via
  CompanyProfileSection; /settings/company-profile redirects.
- Merge Skatteverket-anslutningen into the Skatt tab — OAuth returnTo and the
  callback toast now target /settings/tax; /settings/skatteverket redirects.
- Drop the Säkerhetsbackup tab (already under Importera/Exportera).

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

* fix(inbox): keep booked underlag out of the unmatched queue + widen match window

- categorize: after booking an inbox underlag onto a verifikat, backfill the
  inbox row's matched_transaction_id + created_journal_entry_id so it stops
  showing as unmatched (mirrors the /attach-document paperclip path).
- TransactionMatchPicker: bias the candidate window forward (60d before →
  180d after the invoice date) so late payments aren't dropped before scoring,
  and widen the ranking date tolerance to 120d so the true match floats to the
  top instead of collapsing to "Svag match". Fix "okatigoriserade" typo.

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

* wip: bundle in-progress branch work + agent onboarding chat optimizations

Captures the uncommitted work-in-progress on this branch so it lives on the
remote. Heterogeneous changeset — bundled as one commit since the work was
already entangled across files.

Headline change in this commit (from this session):
- Remove the double interview in agent onboarding. Phase B's verification-
  question form stepper is gone — the Phase C chat (onboarding.intake) now
  owns the entire interview and reads the composer's verification_questions
  server-side as its question bank.
- ReviewCard collapses from 3 steps to 2 (meet → review-and-confirm) with
  value-first ordering: profile + "vad jag kan hjälpa dig med" + facts +
  optional seed note. CTA reads "Möt {namn}" to signal the chat follows.
- ChatIntakeStarter handoff subcopy updated to match reality (assistant
  greets first; user can leave anytime).
- Stamp agent_profiles.intake_completed_at server-side in
  app/api/agent/invoke/route.ts on the first user-typed reply in any
  onboarding.intake conversation (idempotent IS NULL guard, best-effort).
  Closes the previously dead-write column and unlocks the opportunistic-
  follow-up hook the migration anticipated.

Plus in-progress branch work being carried forward (not introduced here):
agent runtime + intent prompts, composer + atom-discovery scripts, MCP
server skills surface, onboarding flow components, dashboard/inbox tweaks,
two new agent_atom_registry migrations, additional agent-chat tests.

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

* refactor(agent): drop inline "Fråga assistenten" affordances — rely on the FAB

The bottom-right "Fråga {namn}" FAB (AgentTrigger) is already route-aware
and picks the right intent per page, so duplicating it as inline page-
header buttons and empty-state links is noise. Removed:

- EmptyState `agentHelp` link ("Eller fråga {namn} hur du kommer igång")
  + the AgentHelpLink component + agent_default_name/agent_ask_link i18n
  keys + the agentHelp props on EmptyInvoices/EmptyCustomers/EmptyTransactions.
- AgentSparkleButton on /bookkeeping (verifikation.draft) and /kpi
  (kpi.explain) page headers.

The FAB stays — when verified, it appears on those routes and routes to
the right intent automatically.

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

* fix(agent): gate the last two ungated "Fråga assistenten" affordances

Both surfaces previously called useAgentSheet directly without checking
identity.isVerified, so they appeared pre-onboarding (everywhere else the
FAB / sparkle buttons / /chat / Assistent nav are all gated on verified_at).

- Settings page header: remove the "Fråga {namn}" pill entirely. The FAB
  covers /settings routes route-aware (settings.help) — no need for a
  duplicate inline trigger.
- Invoice inbox transaction picker: hide the "Fråga assistenten" button
  when the agent isn't built. Done at the parent (InvoiceInboxWorkspace)
  by passing onAskAssistant only when identity.isVerified is true; the
  child renders the button only when the callback is present.

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

* feat(tic,onboarding,agent): single-call TIC lookup + director-aware narrative voice

- TIC: collapse the company lookup from 6 endpoint calls to 1
  (search-public already exposes sniCodes, bank accounts, emails, phones,
  and registration flags). Derive fiscal-year MM-DD from
  mostRecentFinancialSummary; newly-registered companies fall through to
  the client's first-year defaults.
- Onboarding: BankID picker no longer auto-provisions companies. Every
  pick routes through the wizard with orgnr (and entity_type via the
  CompanyRoles match) prefilled; F-skatt/VAT/address get confirmed in
  steps 2-4 instead of being auto-fetched. createCompanyFromOnboarding
  reuses CompanyLookupResult and adds a defensive top-level catch so
  server-action errors surface to the UI instead of being redacted.
- Agent composer: loadUserDirectorship() checks BankID CompanyRoles for
  a director-like position (ceo/boardMember/chairman/externalSignatory,
  active) before the narrative uses second-person ownership voice
  ("Du driver…"); unknown users get neutral third-person voice so we
  never put ownership words in the user's mouth.

Tests cover loadUserDirectorship, narrative voice, tic-fetch path,
onboarding page, and updated TIC client + lookup/profile suites.

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

* fix(tic): extend agent-onboarding TIC budget to 10s + backfill stranded org_numbers

The 5s TIC fetch timeout aborted client-side before the upstream Lens
fan-out (~13 calls) could complete, but the in-flight upstream calls
still counted against quota — actions.ts already documents ~530 wasted
calls from this in May. Same bug still applied to the agent-onboarding
stream path. Adds an optional `timeoutMs` to `ensureTicSnapshot` so
deliberate wait-screen callers (agent onboarding stream) can run with
10s while background/dev callers stay on the conservative 5s default.

Page-level server fetch (page.tsx) intentionally stays at 5s to avoid
blocking TTFB without a visible progress affordance.

Backfill migration mirrors `company_settings.org_number` to
`companies.org_number` for the 105 cases where it's safe (after dedup
+ conflict filtering). 56 of those are on active companies — unblocks
duplicate guards, SIE/SRU exports, and TIC fallback chain. Zero TIC
API calls — pure data move. Idempotent.

Also sweeps a pre-existing SSRF guard on the stream route's origin
derivation that was sitting unstaged in the working tree — it lives in
the same diff hunks as the TIC budget change and couldn't be split cleanly.

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

* wip: bundle in-progress branch work

Sweep up uncommitted agent/MCP/RLS work-in-progress so the branch is fully
backed up to origin. Not reviewed in detail — committed as-is to preserve
working state alongside the TIC fixes in the previous commit.

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

* fix(agent): tag the "Bygg din bokföringsassistent" CTA as Beta

Adds a Beta badge next to the assistant-setup heading on the dashboard
banner, dashboard inline card, and onboarding checklist row. Also drops
the stale "Gratis i 30 dagar" subline from the dashboard card.

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

* fix(build,migrations): PendingOperationType salary ops + resolve migration version collisions

PR #584 went red on three things:

1. core-only build / Vercel: `lib/pending-operations/commit.ts:2666` switched on
   'create_salary_run' and 'generate_agi' but `PendingOperationType` was missing
   both literals. Add them to the union.

2. Supabase preview: migration version 20260526120000 collided with main's
   newly-merged 20260526120000_fix_replace_sie_import_hard_delete.sql.
   Bump the branch's pair to 20260526120050 / 20260526120051 — still ahead of
   20260526120100_restvardeavskrivning so ordering is preserved.

3. 20260527170000 was used twice on this branch
   (_agent_rls_with_check + _journal_entry_no_doc_required). Bump the second
   to 20260527170100 so the pair stays orderable and Supabase doesn't choke
   on the duplicate schema_migrations PK.

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

* fix(ci): reword comment so core-only guard stops flagging it

The "Check no core imports from extensions" step greps for the literal
\`from '@/extensions/\` across lib/, app/api/, components/. A comment in
lib/agent/composer/tic-fetch.ts quoted the exact pattern verbatim to
explain *why* the file does a self-fetch instead of importing the TIC
extension directly — which the grep matched even though no actual
import exists.

Rewrite the line to keep the same meaning without the literal pattern.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
2026-05-28 11:27:01 +02:00
Jakob Wennberg a0485ca1c1 fix: BankID signup takeover + supplier invoice dedup + event_log RLS visibility (#358)
* fix(tic): reject BankID signup when email is already registered (CWE-287)

Signup previously linked BankID to any pre-existing profile matching the
submitted email. Because BankID proves identity but not email ownership,
an attacker who knew a victim's email could bind their own BankID to the
victim's account and then log in via BankID (which skips TOTP MFA).

Now signup returns 409 account_exists; the register page shows a Swedish
error toast and redirects to /login so the user can authenticate with
their password first and link BankID from settings (via the authenticated
/bankid/link route that already exists).

Covered by new bankid-complete.test.ts with an explicit regression test
asserting no side-effects occur on the account_exists path.

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

* fix(bookkeeping): prevent duplicate registration entries for supplier invoices

The invoice-inbox convert flow creates the registration journal entry
inline and then emits supplier_invoice.confirmed. The core handler was
also creating one, producing a second voucher on 2440/2641/expense and
overwriting registration_journal_entry_id on the row.

Two guards in the core handler:
- Payload guard: skip if supplierInvoice.registration_journal_entry_id
  is already set (fast path for callers that include it in the payload).
- DB re-fetch guard: re-read the row and skip if it has been linked
  since the payload was built (handles stale-payload callers).

The inbox extension now stamps registration_journal_entry_id onto the
in-memory invoice before emitting so the fast path trips.

Also fixes a latent bug where the handler filtered company_settings by
userId instead of companyId, which would have selected the wrong row
(or none) on multi-company accounts.

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

* fix(events): write company_id on event_log rows and refuse unscoped events

event_log has a company-scoped SELECT RLS policy, so rows written with
NULL company_id are invisible to every user — effectively silently
dropped from the automation feed. The handler now reads companyId from
the payload (all persisted event types mandate it in TS) and includes
it in both single and batch inserts.

If a caller ever bypasses the type system and emits without companyId,
the handler logs an error and skips persistence rather than writing a
poisoned row.

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

* fix: address PR review nits (event-log meta strip, redirect timer)

- event-log-handler: rename stripUserId → stripMetaFields and also drop
  companyId from the stored data JSONB so it isn't duplicated alongside
  its dedicated column.
- register page: drop the 1500ms setTimeout before router.push('/login')
  on the account_exists branch — the timer had no cleanup and fired a
  stale-closure push if the component unmounted first. The toast survives
  the route change via the root layout's Toaster.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:51:56 +02:00
Jakob Wennberg 6d75b9a1bf feat: BankID authentication via TIC Identity API (#192)
* feat: add BankID authentication via TIC Identity API

Integrate BankID as a login/signup method using the TIC Identity API.
Users can authenticate with BankID QR codes (desktop) or deep links (mobile),
link BankID to existing accounts, and skip TOTP MFA when BankID is linked.
Removes Step 0 (role choice) from onboarding for all users. Adds enrichment
data support for pre-filling company details from Bolagsverket during signup.

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

* fix: address PR review — server-side rate limit, unlink clears MFA bypass

- Add per-IP rate limit (5s cooldown) on /bankid/start to prevent
  unbounded billable TIC sessions from unauthenticated callers
- Add /bankid/unlink endpoint that deletes bankid_identities AND clears
  app_metadata.bankid_linked so MFA enforcement resumes after unlink
- Update BankIdSettings to call server-side unlink instead of client-side delete

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

* fix: move rate limiter to module scope, add BankID logo and year-end skill

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:44:14 +02:00
Jakob Wennberg e18b8dc789 feat: BFL-compliant descriptions, cancelled status, and TIC company lookup (#57)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2)

Reversed entries (storno) must appear alongside their original posted entries
in reports for a complete audit trail. Previously, filtering by status='posted'
excluded them, causing discrepancies when corrections had been made.

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

* feat: semi-manual invoice payment booking with editable journal lines

When marking an invoice as paid, users now see a dialog where they can:
- Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.)
- Review and edit the proposed journal entry lines before committing
- The happy path remains fast — lines are pre-filled correctly

Implementation:
- Pure proposePaymentLines() function for line computation (accrual + cash)
- PaymentBookingDialog with AccountCombobox, balance validation, date picker
- API accepts optional custom lines, falls back to auto-generation without them
- 18 tests (8 unit + 10 API) all passing

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

* fix: address Greptile review — validation fallback, balance check, error handling

- P1: Return 400 on invalid body instead of silently falling back to
  auto-generated lines (split JSON parse from schema validation)
- P1: Add server-side balance check for custom lines before committing
  (debit must equal credit, totalDebit > 0)
- P2: Wrap PaymentBookingDialog init() in try/catch with toast on
  failure and auto-close instead of silent empty state
- Add 2 new tests: unbalanced lines → 400, invalid schema → 400

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

* fix: OAuth callback redirect for local dev and timeout resilience

- Pass redirectUri dynamically from NEXT_PUBLIC_APP_URL so OAuth
  callbacks work on localhost (not just production)
- Encode consentId/provider in OAuth state (base64url JSON) so the
  callback doesn't depend on session storage
- Add skipAuth flag to extension API routes for OAuth callbacks
  (external provider redirects have no user session cookie)
- Wrap AbortError in descriptive timeout messages in arcim-client
- Make preview endpoint resilient to partial failures (company info
  and SIE fetch are individually non-blocking)
- Simplify login page (remove unused magic link auth mode)

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

* fix: create journal entry before marking invoice as paid

Move journal entry creation before the invoice status update so that
if accounting fails, the invoice is not permanently marked paid without
a corresponding entry. Previously the error was silently swallowed.

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

* fix: update mark-paid tests for journal-first ordering

Reorder mock queue to match new flow (settings before update), update
failure test to expect 500 instead of silent success, add try-catch
with proper error response in route handler.

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

* feat: add reverse charge VAT (ruta 20-32) and improve mobile UX across dashboard

Add full reverse charge (omvänd skattskyldighet) support to the VAT declaration:
- Map accounts 2614/2624/2634 to ruta 30/31/32 for self-assessed output VAT
- Calculate purchase bases (ruta 20-24) from supplier invoices by supplier type
- Include ruta 30-32 in ruta 49 formula and totalOutputVat summary
- Display reverse charge section in reports UI and composition chart
- Add comprehensive test coverage for all reverse charge scenarios

Improve mobile UX across the app:
- Convert nav drawer to bottom sheet with drag handle and safe area padding
- Add mobile card layout for PaymentBookingDialog journal lines
- Replace settings tab pills with dropdown selector on mobile
- Make wizard step indicators responsive (collapsed on mobile)
- Ensure all dialog footers stack buttons full-width on mobile
- Add 44px minimum touch targets throughout
- Make onboarding buttons full-width on mobile

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

* fix: address Greptile review — indentation, query efficiency, tab dedup

- Fix misleading try-block indentation in mark-paid route
- Filter reversed entries at DB level (.eq('status', 'posted')) instead
  of fetching then discarding in memory
- Extract shared settingsTabs array so mobile Select and desktop
  TabsList stay in sync automatically

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

* feat: add resilience fallbacks, Arcim retry logic, and client tests

Add FallbackPrompt component and integrate it across banking and migration
error states so users always have a manual import escape hatch. Add retry
with exponential backoff to Arcim API client for transient failures (429,
502, 503, 504) and timeouts. Expand import page deep-linking with ?mode=
parameter. Add persistent error banner on settings page for bank connection
failures. Include 18 new tests for the Arcim client covering retry, backoff,
pagination, timeout, env validation, and singleton resource unwrapping.

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

* fix: address Greptile review — setActiveTab, test cleanup, redundant clearTimeout

- Add missing setActiveTab('banking') when handling bank_error query
  param so the error banner is actually visible (P1)
- Guard env-var cleanup with try/finally in arcim-client tests to
  prevent state leakage on assertion failure (P2)
- Only mock retry-range setTimeout delays in backoff test, letting
  AbortController timers pass through real setTimeout (P2)
- Remove redundant clearTimeout in catch block — finally handles it (P2)

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

* feat: add BFL-compliant counterparty names to journal descriptions and cancelled entry status

Journal descriptions now include customer/supplier names for traceability
(e.g. "Kundfaktura 1001, Foretag AB"). Failed draft entries are marked as
'cancelled' instead of deleted, respecting immutability constraints.
Includes DB migration for the new journal_entries status value.

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

* fix: address Greptile review — Swedish typos, missing source type, trigger and reversal cleanup

- Fix Swedish spelling: leverantor → leverantör in all supplier description prefixes
- Add supplier_credit_note to supplierSourceTypes in VAT declaration so credit
  notes correctly reduce reverse-charge bases (ruta 20–24)
- Mark orphaned concurrent reversals as cancelled instead of attempting deletion
  that the immutability trigger blocks
- Allow posted → cancelled transition in trigger for orphaned reversal cleanup
- Restrict cancelled entry line trigger to DELETE-only (block INSERT/UPDATE)

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

* fix: use main's Step3TaxRegistration (onboarding restructured in PR #54)

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

* chore: retrigger Greptile review

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

* feat: add TIC company lookup extension, extension nav items, and legacy toggle fallback

Introduces the TIC (Bolagsuppgifter) extension for automatic company data lookup
via org number during onboarding. Adds dynamic extension nav items in the sidebar,
legacy general extension fallback for toggle checks, and company lookup type
definitions in core.

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

* fix: address Greptile review — restore push-notifications, filter nav by toggles, fix timeout error name

- Restore push-notifications to LEGACY_GENERAL_EXTENSIONS (was silently
  dropped when extracting the shared constant)
- Remove tic and arcim-migration from legacy defaults (new extensions
  should not default to enabled for all users)
- Filter getExtensionNavItems() against user's enabled extensions so
  disabled extensions don't appear in the sidebar
- Fix AbortSignal.timeout() error name check — Node.js throws
  TimeoutError, not AbortError

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

* fix: add all bundled extensions to legacy defaults (email, arcim-migration, tic)

Bundled extensions configured in extensions.config.json should default
to enabled. Adds email, arcim-migration, and tic alongside the
existing legacy defaults so they are accessible without explicit
toggle rows.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:31:56 +01:00