Commit Graph
114 Commits
Author SHA1 Message Date
Mattsson 627109b5bd Mcp/entry bug (#580)
* feat: restore document version supersession on posted entries

* refactor: update VAT handling logic to remove dependency on seller registration status

* feat: update VAT handling to respect seller registration status across invoice processing

* feat: add no-op migration placeholder for document supersession
2026-05-27 17:07:27 +02:00
MattssonandClaude Opus 4.7 32d9978f1b Fix/chrome pdf preview csp (#572)
* feat: add option to exclude year-end closing entries in SIE export and related reports

* delete docs

* fix: allow Chrome's PDF viewer in verifikat document preview

The /api/documents/:id/inline route shipped with
`object-src 'none'` in its CSP, which blocked Chrome's built-in PDF
viewer (it renders inline PDFs via an internal <embed>). Users on
Chrome saw "Det här innehållet har blockerats" when expanding a PDF
attachment in the bookkeeping view; Firefox (PDF.js) and Edge (own
viewer) were unaffected, and JPGs worked because <img> isn't subject
to object-src.

Drops the CSP for this route to the minimum needed for embeddability:
`frame-ancestors 'self'`. X-Content-Type-Options: nosniff plus the
fixed Content-Type from the handler already block MIME confusion;
X-Frame-Options: SAMEORIGIN + frame-ancestors still block clickjacking.

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

* feat(auth): add webmail deep link to email confirmation screens

Mirrors Stripe's signup UX: after asking the user to verify their email,
detect their webmail provider from the domain and show a button that
opens the inbox in a new tab. Gmail gets a from:<sender> search
pre-populated; Outlook/Yahoo/iCloud/Proton open the inbox directly.
Unknown / custom domains fall back to the existing copy.

Sender address is configurable via NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM
(default noreply@gnubok.se) so white-label installs can match their
Supabase Auth SMTP config.

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

* fix(auth): unblock first-time password set for BankID users with MFA

Supabase rejects updateUser({password}) and mfa.unenroll with "AAL2 session
is required" whenever a TOTP factor is enrolled. BankID magic-link logins
produce AAL1, and middleware skips MFA enforcement for bankid_linked users,
so they had no path to AAL2 — leaving them unable to set a backup password
or disable MFA without going through the email-recovery escape hatch.

- /api/account/password: branch on app_metadata.has_password. First-time set
  writes via service.auth.admin.updateUserById (no existing credential to
  protect, AAL2 guard does not apply). Change-password keeps the user-session
  updateUser so AAL2 still fires for credential rotation.
- /mfa/verify: accept a safeReturnTo query param and route there after
  successful verify, so step-up flows can land back where they came from.
- SecuritySettings: detect the AAL2 error from both change-password and
  mfa.unenroll and redirect through /mfa/verify?returnTo=/settings/account
  instead of toasting a dead-end error.

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

* Add tests and rounding utility for öre precision in bokslut calculations

- Implemented `roundOre` function for rounding SEK amounts to two decimal places, ensuring consistent monetary calculations.
- Introduced `ORE_TOLERANCE` constant for comparing rounded amounts, facilitating invariant checks in financial entries.
- Created comprehensive tests for `roundOre`, covering typical cases, edge cases, and idempotency.
- Added year-end invariants tests to verify database-level guarantees for closing entries, ensuring they balance to the öre and reject discrepancies.
- Developed end-to-end tests for the dispositions chain, validating the correctness of calculations across various scenarios.

* fix: update PDF rendering to remove Swish QR code generation and set default to disable Swish visibility

* fix: enhance security by rejecting data URIs in safeReturnTo function tests

* fix: improve rounding logic in roundOre function and add customer_type migration

* fix: add customer_type column to customers and enforce CHECK constraint

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:29:41 +02:00
Mattsson b34de598e3 feat: add inbox-direct supplier invoice creation from inbox items (#558)
* feat: add inbox-direct supplier invoice creation from inbox items

- Implemented `gnubok_create_supplier_invoice_from_inbox` tool in the MCP server for creating supplier invoices directly from inbox items.
- Enhanced the input schema to include `inbox_item_id` and `document_id` for direct booking.
- Added logic to validate inbox items and link documents to journal entries during the commit process.
- Introduced `commitCreateSupplierInvoiceFromInbox` function to handle the creation and linking of supplier invoices.
- Added unit tests to cover various scenarios including happy path, idempotency, error handling, and rollbacks.
- Updated database migration to extend the `pending_operations` table to include the new operation type.

* fix: extend CHECK constraint to include create_supplier_invoice_from_inbox operation

* feat: add validation for financial fields in supplier invoice creation from inbox
2026-05-22 11:09:46 +02:00
Jakob WennbergandClaude Opus 4.7 2879f6ed17 fix(inbox): sync matched_transaction_id on MCP-staged document attach (#549)
* fix(inbox): sync matched_transaction_id on MCP-staged document attach

The REST attach route (app/api/transactions/[id]/attach-document) already
updates invoice_inbox_items.matched_transaction_id so the inbox list shows
the "Kopplad till transaktion" indicator. The MCP-staged path through
commitAttachDocumentToTransaction did not, leaving inbox rows looking
unprocessed after a /pending approval. Mirror the REST behaviour with a
best-effort UPDATE (idempotent, gated on both FK columns being NULL).

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

* fix(review): inspect supabase error on best-effort inbox link

The Supabase client resolves with { error } rather than rejecting on
RLS/DB failures, so the previous try/catch wrapping the inbox UPDATE
never fired for the documented failure modes — the console.error was
dead code. Switch to destructured { error } + if-block, matching the
pattern used elsewhere in commit.ts.

Applied to both the new MCP-staged path (lib/pending-operations/commit.ts)
and the pre-existing REST route (app/api/transactions/[id]/attach-document/
route.ts) it was mirrored from. Both tests now assert console.error fires
with the expected payload, so the swallow-and-log path is exercised for
real instead of passing for the wrong reason.

Addresses Greptile P1 + P2 on PR #549.

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-05-21 14:00:49 +02:00
Mattsson e71b4a9138 Add/mcp and visma (#547)
* fix: simplify COMING_SOON_PROVIDERS to include only bjornlunden and briox

* feat: add supplier creation functionality and related operations

* feat: reorder and enhance OAuth scopes in Visma integration

* feat: implement create supplier functionality with validation and risk tier management
2026-05-21 01:25:18 +02:00
Mattsson 16164ea14c Fix/mcp fixes and bugs (#518)
* feat(voucher): add create voucher and correct entry previews; update commit methods

* feat: add support for pending operations in API key scopes and OAuth client management

- Introduced new API key scopes for reading and approving pending operations.
- Updated the scope groups to include pending operations.
- Added new tools for listing and managing pending operations.
- Implemented OAuth client registration and revocation endpoints.
- Created a UI panel for managing OAuth clients, including registration and revocation.
- Added tests for pending operations tools and OAuth allowlist functionality.
- Implemented a database migration for OAuth client registrations with appropriate policies and constraints.

* feat: Implement OAuth client registration rate limiting and enhance security measures

- Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks.
- Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained.
- Updated error responses to be uniform across different types of redirect URI validation failures.
- Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided.
- Improved handling of high-risk pending operations, requiring explicit confirmation for approvals.
- Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail.
- Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks.
2026-05-18 19:02:42 +02:00
Jakob WennbergandClaude Opus 4.7 c06395f633 feat(mcp): agent-native API sprint — quick wins (items 8/10/38/39/50) (#505)
* feat(mcp): agent-native API sprint — quick wins (items 8/10/38/39/50)

Five Tier-S items from dev_docs/api_ai_architecture/PLAN.md, picked for highest
impact-per-day on a solo budget. ~7.5 engineer-days of work.

Item 38 — gnubok_reverse_journal_entry MCP tool. Wraps the existing
reverseEntry() engine function (lib/bookkeeping/engine.ts) as a staged
high-risk operation. Description distinguishes pure makulering (use this) from
rättelse (use gnubok_correct_entry) per BFL 5 kap 5§ guidance — leaving a real
affärshändelse unbooked is itself a BFL violation, so agents must understand
which storno pattern to apply. New operation_type 'reverse_entry' wired through
PendingOperationType, risk-tiers (high), commit.ts executor, and TOOL_SCOPE_MAP
(bookkeeping:write). Six executor cases + three staging-gate cases cover the
new tool.

Item 39 — period_status threading. New helper resolvePeriodStatusForDate() in
lib/core/bookkeeping/period-service.ts returns { period_id, status, lock_date }
using the same two-layer logic as the v1 REST check (company-wide
bookkeeping_locked_through + fiscal_period flags). Threaded through
stagePendingOperation via a new dateForPeriodCheck option so agents and widgets
can detect locked/closed periods without round-trips. Applied to seven
bookkeeping-touching tools: categorize_transaction, create_transactions,
create_voucher, approve_supplier_invoice, mark_invoice_as_paid, correct_entry,
reverse_journal_entry. Resolution failure is non-fatal — DB triggers stay
authoritative.

Item 50 — gnubok://company/current expansion. Replaces the metadata-only
resource with per-company working memory: active fiscal period status, lock
dates, counts (customers, suppliers, open AR/AP, uncategorized transactions),
voucher series state across open periods, recency signals (last categorization,
last invoice sent, last bank sync), and the next five approaching deadlines.
All queries parallelized via Promise.all; payload stays well under 8 KB.
Mirrors the context.md pattern from Shipper+Claude's agent-native architecture
guidance and prevents the context-starvation anti-pattern.

Item 8 — schema strictness. additionalProperties: false on every one of the 67
inputSchemas in extensions/general/mcp-server/server.ts. New
strict-schemas.test.ts guards against regression on newly authored tools.
CLAUDE.md documents the tool-authoring contract (strict input schemas,
description ≤280 chars, STAGED_OPERATION_SCHEMA + next as the
completion-signal pattern — do NOT introduce a parallel S/H/C/O envelope).
Payload-size ceiling raised from 20K → 25K tokens with a comment pointing at
item 15 (Tool Search + defer_loading) as the long-term answer rather than
relaxing the watchdog further.

Item 10 — prompt cache groundwork. The only Anthropic SDK call site in the
codebase is the invoice-inbox extension's Bedrock-backed extractor; tagged the
~3.5 KB SYSTEM_PROMPT with cache_control: { type: 'ephemeral' } and added
usage logging (cache_read_input_tokens / cache_creation_input_tokens) so the
hit ratio is measurable. The plan's 1h TTL is direct-Anthropic-only;
documented the constraint and the MCP-side determinism contract (tool
definitions must be byte-stable across requests) in the new mcp-server
README.md.

Carry-over: includes a small untracked migration
(20260516060000_journal_entries_source_type_inbox_item) and its pg test guard
that fix a production CHECK-constraint gap for source_type='inbox_item' —
unrelated to the sprint but bundled per request.

Tests: 3615/3615 pass across 252 files. TypeScript build clean.

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

* fix(mcp): address PR #505 review — cross-tenant leaks, company-wide lock, PII

Five reviewer findings on PR #505 addressed:

1. Cross-tenant leak — voucher_sequences (OWASP V8.2.1, SOC 2 CC6.3).
   Resource query filtered by user_id only; switched to company_id since the
   table has both (added in the 2026-03 multi-tenant refactor migration).

2. Cross-tenant leak — deadlines (OWASP V8.2.1, GDPR Art.5(1)(f), ISO A.8.3).
   Same fix; the deadlines table also gained a company_id column in the
   multi-tenant refactor and the RLS policies enforce it. With the company_id
   filter active, the userId parameter is no longer needed in the resource —
   removed from the destructure.

3. Compliance gap — commitReverseEntry and commitCorrectEntry only checked
   fiscal_periods.is_closed, not company_settings.bookkeeping_locked_through.
   Agents could stage a reversal with period_status: locked warning (caught
   by resolvePeriodStatusForDate at staging time), have the user approve,
   and the commit would slip through. Both executors now run
   resolvePeriodStatusForDate at commit time so the gate matches the
   staging-time signal. Pre-existing gap on commitCorrectEntry also fixed.

4. Schema mismatch — period_status was spread into both `preview` and the
   top-level response, but STAGED_OPERATION_SCHEMA only declares it at the
   top level. Removed the preview-nested copy to match the schema and avoid
   ambiguous reads.

5. Tool description — swedish-compliance bot flagged that "pure makulering
   (storno)" conflates two distinct Swedish accounting terms: storno
   preserves the original; makulering voids it entirely. Code does storno;
   description now says so plainly and cites BFL 5 kap.

6. Input hardening — added ^\d{4}-\d{2}-\d{2}$ pattern to reversal_date in
   inputSchema plus a runtime regex check in execute(), so a malformed date
   never reaches the pending_operations payload.

7. GDPR — ai_extraction_usage and the two pre-existing fileName log
   emissions in extract-invoice-fields.ts replaced raw fileName with a
   12-char SHA-256 prefix. Raw invoice file names (e.g.
   "faktura_Sven_Andersson.pdf") can constitute personal data; hashing
   preserves operator correlation without exposing PII to log destinations
   that may lack documented retention controls.

Notes on findings NOT addressed:
- Double-reversal guard (Greptile/swedish-compliance): false positive.
  reverseEntry() flips the original's status to 'reversed' (engine.ts:538)
  and the staging tool already rejects anything not 'posted'. Engine also
  has a CAS guard at lines 541-551.
- Staging vs commit TOCTOU re-validation: pre-flight + DB triggers remain
  authoritative; the window is narrow enough that adding executor-side
  re-checks isn't load-bearing this sprint.
- Runtime Zod validation of args inside execute(): codebase doesn't do
  this for any MCP tool today; cross-cutting refactor deferred.

New test: voucher-executors.test.ts adds a case for the company-wide lock
branch on reverse_entry (verifies the new resolvePeriodStatusForDate gate
fires when bookkeeping_locked_through covers entry_date).

Tests: 3616/3616 pass. TypeScript build clean.

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

* fix(mcp): address second-round PR #505 review — locked_at, reason cap, log

Re-review by compliance-swarm and swedish-accounting-compliance bots after the
first fixes raised three more legitimate findings:

1. Per-period `locked_at` not directly checked from the fetched row
   (swedish-accounting-compliance). Both commitCorrectEntry and
   commitReverseEntry already call resolvePeriodStatusForDate which covers
   locked_at, but a transient DB blip in the resolve helper would silently
   skip that gate. Now reading locked_at directly from the inner-join row and
   checking it alongside is_closed before the resolve helper runs — same
   pattern, two defense-in-depth layers instead of one.

2. `reason` field had no maxLength (OWASP V4.5). Added maxLength: 500 to the
   inputSchema and a runtime length check; an adversarial agent could
   otherwise push an arbitrarily large string into pending_operations.

3. periodStatus resolution failure was silently swallowed (ISO 27001 A.8.15).
   Now logging via console.warn with operationType, companyId,
   dateForPeriodCheck, and error so a systematic outage (missing
   company_settings row, dropped query) is observable in audit logs rather
   than degraded silently.

Findings deliberately NOT addressed (pushed back to the bots):

- gnubok_reverse_journal_entry needs per-operation role check (V8.2.1) and
  narrower 'bookkeeping:reverse' scope (CC6.3) — cross-cutting refactor; no
  MCP tool in gnubok enforces per-operation roles today. Introducing it just
  for one tool would be inconsistent. Will surface as a separate item.
- Reduce line_description in reverse_entry preview (A.8.3, Art.5(1)(c)) —
  the preview is shown to the human approver who needs to see what they're
  approving under BFL 5 kap. Aggregate-only previews would harm the
  approval workflow.
- Audit company-current fields for PII (A.8.12, Art.25(1)) — vat_number,
  org_number, etc. are intentionally part of working memory; agents need
  them to make compliant booking decisions.
- Payload-size ADR reference (A.8.9) — the test comment already cites plan
  item 15 (Tool Search) as the long-term answer.
- mime_type classification label (CC7.2) — theoretical concern;
  ai_extraction_usage events are already operator-only.
- False positive: commitReverseEntry already has the closed-period check
  (V2.3); bot was hallucinating.

Tests: 3616/3616 pass. TypeScript build clean.

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

* fix(mcp,env): structured logger + description trim + env alias support

Two further follow-ups on PR #505:

1. resolvePeriodStatusForDate catch now uses the structured logger
   (createLogger from @/lib/logger) instead of console.warn. Three
   reviewers (compliance-swarm V16.1.1, ISO 27001 A.8.15, SOC 2 CC7.2)
   independently flagged that console.warn bypasses the centralized log
   aggregation pipeline used elsewhere, so systemic outages of the
   period-status resolver were invisible to the SIEM. log.warn now routes
   through the same sink as other server events.

2. Tool description for gnubok_reverse_journal_entry now routes the refund
   case explicitly to gnubok_credit_invoice. The Swedish accounting
   compliance bot flagged that the previous "cancelled credit invoice"
   example was ambiguous — a real credit invoice flow goes through
   gnubok_credit_invoice, not this tool. Description stays under 280 chars.

3. lib/init.ts: REQUIRED_EXTENSION_VARS now models each entry as a list of
   acceptable aliases instead of a single required name. The fallback in
   extensions/general/enable-banking/lib/jwt.ts already accepts the
   _PRODUCTION-suffixed variants (used by Vercel prod) as equivalent to
   the base names, but the env validator at boot didn't, so every cold
   start in prod warned about missing ENABLE_BANKING_APP_ID even though
   ENABLE_BANKING_APP_ID_PRODUCTION was set and the runtime was healthy.
   Each entry now satisfies if ANY listed alias is present; missing
   entries print all acceptable names so operators can pick either form.

Tests: 3616/3616 pass. TypeScript build clean.

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

* fix(mcp): staging tools reject locked_at periods too, not just is_closed

Swedish accounting compliance bot flagged that gnubok_reverse_journal_entry
and gnubok_correct_entry pre-flight checks only rejected closed periods —
locked-but-not-closed periods passed staging and only got rejected at
commit time. The commit-time gate was correct (both executors check
is_closed AND locked_at AND resolvePeriodStatusForDate), but the
staging-time signal was confusing: agent saw staged:true with
period_status:"locked" in the same envelope.

Now the staging pre-flight reads locked_at from the same inner-join and
rejects on either flag, matching the commit-time pattern. The error
message updated to "locked or closed" since both branches reach the same
throw. BFL 5 kap 5§ alignment is unchanged — both paths still block
mutations to locked/closed periods; only the layer at which the rejection
fires changes.

Findings pushed back (response in PR thread, not addressed here):
- companyId/mimeType in log.warn flagged as PII (overreach; tenant IDs
  are operational identifiers, not personal data, and the codebase logs
  them consistently elsewhere).
- HMAC-keyed file_name_hash instead of plain SHA-256 prefix (overreach;
  48 bits already addresses the immediate GDPR Art. 5(1)(f) concern).
- 'title' field in deadlines may contain PII (overreach; would require
  redacting every text field in every read resource).
- RLS regression test for voucher_sequences/deadlines (legitimate but
  pg-test scope; tracked for a follow-up sprint).
- Payload-size ADR record (comment already cites plan item 15).
- company-current data minimisation (already pushed back; agents need
  the fields for compliant booking decisions).
- Error message conflates "locked" and "closed" — minor UX nit not
  worth distinguishing here since the remediation step (unlock / omprövning)
  is the same for the user.
- reversal_date period attribution & voucher series integrity flagged as
  unverifiable from diff — false positives, both already handled by the
  engine (period_id from original, atomic voucher number).

Tests: 3616/3616 pass. TypeScript build clean.

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

* fix(mcp): address Swedish-accounting compliance round 4 — BFL invariant + VAT warning

Three legitimate findings from the swedish-accounting-compliance bot acted on
(out of five total; two pushed back as theoretical/false positive):

1. BFL 5 kap 5§ invariant assertion (finding 1). The engine guarantees that
   reverseEntry() posts the storno to original.fiscal_period_id (engine.ts:492
   — verified by reading the code), but the executor previously took that on
   faith. commitReverseEntry now asserts reversal.fiscal_period_id ===
   original.fiscal_period_id after the call and returns a 500 with an
   explicit "BFL invariant broken" error if the engine ever drifts. New
   executor test covers this. The reversal_date parameter is unchanged —
   it's used as the storno's entry_date (operational date), not for period
   attribution, per BFL practice (entry_date can differ from period_id's
   range for a rättelse made later).

2. resolvePeriodStatusForDate unhandled-rejection path (finding 2). Both
   commitCorrectEntry and commitReverseEntry now wrap the resolve call in
   try/catch, returning a clean Swedish 500 instead of letting the
   dispatcher surface a raw Postgres error message. Matches the
   log-and-degrade pattern already used at staging time in
   stagePendingOperation.

3. VAT-period warning in the reverse preview (finding 4 — swedish-vat).
   When the original entry contains 2610–2670 BAS accounts, the staged
   preview now includes a Swedish warnings[] field telling the approver
   that a storno is legally insufficient if the moms period has been
   filed with Skatteverket — they must use omprövning per ML 2023:200
   instead. Soft warning (not a hard block) since gnubok doesn't track
   per-VAT-period filing status today; the human decides at approval.

Pushed back:

- Finding 3 (TOCTOU between staging and commit on fiscal_period_id):
  posted entries are immutable per the enforce_journal_entry_immutability
  trigger (migration 20240101000017). fiscal_period_id can't change
  between staging and commit. Status change is already caught by the
  status !== 'posted' check.

- Finding 5 (migration 20260516060000 not wrapped in BEGIN/COMMIT):
  Supabase migration tooling runs each migration file in an implicit
  transaction. PostgreSQL DDL is transactional. The DROP/ADD pair is
  atomic in practice. The bot acknowledges this as low severity.

Tests: 3617/3617 pass (one new — BFL invariant assertion). Build clean.

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-05-16 11:42:47 +02:00
Jakob WennbergandClaude Opus 4.7 1163fd3bee fix(invoices): drop UTKAST banner from sent/archived PDFs (#495)
The "UTKAST – inte en giltig faktura" banner in pdf-template.tsx fires
whenever `invoice.status === 'draft'`. All four send-equivalent code
paths render the PDF while the in-memory invoice still reads 'draft',
so the customer's email attachment and the archived underlag are both
stamped as not-a-valid-faktura.

- /api/invoices/[id]/send: status flip happens after email delivery
  (kept that way so a provider failure leaves the row in draft); render
  with `{ ...invoice, status: 'sent' }` instead of mutating order.
- /api/invoices/[id]/mark-sent: DB flip happens before render but the
  in-memory copy is never re-fetched.
- /api/v1/companies/.../invoices/[id]/send: same shape as the internal
  send route; extend the existing `renderableInvoice` override.
- lib/pending-operations/commit.ts (MCP send path): same shape.

The banner condition itself is unchanged — the genuine draft preview
(`/api/invoices/[id]/pdf`) still surfaces the watermark correctly.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:43:14 +02:00
MattssonandClaude Opus 4.7 3fa871c742 Bug/accounting suggestion (#456)
* feat: add bike benefit handling and optional vacation accrual

- Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value.
- Updated schemas to include new benefit types and validation rules.
- Implemented API routes for creating, updating, and deleting employee benefits.
- Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual.
- Added UI components for managing employee benefits, including input for bike benefit specifics.
- Created database migrations for employee benefits and updated salary line items to support new benefit types.

* chore: remove Langfuse env var checks

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

* feat: enhance OAuth callback URL handling and update default scopes for Visma integration

* feat: remove trade_name field and simplify company naming in invoices

* refactor: destructure canWrite from useCanWrite for consistency across components

* feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates

* feat: add missing label for bike benefit in salary line item types

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:26:04 +02:00
Jakob WennbergandClaude Opus 4.7 eb77ad50b5 feat(mcp): add create_voucher + correct_entry MCP tools (#448)
* feat(mcp): add create_voucher + correct_entry MCP tools

The MCP toolset had no way to post a journal entry outside the preset
workflows (categorize_transaction, create_invoice, …). That blocks
legitimate flows the engine already supports — K3 capitalization to BAS
1010, period-end accruals, FX adjustments, prepayments, and rättelseposter
for foreign reverse-charge VAT that landed on 2641 instead of 2614/2645.

create_voucher exposes the existing createJournalEntry() primitive:
arbitrary balanced lines, optional fiscal-period auto-resolution, staged
for human approval. correct_entry exposes correctEntry() (storno + new
corrected entry per BFL 5 kap 5§) so part of a posted verifikation can be
fixed without losing the legs that were right.

Both are HIGH risk in OPERATION_RISK_TIERS — the arbitrary account/amount/
period inputs make them compliance-critical despite being structurally
similar to uncategorize_transaction (medium). Approval flow unchanged; no
auto-commit, regardless of trust level.

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

* fix(mcp): address PR #448 review — voucher tools hardening

Greptile P1 + compliance bot findings, all in one pass.

commitCreateVoucher (commit.ts):
- Hardcode source_type to 'manual' instead of reading from params. A future
  direct-staging path or hand-inserted pending_operations row could
  otherwise inject 'bank_transaction'/'invoice_created'/etc. and corrupt
  the audit-trail origin.
- Re-validate balance defensively before reaching the engine, so a tampered
  params row surfaces a clean Swedish 400 instead of an opaque engine error.

gnubok_create_voucher (server.ts):
- Validate the explicit fiscal_period_id when supplied: confirm it exists,
  is open (is_closed = false), and that entry_date falls within its span.
  Without this, a closed/locked period was only caught at commit-time with
  a generic DB-trigger error.
- Throw at staging when any line targets an account that's missing from
  chart_of_accounts or marked inactive, rather than relying on the
  approver to spot the advisory flag.
- Remove source_type from the staged params blob entirely — the executor
  ignores it anyway, no point letting it travel through.
- Add a comment that the staging-time period-lock check is advisory and
  the executor is the authoritative guard, so future cleanup doesn't
  remove either as 'redundant'.

Descriptions:
- gnubok_correct_entry now explicitly notes that the storno + corrected
  entries land in the original period (defends against compliance bot's
  speculative "different period" concern recurring on future reviews).
- Both tools' tax_code field gets a note that the BAS account number
  drives momsdeklaration ruta mapping, not tax_code — guards against an
  LLM treating tax_code as the VAT-routing dial.

commitCorrectEntry (commit.ts):
- Add a comment pointing at storno-service.ts:99,102,195,198 to make the
  "uses original period and date" invariant explicit in this file.

Tests:
- +2 voucher-executors cases: source_type tampering is ignored, unbalanced
  params return 400.
- +10 new voucher-tools tests (MCP layer): unbalanced, closed explicit
  period, missing explicit period, entry_date outside period, unknown
  account, inactive account, happy path + correct_entry registration +
  unbalanced replacement.

720 tests pass in the impacted suites; full suite 2998/2998.

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-05-12 17:16:39 +02:00
MattssonandClaude Opus 4.7 5725c25bf1 Logs/improved logging (#398)
* feat(mcp): add create_transactions tool with /pending approval gate

New MCP tool gnubok_create_transactions stages 1–10 transactions per call
as pending_operations of type create_transaction (risk: medium). Each item
becomes its own card on /pending; on confirm, the executor inserts the row
into transactions with import_source='mcp' so MCP-staged ingestion is
distinguishable from PSD2 sync. Designed for skill workflows that pull
external data (e.g., Airtable) and want the user to gate the writes.

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

* fix(bas): strip concatenated group headers from corrupted account names

A chart-data import bug had glued the next group's header onto the last
account in each preceding group across all eight bas-data class files
(e.g. account 2670 read "Utgående moms på försäljning inom EU, OSS 27
PERSONALENS SKATTER, AVGIFTER OCH LÖNEAVDRAG"). The corrupted names
surface in transaction dropdowns, ledgers, SIE exports and årsredovisning,
and risk VAT miscategorization on the OSS (2670) and blandad-verksamhet
(6999) accounts specifically.

- Cleans 69 account_name and 64 description fields across class-1..8 files
- Adds a regression test asserting no name contains a concatenated header
- Ships an idempotent safety-net migration that updates already-seeded
  chart_of_accounts rows, gated on the corrupted string so user
  customizations are preserved

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

* feat(errors): add structured error codes and handling for various operations

- Introduced a new structured error registry in `structured-errors.ts` to standardize error handling across the application.
- Added Swedish and English messages for various error scenarios, including validation, authorization, and bookkeeping errors.
- Implemented a client-side error toast in `use-error-toast.ts` to display user-friendly error messages with remediation hints.
- Created a wrapper for recording operation outcomes in `record-operation.ts`, enhancing audit capabilities for operations.
- Developed a provider call wrapper in `with-provider-call.ts` to handle external HTTP calls with structured logging and error mapping.
- Added a new SQL migration to extend the processing history with new event types and aggregate types for better operational telemetry.

* Refactor supplier API routes to use context-based logging and error handling

- Replaced direct Supabase client usage in GET and POST routes with context-based approach using `withRouteContext`.
- Enhanced error handling to provide structured error responses for supplier creation and listing.
- Updated logging to include request IDs for better traceability.
- Introduced new error codes for supplier-related operations.
- Refactored tax deadlines cron job to utilize context and improved error handling.
- Updated ESLint configuration to enforce logging practices across API and lib directories.
- Enhanced arcim migration extension with structured error handling and logging.
- Added classification for provider errors to improve user-facing error messages.
- Introduced request ID in extension context for better log correlation.

* fix(route-context): update DynamicParams type for improved type safety in route handlers

* feat(transactions): add 'create_transaction' operation to PendingOperationType

* fix(route): ensure companyId is non-nullable in loadAndDeriveAbsence function

* fix(route-context): ensure companyId is always non-null by short-circuiting with COMPANY_CONTEXT_MISSING

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:12:02 +02:00
Jakob WennbergandClaude Opus 4.7 94f15b9c6c feat(invoice-inbox): pin documents to bank transactions + MCP tools (#397)
* feat(invoice-inbox): pin documents to bank transactions + MCP tools

Adds a first-class flow for attaching unmatched inbox documents to bank
transactions, separate from the existing supplier-invoice convert path:

- new transactions.document_id FK → document_attachments (ON DELETE SET NULL)
- POST/DELETE /api/transactions/[id]/attach-document
- categorize route propagates the link to journal_entry_id on commit
- three new MCP tools: gnubok_list_unmatched_documents,
  gnubok_get_document_content (5-min signed URL),
  gnubok_attach_document_to_transaction (staged via pending_operations)
- InvoiceInboxWorkspace gains a "Koppla till transaktion" picker dialog
  ranked by amount-match, plus a "Bilaga" badge in SwipeCategorizationView
- regex extraction unchanged; supplier-invoice convert flow unchanged

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

* fix(invoice-inbox): address PR #397 review findings

- categorize: destructure { error } from the document-link update so
  Supabase-level failures are logged instead of silently dropped (BFL 5 kap 6 §
  receipt-on-verifikation contract).
- list_unmatched_documents: emit next_cursor whenever the inbox query may have
  more rows, not only when the post-filter slice was full; switch to composite
  (created_at, id) cursor to avoid same-second collisions.
- DELETE /attach-document: return 404 when the tx isn't in the company; return
  409 when the linked document already has journal_entry_id set
  (räkenskapsinformation immutability).
- risk tier: attach_document_to_transaction medium (was low) — link becomes
  part of verifikation underlag once categorize propagates it.
- pg-real test: stop reusing $2 across uuid + text-concat contexts (Postgres
  couldn't deduce the parameter type).

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

* fix(migrations): break duplicate version 20260505120000 (Supabase Preview)

Two migrations on main share filename version 20260505120000:
  - 20260505120000_api_keys_refresh_token.sql (PR #392)
  - 20260505120000_drop_agent_auto_commit.sql (PR #394)

The schema_migrations primary key is (version), so any fresh DB doing
`supabase db push` over both files conflicts on the second insert. This is
why every PR with a migration since #394 has had Supabase Preview either
fail or skip.

Renaming _drop_agent_auto_commit to 20260505190027 — that matches the
timestamp recorded in prod schema_migrations from when apply_migration was
called for it, so future `db push` against prod sees the file as already-
applied (no re-run). The migration body is fully idempotent (IF EXISTS on
every drop) so a re-run would be a no-op anyway.

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

* fix(invoice-inbox): address PR #397 round-2 compliance review

Two BFL gaps the compliance bot flagged on the round-1 fix commit:

1. commitAttachDocumentToTransaction silently broke verifikation→underlag if
   the transaction was categorized between staging and approval. Now reads
   transactions.journal_entry_id at commit time and, if non-null, also writes
   document_attachments.journal_entry_id in the same commit so BFL 5 kap 6 §
   is satisfied regardless of order.

2. Application-layer DELETE check was racy (SELECT then UPDATE) and the FK
   ON DELETE SET NULL path could null transactions.document_id even for a
   document that is räkenskapsinformation. Added a BEFORE UPDATE OF
   document_id trigger on transactions that raises check_violation when the
   previously-attached document has document_attachments.journal_entry_id
   set. The app-layer guard stays for friendly Swedish messaging; the
   trigger is the DB-level safety net.

pg-real test extended to cover both directions of the trigger (block detach
+ block swap) and the happy-path detach when there's no JE link yet.

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

* fix(invoice-inbox): address PR #397 round-3 compliance review

Four findings from the round-2 update of the compliance bot. The first three
are genuine compliance gaps; the fourth (preview metadata distinguishing pre-
vs post-categorization overwrites) is a UX nicety left for follow-up.

1. transactions.document_id FK switched from ON DELETE SET NULL to RESTRICT
   (migration 20260506100000). Removes the "trigger ordering" concern: a doc
   that's pinned to any tx now cannot be deleted at all without explicit
   detach first. Belt-and-braces with block_document_deletion.

2. commitAttachDocumentToTransaction now does:
   - pre-check that mirrors the DELETE route's 409 when the existing pinned
     doc is räkenskapsinformation, so the same Swedish message is returned
     in both paths;
   - UPDATE…RETURNING journal_entry_id so the propagation decision uses the
     post-update state, closing the read-then-write race with concurrent
     categorize. Either ordering of attach-then-categorize or
     categorize-then-attach now lands at the same correct final state.

3. Both DELETE /attach-document and the MCP commit path catch the trigger's
   check_violation (SQLSTATE 23514) and translate to 409 with the Swedish
   underlag message. The trigger remains the DB-level safety net; the app
   layer is responsible only for friendly UX.

pg-real test rewritten for ON DELETE RESTRICT (blocks deletion of pinned doc;
detach-then-delete works). Unit coverage added for commitAttach: 404, two
distinct 409 paths (pre-check + trigger-translation), happy-path
uncategorized, and propagation when tx was categorized between staging and
commit.

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

* fix(invoice-inbox): address PR #397 round-4 compliance review

Three of five round-3 findings actioned:

1. commitAttachDocumentToTransaction: surface propagation failure rather
   than logging-and-continuing. If document_attachments.journal_entry_id
   can't be set after the transaction has been categorized, the op fails
   (status 500) with a Swedish message instructing retry. Retry is
   idempotent — same document_id on the tx, same propagate target.

2. Replace check_violation (23514) matching with a stable
   "BFL_DOCUMENT_IMMUTABILITY:" message prefix. The trigger now uses default
   P0001 + tagged message; both the route handler and the executor match on
   the prefix instead of the generic SQLSTATE. Future unrelated CHECK
   constraints on transactions can no longer accidentally surface as the
   räkenskapsinformation message.

3. gnubok_list_unmatched_documents now returns invoice currency alongside
   amount so an agent can FX-normalise before comparing to
   transactions.amount. Description updated to make the requirement
   explicit. Mirrored in the UI: AttachToTransactionDialog ranks
   same-currency rows by amount distance and pushes cross-currency rows to
   the bottom of the list.

Skipped:
- Two-migration window for FK action change is acknowledged as resolved by
  the bot; deploy-atomicity is an ops concern, not code.
- Period-lock check in attach/detach: realistic compliance concern is
  already covered by the existing immutability trigger (post-categorize) and
  by the engine's period-lock enforcement (categorize itself). A dedicated
  period check on pre-categorize attach would only guard against pinning a
  doc to a tx in a closed period — defensible defense-in-depth, but no
  active BFL violation. Left for a follow-up.

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

* fix(invoice-inbox): address PR #397 round-5 compliance review

Three of four findings actioned. The fourth (block_document_deletion
verification) is already covered by 20240101000017_enforcement_triggers.sql
which raises when document_attachments.journal_entry_id IS NOT NULL on a
posted/reversed entry — confirmed via grep, no code change needed.

1. categorize/route.ts: propagation no longer fires-and-forgets. If
   document_attachments.journal_entry_id can't be set after the JE has been
   committed, the response now carries a document_link_warning field with a
   Swedish retry message. The JE is already committed so we can't roll back,
   but the client can no longer mistake a partial attach for a clean
   categorize.

2. Rättelse audit trail (BFL 5 kap 5 §): both the REST POST handler and the
   MCP commit executor now append a TransactionDocumentReplaced event to
   processing_history whenever a non-null document_id is overwritten, with
   previous_document_id and new_document_id in the payload. Best-effort —
   logging failure must not roll back the (compliant) attach. The previous
   doc id is also returned in the response so callers see what was displaced.

3. MCP staging preview now exposes the existing doc's identity
   (existing_document_id, existing_document_file_name) plus an explicit
   existing_document_is_rakenskapsinformation flag, so a human approver sees
   "replaces X.pdf with Y.pdf" rather than just a will_overwrite_existing
   boolean. Mirrors BFL 5 kap 5 § informed-rättelse intent.

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

* fix(invoice-inbox): close trigger SELECT race (PR #397 round-6)

The enforce_transactions_document_immutability trigger SELECTed
document_attachments.journal_entry_id without a row lock. A concurrent
UPDATE setting journal_entry_id on that row could commit between the
trigger's SELECT and its RAISE, letting a detach slip through against a
document that just became räkenskapsinformation.

Add FOR SHARE to the SELECT inside the trigger. A concurrent journal_entry_id
write blocks on our share lock until our transaction commits, so either we
observe the propagation and raise, or we run first and the propagation
observes our committed detach (which is fine because journal_entry_id was
still null at that point).

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

* fix(invoice-inbox): bidirectional immutability + richer staging preview (PR #397 round-7)

Two of six round-6 findings actioned. The other four are recurring
architectural recommendations (atomic audit-log writes, background
reconciliation jobs, migration consolidation, anti-join via materialized
view) that are properly scoped as follow-up work.

1. document_attachments side of the immutability link (BFL 5 kap 6 § works
   in both directions). New trigger enforce_document_journal_entry_immutability
   blocks UPDATE OF journal_entry_id when going from non-null to NULL or to a
   different uuid. The original null→uuid path (initial propagation in the
   categorize / commitAttach flows) still works. Migration
   20260506130000.

2. gnubok_attach_document_to_transaction staging preview now joins on
   invoice_inbox_items.extracted_data and surfaces vendor/amount/currency/
   invoice_date alongside the existing doc filename/mime metadata. Gives the
   human approver the same hints the agent saw before choosing the
   attachment.

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-05-06 10:24:57 +02:00
Jakob WennbergandClaude Opus 4.7 b05f0e59b9 fix(mcp): correct fiscal_periods column ref + remove agent auto-commit (#394)
* fix(mcp): correct fiscal_periods column ref + remove agent auto-commit

The MCP `gnubok_list_fiscal_periods` tool selected a non-existent
`fiscal_periods.status` column, causing `column fiscal_periods.status
does not exist` errors when agents called it. Fixed by selecting the
real columns (`is_closed`, `locked_at`, `closed_at`,
`opening_balances_set`) and deriving `status` in code.

Also removes the agent auto-commit feature (settings card, gating logic,
DB columns, tests). In its current shape only `create_customer` was
auto-commitable, so the toggle changed nothing meaningful in practice
while implying a level of agent autonomy that wasn't actually granted.
The risk-tier infrastructure on `pending_operations` (actor model,
risk_level) is kept since it's still used by the /pending UI filters.

Migration `20260505120000_drop_agent_auto_commit.sql` drops:
  - pending_operations.auto_commit_eligible
  - pending_operations.auto_committed_at
  - company_settings.agent_auto_commit_enabled
  - company_settings.agent_auto_commit_max_amount

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

* fix(mcp): explicitly pick fields in gnubok_list_fiscal_periods response

Greptile flagged that `...p` spreads raw DB columns (`is_closed`,
`locked_at`, `closed_at`) into the tool response alongside the
derived `status`. Drop the spread for an explicit field list so the
tool contract is the derived status only — agents don't need to
reason about raw columns, and future SELECT additions won't silently
leak.

Also drops `closed_at` from the SELECT since it wasn't read.

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-05-05 21:16:01 +02:00
Mattsson bb855d2ddc Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports

* feat(auth): enhance API key scopes and add bookkeeping write scope

- Updated transaction write scope description to include additional tools.
- Enhanced reports read scope description to reflect new functionality.
- Introduced bookkeeping write scope with relevant description.
- Updated SCOPE_GROUPS to include bookkeeping domain.
- Modified TOOL_SCOPE_MAP to include new bookkeeping operations.
- Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution.

feat(tests): add unit tests for MCP resource registry

- Created tests for data resources to ensure all required fields are present.
- Added tests for resource query parsing and retrieval.

feat(resources): implement MCP resources for company and accounting data

- Added capabilities resource to expose API key capabilities based on granted scopes.
- Implemented chart of accounts resource to retrieve active BAS chart.
- Created company current resource to fetch active company details.
- Developed active fiscal period resource to check posting eligibility.
- Implemented recent activity resource to fetch latest journal entries, invoices, and transactions.
- Added VAT treatments resource to provide available VAT rates per customer type.

feat(pending-operations): introduce risk tiers for operations

- Added risk level classification for pending operations to determine auto-commit eligibility.
- Implemented functions to classify operation risk levels and identify high-risk operations.

feat(migrations): add actor model and risk tier to pending operations

- Updated pending_operations table to include actor type and risk level columns.
- Enhanced audit_log to mirror actor information for compliance.
- Modified validate_and_increment_api_key function to return actor details.
- Expanded operation types in pending_operations to include new high-risk operations.

* feat: add auto-commit functionality for low-risk pending operations

- Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings.
- Created commitPendingOperation function to handle execution of pending operations with consistent status updates.
- Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds.
- Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality.
- Added SQL migration to update the database schema for new auto-commit settings.

* feat(idempotency): implement idempotency key handling for safe retries and cleanup

* feat: expand API key scopes and pending operations for bookkeeping

- Added 'suppliers:write' scope to API key scopes for supplier invoice management.
- Updated SCOPE_GROUPS to include the new 'suppliers:write' scope.
- Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice.
- Implemented corresponding commit functions for the new operations in the pending operations module.
- Enhanced PendingOperation type to include actor model and risk level attributes.
- Added tests for new functionality, ensuring proper behavior and constraints in the database.

* feat: implement unlockPeriod functionality and related tests

* feat: add agent auto-commit settings and related functionality

* feat: add attention resource with comprehensive summary of outstanding tasks

* feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes
2026-05-04 11:12:29 +02:00