db843a7a5b0f7ea7030fef604aee31f9d0397b5a
563 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db843a7a5b |
fix(entitlements): gate paid MCP tools (send_invoice/agi_submit/vat_declaration_submit) server-side (#846)
The HTTP routes call requireCapability at every paid chokepoint, but the MCP/agent path bypassed the paywall entirely: the three external-service tools stage operations whose commit calls the email / Skatteverket services directly, with no capability check. After the 2026-07-07 trial cutover a trial-connected non-payer using the gnubok MCP connector could still send invoice emails and file AGI/VAT. Close the gap with two layers, mirroring the existing TOOL_SCOPE_MAP gate: - Dispatch gate (mcp-server/server.ts): MCP_TOOL_CAPABILITY_MAP, checked right after the scope check, blocks a non-entitled company before any pending op is staged. Emits errorKind='capability_denied' telemetry. - Commit-time gate (commitPendingOperation): PAID_OPERATION_CAPABILITY_MAP, checked before the atomic claim. The real external-service chokepoint — applies to the MCP approve tool AND the UI approval path, and closes the trial-connected-token window (the grant has expired by commit time). A blocked op stays 'pending', so it is re-approvable once the company subscribes. Adds a transport-free capabilityBlockedError() helper (shared bilingual copy) and locks both maps with tests (maps, dispatch gate, commit gate). Only the three write/submit tools are gated; SKV read/local tools stay free per the statutory carve-out. No DB/migration change; self-hosted stays all-on. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5df6199bd1 |
fix(invoices): remaining_amount + invoice.paid on agent mark-paid path (#825) (#845)
* fix(invoices): book remaining_amount + emit invoice.paid on agent mark-paid path (#825) The agent/MCP commit path commitMarkInvoicePaid flipped status to 'paid' and set paid_amount = total but never wrote remaining_amount (left at the original total) and never emitted invoice.paid — so partial state and webhooks diverged from the dashboard and v1 mark-paid routes. Route the agent path through the shared planInvoicePayment helper (the source of truth introduced in #841): compute paid/remaining/status with the overpayment guard BEFORE booking the JE (so a rejected payment never burns a voucher number), persist remaining_amount + the partially_paid transition, and best-effort emit invoice.paid for webhook parity. Adds lib/pending-operations/__tests__/mark-invoice-paid.test.ts covering the state + event behaviour of this path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): address review feedback on agent mark-paid path (#825) - CAS-guard 409 message now reflects the expanded payable states: the UPDATE filter accepts partially_paid (reachable via a concurrent settle race), not just sent/overdue. - Derive the settle amount from total − paid_amount when remaining_amount is null (legacy rows) instead of falling back to the full total, so a prior partial payment is not double-counted into a false overpayment rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): derive remaining from total − paid_amount on all mark-paid surfaces (#825) The dashboard and v1 mark-paid routes defaulted the settle amount to invoice.total when remaining_amount was null, which over-settles a legacy invoice that has a prior partial payment recorded in paid_amount (false overpayment / AR over-credit). Align both with the agent path (commit.ts): remaining_amount ?? total − paid_amount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
46039f14f4 |
fix(reports): valid two-file NE-bilaga SRU submission (#318, #319) (#844)
* fix(reports): generate valid two-file NE-bilaga SRU submission (#318, #319) The NE-bilaga "Ladda ner SRU" export produced a file Skatteverket rejects: it was served as UTF-8 text/plain (å/ä/ö mojibake, #319) and was structurally invalid — a single blob with #PRODUKT KONTROLLUPPGIFTER (the KU code), no INFO.SRU/BLANKETTER.SRU split, a #SKAPAT typo, no #FIL_SLUT, and suspect field codes 7310–7350 (#318). Rewrite the generator to mirror the working INK2 generator: a two-file INFO.SRU + BLANKETTER.SRU submission, ISO 8859-1 encoded and zipped, with #PRODUKT SRU, #DATABESKRIVNING_*/#MEDIELEV_*, #BLANKETT NE-<år>P<x>, #IDENTITET <personnummer12> <date> <time>, and #FIL_SLUT. Field codes use the authoritative BAS NE_EJ_K1 coupling table (R1→7400 … R10→7505, R11→7440; period dates 7011/7012). Enskild-firma identity is the owner's 12-digit personnummer (birth-century prefix, not INK2's juridisk-person "16"). - Extract the shared ISO-8859-1 encoder to lib/reports/sru-encoding.ts (was inline in the INK2 route). - Extend the NE engine/types to carry address/postort/email for INFO.SRU. - Frontend: NE SRU download uses the INK2 blob pattern; fix a pre-existing param bug in EfDeclarationSection (fiscal_period_id → period_id, +format=sru). - Add generator tests (structure, BAS field codes, zero-omission, ISO-8859-1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): address review feedback on NE-bilaga SRU generator (#318) - getZipFilename uses the income year (fiscal year END) so the filename matches the blankett type/identity for broken fiscal years. - Refuse to generate a submission when the personnummer is missing/invalid (compute + validate the 12-digit identity once in generateNESRUSubmission and throw) instead of silently emitting a placeholder #IDENTITET that Skatteverket would reject after upload. - validateBlanketterSru now asserts the mandatory räkenskapsår date fields (#UPPGIFT 7011/7012) — their absence is a level-2 rejection. - 10-digit personnummer century is inferred from adult age (≥18, <110) at the income year, fixing the e.g. 1924-born/yy=24 edge that mapped to 2024. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f8504f3bd0 |
fix: audit batch — pagination truncation, MFA/dead-code cleanup, mark-paid fail-closed (#841)
* fix(reports): paginate 8 more report/ledger queries (1000-row truncation) Raw .select() without fetchAllRows() silently caps at PostgREST's 1000-row limit, producing wrong statutory output for high-volume companies. Following #806 (trial-balance/VAT), wrap the remaining offenders in fetchAllRows + a stable .order('id') + dedupeBy: - ink2-engine / ne-engine: INK2 & NE-bilaga tax declarations under-counted - ar-reconciliation (1510/1513), supplier-reconciliation (2440): phantom "Ej avstämd" gaps - full-archive-export: 7-year DR archive (added a unique total order so rows are not silently skipped/duplicated across pages) - avgifter-basis, currency-revaluation, vat-declaration Adds a regression guard test asserting >1000 ledger lines are summed, not truncated at 1000. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): close extension-dispatcher MFA gap, scope /api/events to API key, sweep dead code Security/correctness: - ext/[...path] dispatcher now uses requireAuth() instead of inline supabase.auth.getUser(), enforcing MFA (AAL2) on hosted across the whole enabled-extension surface (banking sync, document upload/booking, supplier invoices, migration). Ratchets antipatterns-baseline raw-route-auth 168->165. - /api/events now filters by the API key's bound company_id instead of the user's active company (was a cross-company read with a scoped key). - enable-banking OAuth callback calls ensureInitialized() at module load so the PSD2 consent audit event (ASVS V16 / GDPR Art.30) isn't dropped on a cold-start instance. Dead-code sweep (all confirmed zero importers): - delete lib/tax/calculator.ts, lib/salary/engangsskatt.ts (+test), lib/email/resend.ts, lib/salary/salary-transaction-matcher.ts, lib/webhooks/diff.ts, lib/salary/effective-values.ts, lib/bookkeeping/template-prompt.ts - trim unused lib/vat/eu-countries.ts helpers (keep EU_COUNTRIES) - remove dead getAutomaticStatus() and the abandoned Activepieces CSP entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): fail closed when a payment journal entry doesn't post Three mark-paid paths (legacy route, v1 API, agent commit) diverged on the "mark paid but the JE failed" case — two would flip the invoice to paid (or leave an orphaned posted voucher) with no booking, silently diverging the GL from the AR/AP sub-ledger. Unify on fail-closed: - legacy + v1 + agent commitMarkInvoicePaid: never mark paid without a posted voucher; on a null/failed JE return INVOICE_PAID_BOOK_FAILED before any state mutation (v1 mirrors the match-invoice strict mode). - agent path: add the .in('status',[...]).select('id') CAS guard and cancel the orphaned voucher (cancelOrphanedPaymentEntry) on a lost race or update error, matching the web route. - legacy route: cancel the orphan on a non-race update error too (was only handled on the race branch). - supplier mark-paid: stop swallowing a failed supplier_invoice_payments insert — that row drives the reversal amount in payment-sync; roll back the status flip and cancel the voucher instead. - pending-ops orchestrator: error-check the terminal 'committed' write so an op stranded in 'committing' (the expire sweep only targets 'pending') is at least logged loudly. Adds a guard test for the legacy fail-closed path. Full unit suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): unblock core build + address compliance-review findings - avgifter-basis.ts: fix the core-build TypeScript error — PostgREST's type-level select parser models the salary_run embed as an array, which wasn't assignable to the object-typed generic. Type it `unknown` (rows are read via an explicit cast), making it robust across postgrest-js versions. - /api/events: add a non-null companyId guard before the event_log query (defense-in-depth for the API-key-bound scope) — addresses ASVS V8.2.1 / ISO A.5.15. - supplier mark-paid: add a CAS guard (.eq('status', newStatus)) to the payment-insert-failure rollback so a concurrent settlement can't be clobbered — addresses ASVS V2.3. - dispatcher: add an AAL2 regression test asserting a non-MFA session is rejected (403) and the extension handler never runs — addresses the GDPR Art.32 review ask for the single extension chokepoint. Verified deletions are safe: effective-values.ts was a dead duplicate — the live AGI/payslip path inlines the same `?? override` coalescing (generate-declaration.ts), so AGI correctness is unaffected. next build: exit 0. Full unit suite: 6147 passing. ESLint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b800dcd403 |
style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup Multi-agent scan of all 404 UI files against the locked design system, then 141 verified surgical fixes across 109 files (net -32 lines): - Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale bounce, hover:shadow on list items, transition-all -> transition-colors. - Drop font-medium from single-weight Hedvig display headings/numerals. - Replace raw rainbow Tailwind status colors with Badge variants / brand tokens / neutral surfaces (achromatic chrome, semantic colors stay data-only). - Route raw dates through formatDate(), hand-rolled currency through formatCurrency(), add tabular-nums to financial figures; text-gray-* -> text-foreground tokens. - Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale. - Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total, collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake), correct mismatch-password toast, correct supplier currency-field label. - Remove PII-leaking debug console.log on register, stray console.logs. Verified: tsc clean on all changed files, eslint clean, production build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): sanitize residual error logs in register flow Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the remaining console.error calls in the register flow passed raw error objects, which Supabase may populate with PII (email) in nested fields. Log only sanitized message strings instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8072f6423 |
feat: realtime updates for dashboard transactions (rebased reimplementation of #757) (#833)
* Add realtime transaction subscriptions to dashboard and transactions page Introduce a shared browser Supabase hook so client components can create a stable realtime-capable client once and reuse it across multiple dashboard surfaces. Dashboard navigation now keeps the uncategorized transactions badge in sync through a company-scoped postgres_changes subscription on public.transactions, and the transactions page now subscribes to the same table so it can refresh its list and uncategorized count live without a full page reload. The transactions page keeps the initial server-driven load path intact, but once mounted it listens for inserts, updates, and deletes on the active company's transactions. When a change arrives it refetches the list and total uncategorized count using the same RLS-scoped filters that already power the page, then hydrates the visible rows in the same way as the initial load. The refresh path is intentionally coalesced so bursts of realtime events do not trigger overlapping database reads. The dashboard nav and the transactions page both use the new shared hook instead of creating browser clients ad hoc. This keeps the realtime client setup consistent, avoids duplicate client construction logic, and makes it straightforward to add more realtime dashboard consumers later without re-implementing the same Supabase plumbing. A Supabase migration is included to add public.transactions to the supabase_realtime publication. Without that publication entry the browser subscription would be correct but silent, so the migration is required for hosted environments as well as local resets. Also included in this commit is the current package-lock.json drift present in the staged set. Signed-off-by: Esaias Westberg <esaias@westbergs.se> * fix(migration): retimestamp transactions realtime publication to clear collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql on main. Renamed to a unique timestamp after main's latest. SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dashboard): fix indentation in collapsed-nav badge block --------- Signed-off-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f8aef335c9 |
fix(ci): persist-credentials: false on the privileged compliance-review checkout (#831)
Stage 2 holds a write token + AWS secrets and never needs git push creds; don't leave the token persisted in .git/config for the steps that process the untrusted diff artifact. Closes the Superagent P2 follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a68123bbe8 |
fix(ci): fork-safe compliance review (two-stage workflow_run) — safe alternative to #829 (#830)
* fix(ci): fork-safe compliance review via two-stage workflow_run Replaces the pull_request_target approach (which would run untrusted fork code with the AWS Bedrock secrets in env) with the GitHub-recommended split: - swedish-compliance-diff.yml (pull_request, no secrets, read-only token): computes the diff and uploads it as an artifact. Never runs project code. - swedish-compliance-review.yml (workflow_run, has secrets + write token): checks out ONLY the base repo (trusted script + skills), downloads the diff artifact, feeds it to the model as DATA, and posts the comment. Never checks out or executes fork PR code. scripts/swedish-compliance-review.mjs reads the diff from DIFF_FILE/FILES_FILE when set, with a fallback to git diff for same-repo runs. Safe alternative to #829. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): pin workflow actions to commit SHAs (Superagent P1) Pin actions/checkout, setup-node, upload-artifact, download-artifact and the peter-evans comment actions to immutable 40-char SHAs with version comments, closing the two Superagent supply-chain findings. Matters most here since the review stage holds AWS Bedrock secrets + a write token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): full base fetch in compliance-diff so merge-base works when branch is behind The --depth=1 base fetch left git merge-base with no reachable common ancestor once main advanced past the PR branch, failing the prepare job under bash -e. checkout already uses fetch-depth: 0, so a full base fetch makes merge-base reliable regardless of how far base has moved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): harden compliance review per security audit Stage 1 (swedish-compliance-diff.yml): pass github.base_ref + PR number via env instead of interpolating ${{ }} into the run: shell (template-injection antipattern); add set -euo pipefail; printf over echo. Stage 2 (swedish-compliance-review.yml): pin @anthropic-ai/bedrock-sdk@0.31.0 and add --ignore-scripts — the privileged job (write token) must not run a floating @latest or dependency lifecycle scripts. set -euo pipefail on the PR-number guard. Script: frame the untrusted diff/files with a per-run unguessable random sentinel (not a code fence a hostile diff could close) plus an explicit 'treat as data, ignore embedded instructions' system-prompt guard and output constraints (no images/@-mentions/links/HTML). Legacy getDiff now uses execFileSync (argv array, no shell). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1cd8863958 |
fix(transactions): resolve bank account from cash_account_id in booking dialog (#769)
* feat(transactions): expose cash_account_id in list API response Add cash_account_id to the transactions list API select so that components can resolve the bank account from the transaction instead of hardcoding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * feat(cash-accounts): extract resolveAccount to shared utility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * refactor(transactions): use shared resolveAccount in MatchVoucherDialog Replace the local resolveAccount function with the shared utility from lib/cash-accounts/resolve-account, reducing code duplication and improving maintainability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): resolve bank account from cash_account_id in booking dialog Replaces the hardcoded '1930' bank leg in TransactionBookingDialog with the actual ledger_account of the transaction's cash account. Companies with multiple bank accounts (e.g. 1930 + 1940) now get the correct account pre-filled in both the blank and template-based booking flows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): cancel stale cash-account fetch on dialog re-open Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): prevent form remount discarding edits during bank account fetch Hold JournalEntryForm render until the /api/cash-accounts fetch resolves by changing bankAccount state to string | null (null = pending). This prevents the form from mounting with key '…-1930', then immediately remounting with the correct account key and losing any user edits made in the sub-100ms window. Also adds r.ok guard before parsing and sets '1930' as explicit catch fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): cancel stale cash-account fetch in MatchVoucherDialog Pass a signal object into loadCandidates and return a cleanup from the useEffect so a stale in-flight fetch (from a previous transaction) cannot call setAccountNumber/setAccountFallback/setGlLines/setSelected after the dialog re-opens for a different transaction. Also adds r.ok check before parsing /api/cash-accounts response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
5b4cefe8ab |
feat(api): v1 endpoints to stamp invoice inbox items as consumed (#767)
* feat(api): v1 endpoints to stamp invoice inbox items as consumed
Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link
(best-effort stamp on the originating invoice_inbox_items row) and a new dedicated
POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping
independently of the document link — both use documents:write scope and require
Idempotency-Key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
* fix(api): wrap stamp response in dataEnvelope and register route in load-routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
---------
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
7cd1a7388f |
fix(customers): add columns the system-migration importer needs (address_line2, default_payment_terms, vat_number_validated) (#780)
* fix(customers): add address_line2 + default_payment_terms columns The customers table lacked address_line2 and default_payment_terms, yet the suppliers table has both and three code paths write/read them on customers: the arcim-migration importer, the built-in customer CSV import, and the customer CSV export. Inserts failed with PostgREST "Could not find the 'address_line2' column of 'customers' in the schema cache", so customer imports landed 0 rows while suppliers imported fine. Add the two columns to match suppliers (text, integer default 30). Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): add missing vat_number_validated boolean column The customers table was missing the boolean vat_number_validated flag that types/index.ts and ~40 code sites depend on (getVatRules/getAvailableVatRates, the customers-list validated badge, the v1 customers API select), yet no migration ever created it — only the vat_number_validated_at timestamp exists. It went unnoticed because normal customer creation writes the flag only when a VIES check runs, and reads degrade to undefined when the column is absent. The arcim-migration importer writes it unconditionally (false), so customer and sales-invoice-stub inserts failed with PostgREST "Could not find the 'vat_number_validated' column of 'customers' in the schema cache". Same class of gap as the address_line2/default_payment_terms columns added in the previous commit on this branch. Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): retimestamp migrations to clear version collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql (merged to main via #797) and with #757. Renamed to unique timestamps ordered after main's latest (20260629160000). SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
837f354d81 |
fix(sie): add ?encoding=cp437 for legacy bookkeeping software (#810)
* fix(sie): add ?encoding=cp437 option for legacy bookkeeping software SIE spec mandates CP437 (#FORMAT PC8) but accounted generates UTF-8. Most modern cloud tools (Fortnox, Bokio) accept UTF-8 fine, so UTF-8 remains the default. Pass ?encoding=cp437 to get a properly encoded CP437 binary with #FORMAT PC8 in the header, required by desktop software such as Visma Administration and BL Administration. Removes the spurious #FORMAT PC8 tag from the default UTF-8 output since declaring CP437 while serving UTF-8 caused mojibake on import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(sie): wrap Uint8Array in Buffer.from so NextResponse accepts it Uint8Array is not directly assignable to BodyInit in the Next.js NextResponse constructor — wrapping with Buffer.from() satisfies the type without changing the byte content. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
37ee125b9b |
fix(mcp): paginate trial-balance and VAT aggregations (1000-row truncation) (#806)
* fix(mcp): paginate trial-balance and VAT aggregations (1000-row truncation) The gnubok_get_trial_balance tool and computeVatReport each ran an unbounded journal_entry_lines aggregation. PostgREST caps an unpaginated .select() at 1000 rows, so any period with >1000 entry lines silently truncated: wrong per-account sums and a false "not balanced" trial balance, and an under-reported momsdeklaration for yearly or busy quarterly VAT periods. - get_trial_balance now delegates to the canonical generateTrialBalance (lib/reports), which paginates via fetchAllRows and rolls opening balances forward, also fixing a latent bug where the tool ignored IB. - computeVatReport now paginates its line fetch via fetchAllRows. The library fixed this class of bug in #79; these two MCP paths kept their own copies that were never updated. Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(mcp): non-null assert periodId in generateTrialBalance call Fixes the core-only TS build error (string | undefined not assignable to string). periodId is guaranteed defined by the !period guard above; mirrors the existing periodId! call later in the file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): support .range() in computeVatReport mock for paginated query computeVatReport now fetches journal_entry_lines via fetchAllRows (.range), but the hand-rolled mock terminated at .lte(). Move the terminal to .range() so the 8 VAT-aggregation tests exercise the paginated path. Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e4a9fdb4a6 |
fix(reports): add missing allLines mock to SIE OB-exclusion test (#828)
#824 moved per-entry line fetching into a single paged journal_entry_lines join query. #809's OB-exclusion test was branched before #824, so its mock queue lacked that response — every later mock shifted by one and the #OBJEKT projects loop read journal-line objects (no .code), throwing TypeError on main. Adds the journal_entry_lines (allLines) mock with lines keyed by journal_entry_id. Test-only; no production code change. Restores green main. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b4bf63fec |
fix(reports): sort trial balance source lines by date then voucher_number (#763)
.order({ foreignTable }) in Supabase/PostgREST sorts the embedded
resource's rows, not the parent result set. Journal entry lines in the
trial balance drill-down were therefore returned in database insertion
order rather than chronological order.
Sort in JavaScript after fetching — mirroring the approach in
generateGeneralLedger — to guarantee entry_date ASC, voucher_number ASC
ordering regardless of what the database returns.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
5e9aa52dea |
feat(mcp): add gnubok_link_document_to_voucher tool (#804)
Links an uploaded document directly to a posted verifikation (journal entry) via the staged-operation pattern. Covers imported/manual vouchers that have no bank-transaction row — the gap left by gnubok_attach_document_to_transaction. - New MCP tool gnubok_link_document_to_voucher (bookkeeping:write scope) - New pending-operation type link_document_to_voucher (medium risk) - Commit executor with WORM guard: refuses to re-link a doc already pinned to a different posted JE (BFL 5 kap 6 §); allows overwriting a draft-JE link; maps period-lock throws to 409 - 5 executor unit tests covering 404, WORM 409, draft-allow, happy path, and period-lock Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
15af0cd7ea |
feat(mcp): add per-line account overrides to create_supplier_invoice_from_inbox (#805)
Adds a line_overrides param (array of {line_number, account_number}) so agents
can override the BAS account on individual lines rather than every line
inheriting the supplier default. Priority chain: line_overrides → extracted
accountSuggestion → supplier default → 4000.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
4f96ce920a |
fix(arcim-migration): dedup customers/suppliers by name when org-number is absent (#788)
The register import skips already-imported customers and suppliers by org-number only. Records without an org-number (private-person customers, suppliers like PostNord/IKANO) can never match, so every re-sync re-creates them — and their invoices — producing 2x, 3x, 4x duplicate rows. The nameToCustomerId / nameToSupplierId maps were already built and populated but never consulted for dedup. Use them as a fallback: match on org-number when present, otherwise on name. This mirrors the sales-invoice customer-stub path, which already keys on `org ?? name`. When an org-number is present we still match on it alone, avoiding false-positive name collisions across distinct legal entities. Signed-off-by: Jonas Hagberg <jonas@lindan.se> |
||
|
|
96989f1cde |
fix(arcim-migration): treat 12-digit Swedish numbers as domestic + route personnummer to personal_number (#799)
Customer/supplier type inference flagged any identity number whose digit count was not exactly 10 as a foreign org number, so a 12-digit century-prefixed personnummer (e.g. 19700616-7113) was misclassified as non_eu_business instead of swedish_business — applying export VAT to a domestic party. Recognize both 10- and 12-digit (19xx/20xx) Swedish numbers via a new looksLikeSwedishIdNumber helper. Separately, mapCustomer always wrote the provider's single identity-number field into org_number, even for individuals. The customer form renders personal_number for individuals (org_number for businesses), so a Privatperson's personnummer was stored in a column the UI hides. Route the number to personal_number for individuals. Adds entity-mapper-customer-type unit tests. Signed-off-by: Jonas Hagberg <jonas@lindan.se> |
||
|
|
47f03f4238 |
feat(arcim): import Bokio underlag and link to verifikat (#786) (#813)
* feat(arcim): import Bokio underlag and link to verifikat Adds an optional, re-runnable step that pages the Bokio /uploads, resolves each receipt's target verifikat via the SIE-preserved voucher number, and archives it through the document service linked to the journal entry. Closes the gap where neither the SIE GL import nor the entity import carries the receipts/underlag attached to each verifikat. - lib/providers/bokio: getBytes() binary download + an attachments resource module (uploads list, GUID->voucher index, per-upload download); pageSize capped at 100, file type taken from the upload's contentType since the download is octet-stream - importProviderDocuments: bulk in-memory resolution keyed on (fiscal period, series, number) — scoped per fiscal year because Bokio restarts numbering at V1 each year; idempotent on (company_id, sha256) so re-runs don't duplicate the undeletable BFL-linked rows - POST /import-documents route, kept off the migration critical path because the Bokio document API is rate-limited (200 req/60s) - journal_entry_id link only for v1; reuses the document-service link path (same module as #804) rather than forking it Closes #786 Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(arcim): stable pagination order + account for unresolvable receipts Addresses two findings from a Codex review pass on the import step: - Add .order('id') to the paged journal_entries / document_attachments / fiscal_periods reads. fetchAllRows pages with .range(), and PostgREST paging without a deterministic order can skip/repeat rows once a table exceeds one page (journal_entries crosses 1000 across several migrated years), which would defeat both voucher resolution and the sha256 dedup. - Keep every upload carrying a journalEntryId in scope instead of pre-filtering on a resolvable voucher ref, so a receipt whose Bokio entry number didn't parse (or resolves to no verifikat) is counted as unmatched rather than silently dropped from the best-effort report. Tests: add unresolvable-ref and zero-uploads cases; mock now supports .order(). Signed-off-by: Jonas Hagberg <jonas@lindan.se> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> |
||
|
|
da692c2898 |
fix(transactions): include 'overdue' in match-supplier-invoice CAS guard (#779)
SupplierInvoicePicker shows overdue invoices as payable candidates, but the CAS update in the match route omitted 'overdue' from its status whitelist. This caused the update to return 0 rows for any overdue invoice, committing a journal entry and then orphaning it before returning MATCH_SI_NOT_OPEN — making the match appear to fail due to a concurrent request. The v1 route already had this correct. Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
b2aa79d553 |
fix(reports): exclude opening-balance entry from SIE #UB movement (#809)
* fix(reports): exclude opening-balance entry from SIE #UB movement getOpeningBalances() returns obEntryId so callers can exclude the OB entry from period queries to prevent double-counting. sie-export.ts was discarding obEntryId, so the OB entry's lines were counted in both openingBalancesByAccount and calculateBalances. The double-count cancelled the real net movement, leaving #UB = #IB for any account that was zeroed out during the year (e.g. a closed bank account). Fix: filter the OB entry out into periodEntries before the #VER loop and calculateBalances, matching the pattern already used by trial-balance.ts and general-ledger.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(reports): add regression test for SIE #UB double-count via OB entry Adds a test that exercises the exact scenario that was broken: the OB entry returned by the journal_entries query being included in both getOpeningBalances (#IB) and calculateBalances (movement), leaving #UB = #IB for an account zeroed out during the year. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
d603042328 |
build(deps): bump The-PR-Agent/pr-agent from 0.37.0 to 0.38.0 (#818)
Bumps [The-PR-Agent/pr-agent](https://github.com/the-pr-agent/pr-agent) from 0.37.0 to 0.38.0. - [Release notes](https://github.com/the-pr-agent/pr-agent/releases) - [Changelog](https://github.com/The-PR-Agent/pr-agent/blob/main/CHANGELOG.md) - [Commits](https://github.com/the-pr-agent/pr-agent/compare/85178bef87b7a03081cd30592a5aad100284f9a7...bd09b6cf89c6d6f3d16b159fa7603fa0e7768cf2) --- updated-dependencies: - dependency-name: The-PR-Agent/pr-agent dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
5cf23c180d |
build(deps): bump alpine from a2d49ea to 28bd5fe in /docker (#819)
Bumps alpine from `a2d49ea` to `28bd5fe`. --- updated-dependencies: - dependency-name: alpine dependency-version: '3.24' dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cb01b946fc |
fix(bokslut): map computer/vehicle depreciation to standard 7832 so it resolves (#755) (#822)
* fix(bokslut): map computer/vehicle depreciation to standard 7832 so it resolves (#755) The computer asset category mapped depreciation expense to 7833 and vehicle to 7834, but neither is in the standard BAS catalog (7834/7835 were removed as non-standard in #463, guarded by bas-reference.test.ts). Because backfillStandardBASAccounts only seeds accounts present in BAS_REFERENCE, the engine threw AccountsNotInChartError on minimal charts and annual depreciation was blocked. - Remap computer and vehicle depreciation expense to 7832 (Avskrivningar på inventarier, verktyg och installationer). Both 1240 (Bilar) and 1250 (Datorer) sit in the maskiner-och-inventarier asset range, so 7832 is the correct standard depreciation account — same one equipment already uses. The asset register still separates them via 1240/1249 and 1250/1259 on the balance sheet. - A regression guard surfaced a second gap: other_tangible mapped to 1280/1289, but 1280 is 'Pågående nyanläggningar/förskott' and 1289 is not a BAS account. Remap other_tangible to 1290/1299 ('Övriga materiella anläggningstillgångar' + its ack. avskrivningar) — the BAS-correct accounts, and the range the iXBRL K2 mapper already classifies other_tangible under. Keeps accumulated = asset + 9. - Add a guard test asserting every DEFAULT_ACCOUNTS_BY_CATEGORY account resolves in BAS_REFERENCE, so a future missing account fails CI instead of a user's depreciation run. No new BAS accounts are added, so the non-standard-accounts guard stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): drop the duplicate next-best-action hero for a single CTA The agent-built dashboard showed a 'next best action' hero card AND the unified 'Att göra' worklist below it — two surfaces pointing at the same work (book transactions, unpaid invoices). Remove the hero so the page leads with metrics + the single 'Att göra' worklist, giving one unambiguous CTA surface instead of two. Drops the now-unused nextBestAction computation and the Receipt/ ArrowLeftRight/Clock imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
663e1232a4 |
fix(bookkeeping): push grundbok series filter into the RPC so pagination total is honest (#798) (#823)
The verifikationslista filtered by voucher series in the API route, AFTER the list_fiscal_period_entries_with_related RPC had already paged — recomputing `count = entries.length` from the current page only. That clobbered the RPC's window-function total_count, so whenever a user filtered the grundbok by series the paginator's total collapsed to the matching subset of one page and 'next page' disabled early — the list appeared to stop after ~one page. - New migration adds p_series to the RPC and applies the filter inside the matching CTE, so COUNT(*) OVER () reflects the filtered set. DROP+CREATE (a new param changes the function identity); p_series DEFAULT NULL keeps existing callers unaffected. Re-GRANTs EXECUTE to authenticated to match the original definition, since DROP+CREATE drops prior grants. - The route passes p_series and drops the post-filter recompute; entries and count now come straight from the RPC. - Extends list-filters.pg.test.ts: series=B over a 2A/3B period returns the 3 B entries with total_count=3, and a short page (limit 2) still reports total 3 — the regression that broke pagination. Note: hosted DB does not auto-apply migrations on merge — apply 20260629160000 to prod after merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4872c0f242 |
fix(reports): stable total order on SIE-export fetchAllRows paging (#793 hardening) (#824)
#820 fixed the primary #793 truncation (the nested embedded-resource select hit PostgREST's row ceiling, exporting ~30 vouchers) by splitting entries and lines into two fetchAllRows calls. But both queries paged without a stable TOTAL order: the entries query ordered by voucher_number alone (not unique across voucher series) and the lines query had no .order() at all. Per the fetch-all.ts invariant, .range() paging without a unique total order can duplicate or skip rows across the 1000-row boundary — so a year with >1000 entries/lines and multiple series could still drop or double a voucher in the SIE file (BFL completeness). - Entries: order by voucher_series + voucher_number (unique per company+period). - Lines: order by the line PK id. - Both carry dedupeBy: r => r.id as defense-in-depth, mirroring the general ledger / trial balance fix in #811. - Give the large-period test's line fixtures unique ids so the >1-page dedupe path is exercised realistically (all 5000 lines survive). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0e8698f538 |
fix(entitlements): billing reachable in settings modal + conversion redesign (#821)
- Register BillingSettingsContent in SETTINGS_SECTIONS so the settings MODAL renders Abonnemang (it was falling back to Företag — billing was only a standalone page, never a registered section). Page is now a thin wrapper over the same component. - Add GET /api/billing/status (isPaying / configured / trialEndsAt) so the client section gets state without server-only reads. - Redesign for conversion: trial days-left urgency banner, reactive monthly/yearly price with a 'Spara 2 mån' badge, full-width price-bearing CTA, Stripe trust line, design-system-compliant chrome (flat Card, no shadow/rounded-xl, on-scale spacing, serif headline). Trialing companies now see the upgrade path (not the manage button). The reported 'peach band' was not reproduced in code — no peach/salmon color exists in the app CSS and the only bottom drag-handle is in a md:hidden mobile sheet; most likely a macOS screenshot/desktop artifact. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
60e33c4b51 |
Fix/cus fee 28 (#820)
* feat(invoices): add Plusgiro input to bank details settings Plusgiro was already persisted, validated by the API schema, rendered on the invoice PDF and toggleable via "Visa plusgiro" — but the settings UI had no field to enter the number, so plusgiro-only users could not fill it in. Add the input next to Bankgiro with Luhn validation and hyphen formatting, include it in the save payload (normalised on save so raw digits still match the dashed schema format), and add sv/en strings. Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): respect non-VAT-registered seller in PDF preview + portal tooltips Two user-reported bugs: - PDF preview (/api/invoices/preview-pdf) ignored company.vat_registered and fell back to the customer-driven 25% rate, so a non-momsregistrerad seller saw VAT in the review step even though the created invoice books none. Mirror the server-side write gate (build-invoice-write.ts): force 0% when vat_registered is false (delivery notes excepted). - InfoTooltip rendered TooltipContent without a Portal, so tooltips were clipped by the scrollable DialogContent (overflow-y-auto) in the send-invoice journal-entry review. Wrap in TooltipPrimitive.Portal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): book library mall from its literal lines, not a lossy fallback Booking a bank transaction with a user-created booking-template (mall) via the convertible "QuickReview" fast path reduced the template to a single category + one account_override, silently discarding the chosen debit/credit. A kundinbetalning mall (D 1930 / K 1510) booked as a generic cost (D 6991 / K 1930), or with a VAT line as D 1930 / K 1930 / K 2611 — and the result flipped with the direction inferred from the business/settlement line tags, so visually-identical templates produced different verifikationer. Route every library template through the journal-entry editor (applyTemplate -> /book), which posts the literal lines, regardless of convertibility. Add regression tests locking the contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): make the booking-time duplicate guard bypassable TRANSACTION_BOOK_POSSIBLE_DUPLICATE told users they could "book anyway" but the UI dead-ended on a toast with no way to do so. Add a shared DuplicateBookingDialog that surfaces the already-booked sibling and lets the user review it or book anyway (force bound to the reviewed candidate, which the server re-detects so a stale id cannot wave the guard away). - Wire the dialog into the /transactions categorize flow and the manual booking dialog (JournalEntryForm -> /api/transactions/[id]/book) - Bind the override to expected_duplicate_transaction_id OR expected_duplicate_journal_entry_id so ledger-only vouchers (paid invoice, salary run) can be confirmed too - Extend the guard to the pending-operations commit path and the MCP server - Tests for book/categorize routes, detection, and the commit guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): log duplicate-guard bypass to behandlingshistorik in the agent commit path The web /book and /categorize routes append a durable BankTransactionDuplicateDismissed event when a user books over a detected possible double-booking. The agent commit path (commitCategorizeTransaction, commitMarkInvoicePaid) skipped the guard silently on allow_duplicate=true, leaving no behandlingshistorik — an auditor could not reconstruct why the duplicate was allowed (BFNAR 2013:2 kap 8). When allow_duplicate=true, re-detect the candidate and append the dismissal event (BankTransactionDuplicateDismissed for the bank-line path, InvoiceDuplicatePaymentDismissed for mark-paid). Best-effort — a logging failure never blocks a legitimate booking. Payloads stay PII-safe (ids, amounts, dates only — no customer or merchant name). Also fix the misleading DuplicateBookingDialog JSDoc: the retry binds expected_duplicate_journal_entry_id, not candidate.transaction_id, so the systemdokumentation matches the actual control (BFL 7 kap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp-server): stub booking-duplicate guard in receipt-matcher categorize tests The gnubok_categorize_transaction tool runs the booking-time duplicate guard before staging; its detection queries consumed the queued supabase mock results, so the staging assertions saw a thrown duplicate error instead of a staged op. Mock detectBookingDuplicate to "no duplicate" since these tests don't exercise that path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(transactions): use roundOre for duplicate-guard öre rounding Replace naive Math.round(x*100)/100 with roundOre() from @/lib/money in the booking-time duplicate guard (detection lib, commit executor, MCP categorize tool), satisfying the no-new-antipatterns ratchet guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sie-export): paginate journal entries and lines to prevent truncation * fix(bookkeeping): keep the Verifikat/Utkast toggle reachable on an empty list The journal entry list early-returned a pristine empty card whenever the visible list was empty and no filter was active, returning before the Verifikat/Utkast toggle rendered. This stranded users with only drafts (no posted entries) and users who emptied the drafts list, who then had to use the main menu to get back to posted entries. Narrow the early return to a genuinely empty ledger (committed view, no drafts, no filters); make the in-list empty placeholder context-aware (no drafts / no filter matches / no posted entries yet); resolve the draft count before clearing loading on an empty committed list to avoid a toggle flicker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enable-banking): persist psu_type and reuse it on reconnect Reconnecting a bank connection re-derived psu_type from the company entity_type every time (aktiebolag -> 'business'), silently overriding the type the user actually authorized with. A connection that only signs as 'personal' — common for AB owners who use a personal Mobile BankID, notably at Handelsbanken — flipped back to 'business' on every consent renewal and failed at the bank's signing step. - Add nullable bank_connections.psu_type column (idempotent migration) - Persist psu_type on connect; on reconnect reuse the stored value (explicit client override still wins) - Let users switch account type (Företag/Privat) from the reconnect button - Tests for persistence, reuse, and override Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): set maxDuration=300 on bank-file execute to prevent timeout A full-year bank file (300+ rows) runs a sequential per-row ingest that takes ~85s of server time. The execute route set no maxDuration, so it inherited the platform default and was killed mid-run — the import "spins then aborts" for the user. Match the SIE import route and give it a 5-minute budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(transactions): add assistant entry point on transaction rows The agent ("Lena") could only be reached from Dokumentinkorgen, and only once an underlag was matched to a transaction. Transaktioner is the most common starting point for booking, so users could not start a booking with the assistant from there at all. Add a per-row "Fråga [namn]" button on unbooked transaction rows that opens the existing transaction.categorization intent with the row's transaction_id. The intent already reads any linked underlag, so it works whether or not a receipt is attached. No new logic — only the missing entry point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): enable Swish payment QR on invoices Flip SHOW_SWISH_ON_INVOICE on so the Swish row and payment QR render on the invoice PDF, and make the "Visa Swish" settings toggle live (it was hardcoded disabled). The preview-pdf route now builds the QR too, so it shows in forhandsvisning. Position the QR in the top-right of the payment box. No Swish API integration -- the QR is generated offline and prefills the customer Swish app; reconciliation stays via bank matching. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): scope verifikat list to current year, add storno action, clarify correction preview Three UI fixes from user feedback; no engine logic changed. - List defaults to the current räkenskapsår instead of all years. Voucher numbers run per fiscal year (one A42/year), so showing every year at once made them look like duplicates. New resolveCurrentPeriodId helper. - Add 'Återför (storno)' action on the entry detail page and list row, wiring the existing reverseEntry — a pure reversal (BFL 5 kap 5§) with no replacement, distinct from 'Rätta'. - Correction 'Effekt per konto' preview now labels a removed account 'tas bort' (vs a bare dash) and warns when the proposal is unbalanced; dialog explains the rows are the full new verifikat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bank_connections): add psu_type column to persist chosen authorization type * feat(errors): add CannotReverseStornoError for handling reversal of storno or correction entries --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4f0a7b1db0 |
feat(entitlements): per-company capability paywall — gate, trial seeding, UI upsells, Stripe checkout (#815)
* feat(entitlements): capability-grant gate substrate (paywall + modularity) Two-axis capability primitive behind the SaaS paywall and the per-tenant modularity/marketplace vision: - migration: capability_grants (entitlement axis, polymorphic company/firm scope), company_capability_config (enablement axis), metered_events (append-only), company_has_capability() RPC reusing the 20260619130100 tenant guard; SELECT-only RLS (writes service-role only, no self-grant). - lib/entitlements: hasCapability/requireCapability gate (mirrors guardSandbox, fail-closed, NEXT_PUBLIC_SELF_HOSTED bypass), capability key namespace, metering helper. - unit (11) + pg-real tests (RPC/RLS/tenant-guard incl. no-self-grant). Gate not yet wired into call sites (follow-up commit). Paid keys: ai, bank_sync, skatteverket, email_send. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): enforce capability gate at paid external-service chokepoints Wire the gate into the paid surfaces (keys: ai, email_send, bank_sync, skatteverket): - AI routes (agent invoke/composer/onboarding stream): requireCapability(ai) - Invoice send (web + v1): requireCapability(email_send) - document-extraction event handler: skip Bedrock extract if ai not entitled - enable-banking + skatteverket crons: per-company hasCapability skip in loop - colocated send-route test mocks updated (requireCapability -> null) Free per founder decision: TIC org lookup, VIES VAT validation, FX auto-fetch, cloud backup, BankID login, all internal bookkeeping. DEPLOY ORDER: fail-closed by design — do NOT deploy before trial/comp grant seeding lands, or companies without grants lose these features. Seeding + Stripe checkout/webhook are the next steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): seed trial + comp capability grants Makes the fail-closed gate safely deployable — nobody is locked out at cutover: - AFTER INSERT trigger on companies grants every NEW company a 30-day trial on the PAID keys (ai, bank_sync, skatteverket, email_send), on ALL creation paths (RPC/MCP/direct) — so a new signup can use onboarding AI immediately. - one-time backfill for EXISTING companies: created <=2026-06-07 -> trial ends 2026-07-07; created later -> created_at + 30 days. - permanent comp grants for Arcim/Mattsson (matched by name, no hardcoded UUIDs). - pg tests: clearGrants() for controlled resolver tests + trigger coverage. Trigger fn is SECURITY DEFINER so it writes grants regardless of caller RLS (table has no INSERT policy for authenticated — no self-grant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): client capability visibility + billing page Non-payers get a clean upsell instead of broken/empty features: - CompanyContext gains capabilities[] + useCapability(key); resolved once server-side in the dashboard layout via getCompanyCapabilities (batched, 2 queries), all three provider branches wired. - /settings/billing upgrade page — the destination upsells point to (Stripe Payment Link via NEXT_PUBLIC_STRIPE_PAYMENT_LINK; degrades to 'coming soon' until automated checkout lands). - ChatEmptyState: non-payer sees an Uppgradera CTA (mirrors the sandbox state). - SendInvoiceDialog: email send disabled + upsell note when email_send missing (extends the existing sandbox-disable pattern). Fast-follow: chat input/FAB + document-inbox empty state + bank/skatteverket/ AI-suggest buttons + a shared capability_blocked->toast backstop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): gate remaining paid UI surfaces with upsell (fast-follow) disable-with-upsell across the rest of the paid surfaces (keys: bank_sync, skatteverket, ai): - BankSyncNowButton: sync/reconnect disabled + note when !bank_sync (CSV/SIE stays free) - AGIPanel: AGI submit-to-Skatteverket disabled + note when !skatteverket - SkatteverketConnectPanel: BankID connect/reconnect disabled + upsell - ApprovalCard: AI re-propose (correction) gated; manual approve/reject stay free - InvoiceInboxWorkspace: upsell when extraction empty AND !ai (deterministic parse + manual entry unaffected) - AgentTrigger FAB: routes to /settings/billing when !ai (no dead chat) - settings nav: 'Abonnemang'/'Subscription' link to /settings/billing (sv/en) TaxPaymentPanel + TransactionInboxCard intentionally untouched — only local/ deterministic actions there, nothing paid+external to gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): automated Stripe subscription checkout + webhook Self-serve revenue wired to the same capability-grant primitive: - migration: company_subscriptions (company<->Stripe link/status) + stripe_webhook_events (idempotency) - lib/stripe: getStripe singleton, plan->price mapping, subscription-sync (statusGrantsAccess / subscriptionToState / applySubscriptionState / handleStripeEvent). Active sub -> upsert source='stripe' grants for PAID keys (expiry = period_end + 3d grace); canceled/unpaid -> remove ONLY stripe grants (freeze-and-retain). - routes: POST /api/billing/checkout (hosted subscription Checkout, company_id metadata), POST /api/billing/portal (Customer Portal), POST /api/stripe/webhook (raw-body signature verify, event-id dedup; handles checkout.session.completed + customer.subscription.*) - billing page: real plan-toggle Checkout CTA / manage-subscription portal, gated on isStripeConfigured() - adds stripe@22; unit tests for sync logic Provisioning is webhook-driven (never trusts the success redirect). Needs env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(entitlements): validate UUIDs in capability filter + log webhook errors Addresses PR review (Superagent Security / PR Agent): - has-capability.ts: validate companyId/teamId as UUIDs before interpolating into the PostgREST .or() filter (fail-closed) — removes the latent injection vector flagged in the entitlement gate. Unit tests updated to use UUIDs. - stripe/webhook: log processing failures with event id + type before the generic 500, so a failing webhook is visible to operators. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): always-free AGI XML download for manual filing; only direct API submit is paid Per founder decision on the swedish-compliance-review finding: AGI is a mandatory statutory filing, so producing/downloading the AGI XML must never be paywalled. Adds a free 'Ladda ner AGI-fil' button (generates + downloads the XML for manual upload to Skatteverket's e-service) on all tiers; the gated 'Skicka in underlag' stays the paid convenience (direct API submission — which also requires the paid BankID connection). Upsell reworded to point to the manual path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(entitlements): harden comp-grant match after prod verification Verified Arcim/Mattsson in prod (pwxtzglxptnnvjrpixpg): the name match was case-sensitive (missed the active 'Arcim technology AB' lowercase variant) and would have granted 3 archived dupes. Now match by org_number (5595386219 / 5595719864) OR case-insensitive name, active companies only — hits exactly the 3 active comp companies, excludes archived dupes and the unrelated 'Amnäs Mattsson, Emil' enskild firma. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fce6faff2c |
fix(api): stabilize report pagination + declare real { data, meta } envelope on v1 single/write endpoints (#811)
* fix(reports): stabilize fetchAllRows paging to stop doubled/dropped balances (#790, #791) PostgREST `.range()` paging is only correct when the underlying query has a stable TOTAL order. Several aggregating report queries (general ledger, trial balance, grundbok, supplier/AR ledgers, etc.) paginated without `.order()`, so on datasets larger than one 1000-row page Postgres could return rows in a different order between requests — silently DUPLICATING or SKIPPING rows on a page boundary and doubling or dropping financial totals. - fetch-all.ts: document the ordering invariant and add an optional `dedupeBy` defense-in-depth that drops cross-page duplicates and warns when it fires (surfaces a missing `.order()` in logs instead of corrupting money). - Add a stable `.order()` (line PK or account_number) to every paginated query in lib/reports/ and the account-balances route; pass `dedupeBy` on the money-aggregating line queries. - Add fetch-all unit tests and update report test fixtures to carry row ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): declare the real { data, meta } envelope on v1 single/write/204 endpoints (#794) The OpenAPI generator derives each endpoint's documented body purely from its registered `response.success` Zod schema, and that schema is never validated at runtime — so a route could advertise a shape its handler never sends. #802 fixed this for list endpoints; the same drift was latent on single-resource and write endpoints, which declared the bare resource schema instead of the `{ data, meta }` envelope the handlers actually return. - registry.ts: extend `ResponseMetaSchema` with the optional `audit` block and `partial_expansions` list that writes/expansions emit; add the `NoBodyResponse` sentinel so 204 DELETE handlers document a bare 204 instead of a phantom 200. - Wrap every single/write endpoint's `response.success` in `dataEnvelope(...)` (or `NoBodyResponse` for 204s) across the v1 routes. - Add a response-envelope contract test that fails CI if any JSON endpoint forgets to wrap its schema, with binary downloads and 204s as the only exemptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): extend paging dedupeBy to rc-basis-gaps and opening-balances Address PR review: these two money-aggregating line queries already had the stable `.order('id')` (so paging was correct) but didn't carry `id` in the select, so they couldn't use the `dedupeBy` defense-in-depth that general-ledger and trial-balance got. Select `id` and pass `dedupeBy: r => r.id` so the whole report layer applies the ordering invariant consistently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ae17b304d7 |
fix(import): add stable .order() to account-sync chart paging (#790, #791 follow-up) (#812)
`syncMappedAccounts` pages the company's full chart via `fetchAllRows` to avoid the silent 1000-row PostgREST cap, but the query had no `.order()`. Like the report queries fixed in #811, PostgREST `.range()` paging is only correct with a stable total order — without it, a chart larger than one page could duplicate or skip accounts across page boundaries, corrupting the existing-account Map and causing spurious create/update churn on import. Order on the unique `account_number` (stable total order; the result is read into a Map so the order is invisible to callers). Extend the test mock's query chain to include `.order()`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fc2b4d1e23 |
fix(api): declare the real { data, meta } envelope for v1 list endpoints (#802)
The OpenAPI success schemas for v1 list endpoints declared a bare
{ <name>: [...] } object that no handler returns, so the published spec
advertised a shape the API never emits (#781, item 2). response.success is
doc-only (feeds zodToJsonSchema for /openapi.json; not validated at runtime),
so this is a documentation fix with no behaviour change.
Add listEnvelope() ({ data: [...], meta }) and dataEnvelope() ({ data, meta })
plus a shared ResponseMetaSchema. Ten endpoints that return paginated() now use
listEnvelope; the three that deliberately wrap their array under a named key via
ok() (accounts, fiscal-periods, webhooks — a shape their route tests lock in)
use dataEnvelope. Also corrects the accounts/fiscal-periods examples, which
showed an unwrapped data: [...] that contradicted their handlers.
Refs #781.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
55ba66908b |
feat(salary): let an enskild firma employ staff while blocking owner/board payroll (#797)
An enskild firma that hires staff should get the payroll module, but its owner or board can never be on payroll (owner compensation is egna uttag / BAS 2013, not lön). - Migration 20260628120000 adds the enforce_ef_no_owner_employee trigger (BEFORE INSERT OR UPDATE OF employment_type) as the all-paths backstop. - lib/salary/employment-rules.ts is the app-layer mirror (forbidden set kept byte-identical to the trigger); getCompanyEntityType() resolves the same company_settings -> companies precedence. - The two UI salary routes and the v1 POST guard before insert/update for a clean 400 with guidance. - Payroll nav + Lön settings now show for any employer (aktiebolag OR company_settings.pays_salaries), wired through the dashboard layout. Fixes #782. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5bacda4839 |
fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14 (#796)
* fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14
Onboarding derived the VAT number as SE${orgNumber}01. For an enskild firma the
org number is a 12-digit personnummer, producing SE + 14 digits, which fails the
^SE\d{12}$ validation — the pre-filled value is re-submitted on save and the tax
settings page becomes unsavable.
New shared helper lib/vat/vat-number.ts (normalize/validate/derive, reusing
normalizeOrgNumber to drop the century + Luhn-validate). UpdateSettingsSchema,
the onboarding wizard, the onboarding upsert in lib/company/actions.ts, and the
arcim-migration provider import all route through it. Backfill migration repairs
existing SE+14 rows to SE+12 (idempotent, scoped to ^SE\d{14}$ only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(arcim): warn when a provider VAT number is dropped as malformed
The provider VAT guard silently discarded a value that doesn't normalise to a
valid SE+12 momsregistreringsnummer. Emit a structured warn (provider +
company, no raw value — it can embed a personnummer) so consistently-bad
provider data is observable rather than invisible. Addresses the OWASP V16
logging finding on the arcim VAT-normalisation change in this PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9278221616 |
fix(api): guard params await so static v1 routes don't 500 (#795)
Next.js 16 invokes a static route handler (no [segment]) with
{ params: undefined }. /api/v1/companies is the only authenticated static
route on the v1 surface, so awaiting params.params null-derefs and the catch
turns it into a 500 for every valid API key. Guard the await:
((await params?.params) ?? {}). Dynamic routes are unaffected. Fixes #781.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9ed0b9515a |
Fix/invoice booking vat fixes (#778)
* feat(invoices): add Plusgiro input to bank details settings Plusgiro was already persisted, validated by the API schema, rendered on the invoice PDF and toggleable via "Visa plusgiro" — but the settings UI had no field to enter the number, so plusgiro-only users could not fill it in. Add the input next to Bankgiro with Luhn validation and hyphen formatting, include it in the save payload (normalised on save so raw digits still match the dashed schema format), and add sv/en strings. Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): respect non-VAT-registered seller in PDF preview + portal tooltips Two user-reported bugs: - PDF preview (/api/invoices/preview-pdf) ignored company.vat_registered and fell back to the customer-driven 25% rate, so a non-momsregistrerad seller saw VAT in the review step even though the created invoice books none. Mirror the server-side write gate (build-invoice-write.ts): force 0% when vat_registered is false (delivery notes excepted). - InfoTooltip rendered TooltipContent without a Portal, so tooltips were clipped by the scrollable DialogContent (overflow-y-auto) in the send-invoice journal-entry review. Wrap in TooltipPrimitive.Portal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): book library mall from its literal lines, not a lossy fallback Booking a bank transaction with a user-created booking-template (mall) via the convertible "QuickReview" fast path reduced the template to a single category + one account_override, silently discarding the chosen debit/credit. A kundinbetalning mall (D 1930 / K 1510) booked as a generic cost (D 6991 / K 1930), or with a VAT line as D 1930 / K 1930 / K 2611 — and the result flipped with the direction inferred from the business/settlement line tags, so visually-identical templates produced different verifikationer. Route every library template through the journal-entry editor (applyTemplate -> /book), which posts the literal lines, regardless of convertibility. Add regression tests locking the contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): make the booking-time duplicate guard bypassable TRANSACTION_BOOK_POSSIBLE_DUPLICATE told users they could "book anyway" but the UI dead-ended on a toast with no way to do so. Add a shared DuplicateBookingDialog that surfaces the already-booked sibling and lets the user review it or book anyway (force bound to the reviewed candidate, which the server re-detects so a stale id cannot wave the guard away). - Wire the dialog into the /transactions categorize flow and the manual booking dialog (JournalEntryForm -> /api/transactions/[id]/book) - Bind the override to expected_duplicate_transaction_id OR expected_duplicate_journal_entry_id so ledger-only vouchers (paid invoice, salary run) can be confirmed too - Extend the guard to the pending-operations commit path and the MCP server - Tests for book/categorize routes, detection, and the commit guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): log duplicate-guard bypass to behandlingshistorik in the agent commit path The web /book and /categorize routes append a durable BankTransactionDuplicateDismissed event when a user books over a detected possible double-booking. The agent commit path (commitCategorizeTransaction, commitMarkInvoicePaid) skipped the guard silently on allow_duplicate=true, leaving no behandlingshistorik — an auditor could not reconstruct why the duplicate was allowed (BFNAR 2013:2 kap 8). When allow_duplicate=true, re-detect the candidate and append the dismissal event (BankTransactionDuplicateDismissed for the bank-line path, InvoiceDuplicatePaymentDismissed for mark-paid). Best-effort — a logging failure never blocks a legitimate booking. Payloads stay PII-safe (ids, amounts, dates only — no customer or merchant name). Also fix the misleading DuplicateBookingDialog JSDoc: the retry binds expected_duplicate_journal_entry_id, not candidate.transaction_id, so the systemdokumentation matches the actual control (BFL 7 kap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp-server): stub booking-duplicate guard in receipt-matcher categorize tests The gnubok_categorize_transaction tool runs the booking-time duplicate guard before staging; its detection queries consumed the queued supabase mock results, so the staging assertions saw a thrown duplicate error instead of a staged op. Mock detectBookingDuplicate to "no duplicate" since these tests don't exercise that path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(transactions): use roundOre for duplicate-guard öre rounding Replace naive Math.round(x*100)/100 with roundOre() from @/lib/money in the booking-time duplicate guard (detection lib, commit executor, MCP categorize tool), satisfying the no-new-antipatterns ratchet guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bc09cea07e |
chore(scripts): add prod repair scripts for Arcim and Capelix incidents (#773)
* chore(scripts): add prod repair scripts for Arcim and Capelix incidents Two idempotent, dry-run-by-default repair scripts, committed for the audit trail (matching the existing scripts/repair-*.ts convention). Neither runs automatically — applying requires an explicit --execute/--commit flag. - repair-arcim-supplier-payments.ts: Arcim Technology AB (2026-06-11). Two supplier invoices left in inconsistent half-states (swallowed AccountsNotInChartError on 3740; bank-sync auto-link without a booked payment) plus expense booked on 5010 instead of 5420/6580. Runs through the real engine (createJournalEntry/correctEntry) so voucher numbering and balance triggers behave as in-app; every step checks its precondition. - repair-capelix-invoice-payment.ts: Capelix AB invoice-001 double-booking (2026-05-29), root-caused to the invoiceAlreadyBooked dead-column read fixed in PR #713. Storno-only per BFL/BFNAR 2013:2: reverse the wrong cash entry, post the correct 1930/1510 clearing entry, relink the bank tx + payment row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(scripts): scope Capelix invoice_payments relink to company_id Address review (PR Agent + compliance swarm): the Step 3b invoice_payments update filtered on journal_entry_id only; add .eq('company_id', COMPANY_ID) to match the sibling transactions update directly above it (tenant isolation / defense-in-depth). invoice_payments carries company_id (multi-tenant refactor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10a0b1d8dd |
fix(invoices): embed company logo as PNG so it renders on invoice PDFs (#772) (#776)
* fix(invoices): embed company logo as PNG so it renders on invoice PDFs (#772) @react-pdf/renderer's <Image> only decodes JPG/PNG, but the logo upload route and the `logos` bucket also accept SVG and WebP. For an SVG/WebP logo @react-pdf silently swallows the decode error (console.warn inside a try/catch in its fetchImage step), so the invoice renders with NO logo and nothing surfaces — "Logotyp kommer inte med på fakturor". Fix: prepareInvoicePdfRender now fetches the stored logo and re-encodes it to a PNG data URL via sharp (SVGs rasterized at higher density), handing the template a company whose logo_url is that data URL. Renders regardless of upload format and removes the render-time dependency on a remote fetch inside @react-pdf. Falls back to the original URL unchanged on any failure (network, unreadable image, sharp unavailable), so behaviour is never worse than before. Result is cached per logo URL (5-min TTL, bounded to 50) since the logo is re-rendered on every invoice — twice per send and once per invoice in recurring/batch loops. prepareInvoicePdfRender becomes async and returns the resolved { branding, company }; all 8 call sites updated (6 routes, recurring-schedule-service, pending-operations/commit) to await it and pass the resolved company. Layered cleanly on top of the Swish-QR feature already on main — both coexist at every call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): bound and dedupe the logo fetch (review hardening) Review (PR Agent security): resolveLogoDataUrl fetched logo_url with no timeout or size limit. Add a 5s AbortSignal.timeout and a 5 MB cap (checked on the declared content-length and the read body) so a slow/oversized logo host can't hang or balloon an invoice render. SSRF itself isn't reachable today — logo_url is only ever set to a Supabase logos-bucket URL by the upload route — so an origin allowlist is intentionally skipped (would break self-hosted storage). Also coalesce concurrent renders of the same logo (preflight+final on a send, and recurring/batch loops) onto one in-flight fetch+encode instead of N. New test covers the size-cap fallback; existing SVG test now asserts the timeout signal. 9/9 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
739f18fd1c |
fix(reconciliation): drive bank avstämning period from the report header (#771) (#774)
The bank reconciliation view hosted its OWN FiscalYearSelector inside the action bar — but that bar renders below the loading-skeleton early-return, so the selector never mounted, its onReady/onChange never fired, the periodReady gate never flipped, and the page hung on a permanent skeleton (#771). Make the report period-scoped like the ledgers: lib/reports/catalog.ts marks bank-reconciliation `params: 'fiscal'`, so the report page's räkenskapsår selector owns the period. FocusedReport passes periodId + periodBounds down, and BankReconciliationView takes them as props instead of self-selecting. The window seeds from periodBounds and a periodId-keyed effect re-seeds (and writes dateFromRef/dateToRef synchronously) on a year switch, preserving the #751 period-scoped IB-floor behaviour without the deadlock. Manual date edits still apply on demand via "Filtrera". Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
43007fa869 |
feat(mcp): self-describing agent surface — staging _meta, company identity, clean skill summaries (#775)
* feat(mcp): make the agent surface self-describing (staging _meta, company identity, clean summaries)
A pass over the MCP server's agent-facing surface so an agent can act
correctly without parsing description prose:
- Machine-readable staging contract: deriveToolMeta() attaches _meta to
tools/list (and search detail=full) — { requires_approval, approve_tool,
preflight? } — keyed off the STAGED_OPERATION_SCHEMA output schema. Literal
_meta (e.g. UI widget hints) wins on collision. TOOL_PREFLIGHT_MAP names the
read-only pre-flight for the few writes that have one (year-end readiness,
VAT validate, depreciation proposal). Guarded by staging-meta.test.ts.
- Company identity in gnubok_get_agent_briefing: returns a `company` block
(id, name, org_number, entity_type, accounting_method) so the agent can
confirm WHICH entity it operates on and pick the right settlement account
(accrual = credit 1510; cash = debit 19xx) before any write. Best-effort —
a missing row never blocks the briefing. Covered by agent-briefing.test.ts.
- toSummary(): trims the long, keyword-stuffed SKILL.md frontmatter into clean
one-liners for gnubok_list_skills / gnubok_get_agent_briefing so the client
never truncates one mid-sentence; full bodies stay in gnubok_load_skill.
Covered by to-summary.test.ts.
- bank-reconciliation skill: a match/link decision tree (what you have x
whether a verifikat exists) and kontant- vs faktureringsmetoden settlement
accounts.
- Prose/description clarifications: "Stages"/"Stages for approval" on the
link tools; propose_dispositioner/accruals note there is no dedicated MCP
poster; server-info documents _meta and the legacy gnubok_ tool prefix.
All 34 touched MCP tests pass. Merged cleanly on top of #759/#760 (server.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): make accounting_method description state the full settlement posting
Review (swedish-accounting-compliance): the agent-briefing schema described
accrual as "credit 1510 on payment", which reads as a one-sided entry. Spell
out both sides (payment debits 19xx AND credits 1510) so an agent can't infer a
single-leg posting that violates BFL 5 kap double-entry. Mirrors the precision
already in the bank-reconciliation skill body. Payload-size guard still passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d5abc8dd1c |
fix(inbox): set_inbox_extracted_data should accept and persist accountSuggestion (#760)
The tool was using InvoiceExtractionSchema which forces every
lineItems.accountSuggestion to null via .transform() — the same guard
that prevents the AI extractor from hallucinating BAS accounts.
Agents supplying their own extraction should be able to pin a cost
account per line.
Adds AgentExtractionSchema (exported alongside ExtractionSchema)
where accountSuggestion accepts a validated BAS expense account
(class 4–7, /^[4-7]\d{3}$/) or null. set_inbox_extracted_data now
parses through this schema so the field survives the round-trip to
the DB and is available when gnubok_create_supplier_invoice_from_inbox
builds line items.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
6fe4164adc |
fix(inbox): fall back to supplier.default_expense_account in create_supplier_invoice_from_inbox (#759)
The line-item account lookup was using snake_case `li.account_number` (never populated) instead of camelCase `li.accountSuggestion` from the extraction schema. When accountSuggestion is null, the fallback now checks supplier.default_expense_account before hard-coding account 4000. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
a3491b6d89 |
fix(transactions): resolve bank account from cash_account_id in bulk-book and direct-book dialogs (#770)
Follow-up to #769: applies the same fetch-on-open + resolveAccount pattern to BulkBookDialog (manual tab bank-leg pre-fill) and BookDirectlyDialog (settlement line in buildPrefillLines). Both dialogs now fetch /api/cash-accounts when they open, gate the pre-fill on the fetch resolving, and resolve the correct BAS ledger account per transaction instead of always emitting '1930'. Adds lib/cash-accounts/resolve-account.ts (cherry of the utility from #769) since that PR is not yet merged into main. Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
a4da2d62df |
feat: add ignore menu item to transaction inbox card (#758)
Imported bank transactions can now be hidden from the inbox via a new
"Ignorera transaktion" item in the ⋯ overflow menu. The backend
(POST/DELETE /api/transactions/[id]/ignore) and the is_ignored DB column
already existed; this wires up the missing UI affordance.
- TransactionInboxCard: add onIgnore prop + EyeOff menu item (imported
rows only — manually created rows use delete instead)
- page.tsx: pass onIgnore={handleIgnoreTransaction} to the card; add
handleBatchIgnore for the batch action bar's new "Ignorera" button
- sv.json / en.json: add ignore_btn translation key to tx_inbox_card
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
5279dfb494 |
fix(bookkeeping): gnubok_categorize_transaction now handles incoming expense refunds correctly (#761)
Incoming expense refunds (positive amount on an expense category) previously booked with inverted debit/credit — crediting the bank and debiting the expense account — which both imbalanced the book and reported negative bank flow. getCategoryAccountMapping now detects amount > 0 on expense categories and returns the reversed mapping: debit 1930, credit expense account, with 2641 as vatCreditAccount so ingående moms is correctly reversed on the VAT line. The VAT line description is "Återföring ingående moms X%" rather than the income-side "Utgående moms" label. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
bbc8063f53 |
fix(bookkeeping): reverseEntry defaults to original entry_date, not today (#762)
* fix(bookkeeping): reverseEntry defaults to original entry_date, not today When reverseEntry() is called without an explicit reversalDate, it was defaulting to getSwedishLocalDate() (today). This caused the storno to land in the current period rather than the original entry's period — any reversal of a past verifikation through uncategorize, re-categorize, the dashboard reverse button, salary correction, or fix-cash-mismatch would produce a makulering dated today instead of the original booking date. The entry is already fetched before the date is resolved, so defaulting to original.entry_date is safe. Callers that intentionally want a different date (credit notes, mark-paid with payment date, user-provided reverse date) still pass an explicit reversalDate and are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * feat(api): v1 endpoints to stamp invoice inbox items as consumed Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link (best-effort stamp on the originating invoice_inbox_items row) and a new dedicated POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping independently of the document link — both use documents:write scope and require Idempotency-Key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * Revert "feat(api): v1 endpoints to stamp invoice inbox items as consumed" This reverts commit f1bf3a86385ec556a49830c9e0966236a10d3164. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
429ab7b230 |
build(deps): bump The-PR-Agent/pr-agent from 0.36.0 to 0.37.0 (#765)
Bumps [The-PR-Agent/pr-agent](https://github.com/the-pr-agent/pr-agent) from 0.36.0 to 0.37.0. - [Release notes](https://github.com/the-pr-agent/pr-agent/releases) - [Changelog](https://github.com/The-PR-Agent/pr-agent/blob/main/CHANGELOG.md) - [Commits](https://github.com/the-pr-agent/pr-agent/compare/ffe1f89a4dafc7d8e88b9cf010a3233e30b49f43...85178bef87b7a03081cd30592a5aad100284f9a7) --- updated-dependencies: - dependency-name: The-PR-Agent/pr-agent dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
be9bec6b0d |
build(deps): bump actions/checkout from 6 to 7 (#764)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
931f4fe36d |
build(deps): bump alpine from 3.22 to 3.24 in /docker (#732)
Bumps alpine from 3.22 to 3.24. --- updated-dependencies: - dependency-name: alpine dependency-version: '3.24' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |