15af0cd7eadcecafd6d17618d04bf67b4f736fd2
69 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
7aa37fd3b8 |
fix(mcp): return 405 (not 401) on GET /mcp to stop client re-auth storm (#747)
The Streamable HTTP GET handler returned 401 unconditionally. This server is stateless and offers no server-initiated SSE stream, for which the MCP Streamable HTTP spec requires 405 Method Not Allowed. Returning 401 made spec-compliant clients (Claude connector, Claude Desktop, Cursor) treat the SSE GET as an auth failure and enter a refresh-token → re-open-GET → 401 retry loop. Across the active connector base this storms /api/extensions/ext/mcp-server/mcp (observed ~steady GET→401 traffic on app.gnubok.se) and churns OAuth API-key rotation — and tripped a Vercel usage anomaly (edge requests + function invocations spiking ~16x). OAuth discovery remains bootstrapped on the POST 401 (WWW-Authenticate + .well-known/oauth-protected-resource); the POST JSON-RPC channel and the POST-only npm bridge are unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2a8bf9b42e |
Bug/year end numbers (#744)
* fix(bookkeeping): allow creating a fiscal year that fills an interior gap Fiscal-period creation only allowed chaining a new räkenskapsår before the earliest or after the latest existing period, so a company with a gap between years (e.g. 2024 + 2026 from an SIE import, missing 2025) could not create the missing year — it failed with "New period must chain before the earliest or after the latest existing period". Generalise forward chaining onto the new period's immediate predecessor, which covers both appending a new latest year and filling an interior gap. The "prior year must be locked" guard now applies only to true appends, not gap fills (a backfill, like backward chaining). previous_period_id is set to the predecessor and the successor is relinked so the BFNAR 2013:2 continuity chain stays intact. The create dialog suggests the missing year (capped so it never overlaps the next period), the settings page seeds the dialog at the earliest gap, and the default suggested name is now "Räkenskapsår <year>". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): omföra föregående års resultat (2099 → 2098) at year-end Year-end closing posts the result to 2099 "Årets resultat" and the opening balance carried it forward on 2099 every year, so 2099 accumulated across years and the prior result never moved off "Årets resultat". executeYearEndClosing now posts a separate "Omföring av föregående års resultat" verifikat (Dr 2099 / Cr 2098 for a profit, reversed for a loss) into the new period after the continuity check passes, so 2099 starts each year at zero. Kept as a standalone entry rather than folded into the opening balance so the IB stays a faithful mirror of the prior UB and IB/UB continuity still holds. Aktiebolag only; idempotent; no-op when 2099 is flat. The 2098 → 2091/2898 disposition (bolagsstämma decision) is intentionally left to a separate step. - new source_type 'result_appropriation' (migration + type + Zod enum) - generateResultAppropriation helper (planner + poster) wired as step 11 - ResultStep surfaces the omföring voucher - unit tests + pg-real invariant - scripts/repair-result-appropriation.ts: retroactive catch-up (dry-run default) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): shadow-detect date-drift duplicate bank transactions The content-dedup bridge buckets on exact (date, ore), so the same transaction re-imported with a booking date that drifted a day lands in a different bucket and slips past every dedup layer. Add a measure-only ("shadow") detector that flags would-be +/-1-day duplicates and counts them, without changing what is inserted - so the gap can be validated on real data before any enforcement, mirroring the scope-drift shadow. - shiftIsoDate(): pure, deterministic adjacent-date helper - ingest: DEDUP_DATE_DRIFT_MODE flag (default on), pre-loop bucket snapshot, per-row gate with desc-bridge + cross-channel-symmetry signals; logs shadow_date_drift_candidates, never alters inserts - fail-safe date guard so the measurement can never abort an import - regression tests for both signals, account/window/distinct guards, no-double-count, and the malformed-date fail-safe Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(bookkeeping): anonymize a customer reference in fiscal-period tests Remove a real customer name ("AXMD AB") from regression-test comments; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): enhance Docker image scanning and caching mechanisms * fix(bookkeeping): enhance year-end result appropriation handling and error reporting --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e5f1d7a916 |
fix(reports): count reversed entries in bank/supplier/AR reconciliation (#734)
A reversed (storno-corrected) journal entry must be summed together with
its storno + correction, exactly as the trial balance and balance sheet
already do (.in('status', ['posted','reversed'])). The reconciliation
paths used posted-only and manufactured phantom differences.
- bank-reconciliation getReconciliationStatus: gl_1930_balance now counts
posted+reversed (so it equals the balansräkning for the account). Dropped
the correction_adjustment subtraction and the reversed-linked-tx drop — a
corrected/amount-corrected bank receipt now reconciles. The prior model
broke once correctEntry began re-pointing the bank transaction to the live
correction (the two changes were mutually inconsistent and produced a
difference equal to the corrected amount).
- supplier-reconciliation (2440) and ar-reconciliation (1510/1513): same
posted -> posted+reversed fix. Removes the false "Ej avstämd" gap a fully
paid, fully corrected company shows — the books net to 0 over posted+reversed
while a posted-only query double-counted the payment legs.
Also in this change:
- MCP gnubok_get_reconciliation_status: add account_number param (was hardcoded
to 1930; the lib already supported per-account reconciliation).
- counterparty-templates normalizeCounterpartyName: strip trailing month /
personal-initials tokens so "ngrok JW" / "Ngrok Mars" learn as one merchant.
- pending-operation reject 409 (route + MCP tool): clarify a resolved op was
approved explicitly (no auto-commit path exists) instead of a bare
"already committed".
- create_voucher: normalize description with String().trim() for consistency
with line_description.
- migration 20260625120000: backfill stripped diacritics on ~830 companies'
seeded chart-of-accounts names (Foretagskonto -> Företagskonto, etc.); the
seed function was fixed for new companies in 20260516130000 but never
backfilled. UPDATE-only and idempotent. Already applied to production.
Tests: 899 passing across the touched suites; lint + typecheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
88f49c0ccc |
fix(bookkeeping): harden correction flow and align VAT/cashflow reports (#726)
Bundles a set of bookkeeping-correctness fixes developed together. Correction / storno flow - correctEntry resolves (and seeds standard BAS) accounts for the corrected lines BEFORE writing the storno. The old order created and posted the storno first, then hit AccountsNotInChartError on the corrected lines and had to cancel it again — leaving a voided 0 kr storno in the chain and permanently burning a voucher number (an unexplained BFNAR 2013:2 gap). It now fails fast with nothing written. - correctEntry re-points the bank transaction and underlag from the reversed original to the live corrected entry, so the transaction keeps reading as booked (and stays correctable) and the underlag travels with it. recordateEntry delegates both relinks to correctEntry. - reverseEntry (engine) clears transactions.journal_entry_id for rows booked by the reversed entry, so a plain storno returns the bank row to "Att bokföra" with a re-booking affordance. The agent paths did this manually; the dashboard reverse route did not. - findUnresolvableAccounts replaces findMissingActiveAccounts in the categorize routes: a standard BAS account merely absent from the chart is seeded on demand by the engine, so pre-validation must not 400 on it — only unknown numbers or deactivated accounts block. - CorrectionChain dims cancelled (0 kr) entries and labels them so they no longer render like a live storno. Report accuracy - calculateVatLiability() (lib/reports/kpi.ts) is shared by the KPI route, the KPI xlsx export and the MCP period-summary tool, and uses the same 26xx accounts as the momsdeklaration (ruta 49). Reverse-charge and import pairs (e.g. 2614 credit + 2645 debit) net to zero instead of inflating the receivable (#715). VAT_OUTPUT_ACCOUNTS / VAT_INPUT_ACCOUNTS are derived from ACCOUNT_RUTA so the widget can never drift from the declaration. - Kassaflödesanalys records erhållna aktieägartillskott (2093) as a financing inflow and counts överkursfond (2086/2097) toward nyemission. 2093 was previously unmapped, so any contribution broke the 19xx reconciliation by exactly the contributed amount (#716). Wired through the report type, both PDF templates, the K3 PDF, the dashboard client and the årsredovisning summary type. Agent guidance - shared-rules: describe the real Accounted correction flow (Rätta rader / Rätta datum / Radera verifikat, on-demand BAS backfill) so the assistant stops inventing flows that don't exist. - verifikation-draft: clearer locked-period guidance. Tests cover all of the above (storno fail-fast + seeding + relink, reverseEntry unlink, findUnresolvableAccounts, VAT netting and the cashflow reconciliation cases). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
db8983ba9e |
Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import - Briox auth via account ID + application token (no app-level credentials); both tokens rotate on refresh and are persisted - New sie-fetcher pulls the general ledger as SIE through the provider API for Fortnox, Briox and Bjorn Lunden - Wizard stops on a failed SIE import and surfaces the real errors instead of proceeding to the misleading migrate-guard message - PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED; new PROVIDER_TOKEN_INVALID for rejected provider credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices Defer revenue/costs per invoice line to 29xx/17xx interim accounts with automatic monthly dissolution (nightly cron + catch-up at registration), schedule cancellation on credit, year-end auto-detect exclusion for already-scheduled invoices, invoice-inbox service-period extraction for prefill, and an MCP tool to list schedules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing Generate the annual report as iXBRL from a generated taxonomy registry (K2 element lists, taxonomy:generate/check scripts + CI guard), expose it via the fiscal-period API, and add the bolagsverket extension for digital submission to eget utrymme with webhook-driven status tracking (submissions table + pg tests, lifecycle events, year-end wizard UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): raise origin-guard test timeout to 20s The dynamic import pulls in the full server module; the parse alone flirts with the 5s default under full-suite parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new scripts and documentation for K2 AB taxonomy generation and validation - Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models. - Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle. - Included new documentation files: - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx` - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx` - `taxonomi-paket-2024-09-12_rev20250312.zip` * Add tests for bookkeeping accruals dissolution and supplier invoices - Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios. - Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions. - Introduce tests for the Arcim migration provider client, ensuring token handling and error classification. - Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings. - Add Zod schemas for Bolagsverket response payloads to ensure proper validation. - Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping. - Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly. - Introduce typed domain errors for accrual schedules to improve error handling in the service. - Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling. * fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments * fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated * feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id * feat(bokslut): enhance compliance and financial processing features with new submission details and security measures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8e8b63a200 |
fix(bookkeeping): honor underlag VAT via vat_amount override in categorize flow (#717)
* fix(bookkeeping): honor underlag VAT via vat_amount override in categorize flow
The categorize flow always derived VAT as rate × gross/(1+rate) from the
transaction amount, with no way to use the underlag's actual moms. On e.g.
a restaurant receipt with dricks (no VAT on the tip), the agent could see
the document's correct VAT but the staged booking recomputed the wrong
rate-based amount on every attempt.
- buildMappingResultFromCategory: optional vatAmountOverride replaces the
rate-derived VAT line ("Ingående/Utgående moms (enligt underlag)"; 0 =
no VAT line). Rejects negatives, amounts above the 25%-extraction bound,
and combination with reverse_charge / VAT-less treatments / private.
- gnubok_categorize_transaction: new vat_amount input, threaded into the
staged preview and persisted in the operation params.
- commitCategorizeTransaction: reads params.vat_amount so the approved
posting matches the staged preview exactly.
- PATCH /api/pending-operations/[id]: accepts vat_amount (null clears);
preserves a staged override across category edits while the treatment
still carries rate-based VAT, drops it when it no longer does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: guard order + agent guidance on vat_amount (PR #717 bots)
- Check treatment compatibility before the 25%-extraction bound so an
oversized override on reverse_charge reports the actual mistake (the
treatment), not the amount. Document why the typeof re-check stays:
commit-time params come from jsonb, so TS types don't hold at runtime.
- vat_amount property description now warns that foreign VAT is never
deductible as ingående moms and that a 0-moms document should use
vat_treatment="exempt" rather than vat_amount=0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): tools/list payload budget + reject vat_amount 0
core-only failed: the verbose vat_amount descriptions pushed the projected
tools/list payload to 36,051 tokens (ceiling 36,000; main is at 35,862).
Per the guard's own guidance, trim descriptions instead of bumping:
now 35,943.
Folds in the Swedish review's round-2 point while trimming: vat_amount 0
is now rejected with a pointer to vat_treatment "exempt". A 0-moms
document is an exempt supply — "exempt" produces the identical expense
booking and the correct income account (3004), so 0 had no use case and
only created a silent momsdeklaration misclassification path. Schema
declares exclusiveMinimum: 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bookkeeping): use roundOre for vat_amount math (antipattern ratchet)
Second core-only failure: the naive-ore-round ratchet caught the new
Math.round(x*100)/100 lines (662 > baseline 661). Switch the override
path to roundOre from lib/money — including the pre-existing computed-VAT
line this PR touched — and ratchet the baseline down (659, raw-route-auth
168 locked in from main-side fixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
205610d200 |
feat(mcp): distribution-channel client marker in MCP telemetry (#706)
* feat(mcp): distribution-channel client marker in MCP telemetry
Record an optional client marker on mcp.tool_called, mcp.tools_list_called
and mcp.resource_read events so per-channel adoption (e.g. the OpenClaw
skill) is measurable in event_log (180-day TTL).
- Server reads X-Gnubok-Client header, falling back to a ?client= query
param on the endpoint URL. Sanitized ([A-Za-z0-9._-]{1,64}, lowercased),
telemetry-only — same trust level as Mcp-Session-Id, never auth.
- The query param works with the already-published gnubok-mcp 1.0.1 via
GNUBOK_URL, so no npm release is required to start measuring.
- Bridge 1.1.0 additionally forwards GNUBOK_CLIENT as X-Gnubok-Client.
OAuth-path attribution via DCR client_name is a possible follow-up — DCR
is stateless today, so client_name isn't recoverable at token time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): address PR #706 compliance findings
- ropa.yaml: declare the distribution-channel marker in the mcp.telemetry
processing activity (GDPR Art. 30 — RoPA was drifting from actual flow)
- bridge: mirror the server's allow-list on GNUBOK_CLIENT so an invalid
value degrades to no header instead of fetch() rejecting every request
- lib/events/types.ts: annotate client as client-supplied/telemetry-only
- test: pin that the allow-list runs on the percent-decoded ?client= value
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f9ea9c0082 |
Add/pdf and templates (#705)
* fix(invoices): apply configured voucher series to payments + preview next voucher The booking engine resolves the series from default_voucher_series_per_source_type, but the global "Standardserie" dropdown wrote a separate field the engine ignored, and cash-method invoice payments (invoice_cash_payment) weren't exposed in settings — so configured series were silently dropped to "A". - Expose cash/private payment source types in the per-source-type form - Write the global default through to the map on save, keeping overrides - Resolve voucher-sequences/next by source_type (+date) to match the engine - Show the upcoming voucher (V2) in the payment dialog title - Share resolveInvoicePaymentSourceType so preview and booking can't drift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): keep AGI panel in sync with Skatteverket signing state The AGI panel mixed run-scoped generation state (agi_generated_at, agi_declarations) with period-scoped submission state (extension_data agi_submission_{period}), so the two could drift and present contradictory UI. Reconcile them: - Auto-detect a Mina Sidor BankID signature: while awaiting_signing, poll /agi/kvittenser on mount and on tab refocus so the panel flips to "signed" (hiding the signing actions) without a manual "Hamta kvittens" click. - Warn instead of offering to sign when the locked granskningsunderlag predates the run's latest AGI generation (draftIsStale) — avoids filing superseded figures. - Self-heal a stale "AGI-XML saknas" error once the run's AGI is (re)generated out-of-band (MCP/API/other tab). - Refetch the salary run on tab focus so agi_generated_at reflects out-of-band generation without a hard reload. - /agi/lasUpp now clears the cached agi_submission_{period} record, so unlocking drops the panel back to the pre-submission state instead of stranding it on a released "redo att signeras" draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Implement VAT registration handling and invoice item line types - Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies. - Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly. - Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field. - Enhanced invoice and credit note handling to accommodate new line types. - Added new localized messages for text rows in English and Swedish. - Created tests for salary run approval logic, ensuring bank details are validated correctly. - Implemented effective net payout calculation for salary runs, considering tax overrides. - Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers. * feat(articles): artikelregister with revenue account + VAT rate per article Article register (non-inventory) with per-article VAT rate and optional BAS class-3 revenue-account override. Includes API routes, UI pages, MCP tools, pending-operation staging, and the activate-or-create account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog, unknown numbers -> AddAccountDialog) reusing the journal entry UX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): no-doc-required batch + bulk-missing endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(payments): supplier payment lines + cash-method invoice matching Shared payment-line proposal for supplier invoices, improved match-invoice/match-supplier-invoice flows (kontantmetoden-aware), and voucher-link support without requiring a 151x clearing entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc New journal entry dialog component, journal list/page updates, invoice editor updates, SIE import adjustments, transaction ingest and api-key tweaks, pr-agent workflow update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): implement tax reduction features and localization updates * feat(tests): add VAT registration gate to pending operations commit tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0b006fcc1 |
feat(invoicing): artikelregister (product/article catalog) with per-article revenue account (#703)
* feat(invoicing): artikelregister (product/article catalog) with per-article revenue account Add a lean, non-inventory article catalog (artikelregister) so users can define reusable invoice-line presets (name, unit, price excl VAT, VAT rate) with an optional per-article BAS class-3 revenue-account override. - DB: articles table (RLS via user_company_ids(), audit + updated_at triggers, unique-per-company article_number), generate_article_number RPC (atomic + idempotent), company_settings counter, nullable invoice_items.revenue_account + article_id, pending_operations CHECK expansion. - Engine: generatePerRateLines groups revenue by (vat_rate, account) — byte-identical with no override, balance-safe when split (last account absorbs the rounding remainder), reverse_charge/export still force 3308/3305. - API: /api/articles CRUD (soft-deactivate); override validated against chart_of_accounts (active class-3) and frozen onto invoice lines at create. - Propagation: override carried through send/mark-sent/credit/convert/cash and the staged commit paths (recurring deferred — documented inline). - MCP: gnubok_list/create/update_article (staged, scoped, risk-tiered). - UI: articles register (list/detail/form) + nav + bilingual i18n + invoice-line article picker & "Spara som artikel" quick-create. - Tests: engine regression, route, and pg-real (RPC/RLS/triggers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): strip ILIKE _ wildcard from gnubok_list_articles search Underscore is a single-character ILIKE wildcard; stripping it (alongside the existing %,()\* set) keeps a stray char in the article search from matching every row. Read-only + RLS-scoped, so no security impact — addresses PR #703 reviewer + compliance-swarm CC6.3 notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
679b154ad2 |
feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5) (#692)
* feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5) Expose the complete Skatteverket extension as five MCP tools so VAT (momsdeklaration) and employer (AGI/arbetsgivardeklaration) filing can be driven from Claude. Commit = "send for BankID signing" (returns a signing link), never "file" — the user's signature in the browser is the irreversible act, kept outside the tooling. Tools (extensions/general/mcp-server/server.ts): - gnubok_vat_declaration_validate (compliance:read) — live POST /kontrollera - gnubok_vat_declaration_submit (skatteverket:write) — stages submit_vat_declaration - gnubok_vat_declaration_status (compliance:read) — GET /inlamnat + /beslutat - gnubok_agi_submit (skatteverket:write) — stages submit_agi - gnubok_agi_status (compliance:read) — local state + live kvittenser Architecture: - Core (lib/pending-operations/commit.ts) cannot import @/extensions (CI guard), so the two submit ops dispatch into the extension via the new Extension.services channel (first use): registry-resolved commitSubmitVatDeclaration / commitSubmitAgi run the SKV chain and return a shared SkvSubmitResult (lib/pending-operations/skatteverket-commit.ts). - Recoverable failures (extension disabled, no connection, rate-limited, still processing) release the op back to 'pending' via SkatteverketRecoverableError — same contract as AccountsNotInChartError — so the user reconnects and re-approves the SAME op. SKV business rejections reject the op. - No-drift: parseDeclarationRequest / loadAGIXml extracted to lib/declaration-prep.ts (buildMomsuppgift / buildAgiUnderlag / resolveRedovisare) so route, preview, and commit file identical figures. writeSkatteverketAudit hoisted to lib/audit.ts; read tools + executors write BFL audit rows too. - New scope skatteverket:write (opt-in, in STAGING_SCOPES so SoD ack fires), 4 structured error codes, sv/en strings, ApiKeysPanel row. - Migration 20260620120000 adds submit_vat_declaration / submit_agi to the pending_operations.operation_type CHECK (must apply to prod post-merge). Tests: 42 new across executors, MCP tools, declaration-prep, error-map, and the VAT commit chain. Full suite green (5287), build clean, lint-ratchet at baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add PR-Agent AI review (SHA-pinned, dedicated Bedrock key) Greptile went silent after #682 (app/account-side, not repo config). Add the open-source PR-Agent GitHub Action as a replacement, hardened for supply chain: - Pinned to the v0.36.0 commit SHA (ffe1f89), not the movable tag — the repo was recently transferred to a new, unverified org (The-PR-Agent), though it's the genuine original pr-agent (repo id 662766482, 11.5k stars). - Runs on a DEDICATED, minimal IAM key (bedrock:InvokeModel only) via PR_AGENT_AWS_* secrets — never the app's general AWS credentials. - Only /review runs automatically; /describe and /improve are disabled so PR descriptions are never overwritten. Requires three new secrets before it functions: PR_AGENT_AWS_ACCESS_KEY_ID, PR_AGENT_AWS_SECRET_ACCESS_KEY, PR_AGENT_AWS_REGION (EU region). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): handle push events + restrict push to /review PR-Agent skips synchronize (push) events by default, so the bot ran green but posted nothing. Enable handle_push_trigger and scope push_commands to /review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): fix pr_actions (event list, not commands) + add synchronize pr_actions is the list of PR event actions to handle, not slash-commands. Setting it to ["/review"] removed every real event from the allowlist, so the bot skipped everything. Restore the default events + synchronize; command selection stays on the auto_review/describe/improve booleans (review-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): raise max_model_tokens to 64k for fuller diff coverage Default ~32k input window truncated large PRs. Sonnet 4.6 has 200k context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): use Claude Opus 4.8 (Sonnet 4.6 fallback) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skatteverket): scope AGI status flips by salary_run_id Bot review (swedish-compliance) caught that commitSubmitAgi flipped agi_declarations status by (company_id, period) only. A correction run sharing the period would have its still-valid declaration co-flipped to rejected/ pending_signature. Scope both updates by salary_run_id (in scope from params) — more precise than the period-only route handler, which has no run id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pg): fix gen_random_bytes assertion for modern pgcrypto OpenSSL-backed pgcrypto (CI Postgres image) rejects gen_random_bytes(0) with 'Length not in range' rather than returning empty bytea, so the pre-existing 'returns empty bytea' assertion fails on every pg-real run (repo-wide, not specific to this PR). Assert the real contract — exactly n bytes for a positive n — instead of the version-dependent 0-byte edge case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f79a74a2e |
fix(mcp): allow null optional fields on transaction list tools (#484)
* fix(mcp): allow null optional fields on transaction list tools Signed-off-by: antonisoaho <anton@isoaho.se> * test(mcp): assert null optional fields on list_transactions_without_documents * Update extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Signed-off-by: antonisoaho <anton@isoaho.se> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> |
||
|
|
03a2130919 |
feat(mcp): Origin-header validation + serverInfo title + connect-claude docs export (P0-4 follow-up) (#684)
Closes the two code-side gaps found while auditing the Claude Connectors Directory submission checklist after #682/#683: 1. Origin-header validation on the /mcp endpoint (POST/GET/DELETE) — an explicit directory submission requirement and an MCP spec MUST for the Streamable HTTP transport (DNS-rebinding defense). Requests without an Origin header (claude.ai backend, Claude Desktop, npx gnubok-mcp, Claude Code, MCP Inspector's proxy — every known client) pass through unchanged. A present Origin is allowed only when its host matches the request Host (covers Vercel previews + self-hosted without hardcoding) or NEXT_PUBLIC_APP_URL (proxy-rewritten Host); anything else is 403 with a JSON-RPC error envelope. The endpoint sets no CORS headers, so no currently-working browser flow is affected. 2. serverInfo.title: 'Accounted' (MCP 2025-06-18 display name). name stays 'gnubok' — stable identifier clients may key state on. 3. export-docs-to-website.mts now also exports CONNECT_CLAUDE_MD to the gnubok-website repo, so docs.gnubok.se/connect-claude (the target of the canonical /docs/api redirect) stays in sync. Companion website PR: jakobwennberg/gnubok-website#1. Tests: new origin-guard.test.ts (10 tests — no-Origin pass-through, same-origin, preview host, proxy host via env, foreign/port-mismatch/ null/malformed rejection, 403 envelope, and per-method enforcement on the registered apiRoutes). Full MCP suite 295/295 green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a894af11d0 |
feat(mcp): tool titles + Connect-with-Claude docs for Connectors Directory readiness (P0-4) (#682)
* feat(mcp): tool titles + Connect-with-Claude docs for Connectors Directory readiness Add a top-level Tool.title (MCP spec 2025-06-18) to all 92 MCP tools and flow it through the tools/list serializer for the Claude Connectors Directory. Trim the ~10 longest tool descriptions toward 180-200 chars (semantics + agent hints preserved) to partly offset the added bytes, and raise the payload-size bench ceiling 32K → 36K with headroom reserved for upcoming Skatteverket tools. Ship a "Connect with Claude" docs page (OAuth 2.1 connector + npx gnubok-mcp stdio bridge, sample sandbox prompts, 10-minute reviewer test, support pointer), wire it into the docs nav, and add a README to the packages/gnubok-mcp npm bridge. Bump one categorized sandbox business expense above 4 000 kr so the VAT close-check demo surfaces a high-value receipt-less expense. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): restore six spaces lost in the description-trimming pass Review finding (Greptile P2): the trim deleted single spaces in six tool descriptions ("Stages foruser", "länkatill", "kundfordran(1510)", …), producing malformed text in tools/list. All six were pure space deletions with no token gain — restored verbatim. Swept all 92 descriptions for residual merge artifacts; the two remaining matches are pre-existing function-call notation from main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(compliance): correct BFL citation in seed + mark 4000 kr as app heuristic Swedish compliance review findings: - The seed comment cited BFL 5 kap 6§ as requiring verifikat on high-value affärshändelser; 6§ governs verifikationens innehåll. Rephrased: BFL 5 kap 6-7§ require every affärshändelse to be documented — the 4 000 kr cut-off is the tool's own heuristic, not a statutory threshold. - The connect-claude docs read as though 4 000 kr were a legal floor (conflatable with the förenklad faktura ceiling, ML 17 kap 26-28§). Clarified that BFL requires underlag regardless of amount. Dismissed as by-design: the gnubok_sk_test_... README placeholders (standard practice; the prefix schema is already public in llms.txt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0bc81d4c88 |
feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools (P0-3) (#681)
* feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools Segregation of duties on API keys is now warn + explicit acknowledgement (not block): minting a key with any staging write scope AND pending_operations:approve returns 409 API_KEY_SOD_CONFLICT unless the caller re-POSTs with acknowledge_sod: true. The acknowledgement is recorded (sod_acknowledged_at / sod_acknowledged_by) for an auditable risk acceptance (ISO 27001:2022 A.5.3 / BFNAR 2013:2). The create UI surfaces an inline warning and an explicit confirm dialog before submitting the ack — the default "all scopes ticked" create routes through that path. Also introduces the agent:write scope and maps the previously-UNMAPPED memory tools gnubok_remember_fact / gnubok_forget_fact to it. Because unmapped tools were callable by any key, the migration grandfathers agent:write onto every existing non-revoked key with an explicit scope list so nothing regresses; new keys must opt in. agent:write is deliberately excluded from the default grants and is NOT a staging scope (no SoD conflict with approve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): enforce both-or-neither on the SoD acknowledgement pair Review finding (Greptile P2): sod_acknowledged_at/sod_acknowledged_by were independently nullable, so a partial write could silently pass and undermine the auditable risk acceptance (ISO 27001 A.5.3 / SOC 2 CC6.1). Adds a paired-NULL CHECK constraint + pg-real coverage for both partial-write directions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth)+feat(auth): compliance-review round — self-attestation documented, ack logged, SoD boundary assumption captured - Migration header now states explicitly that the SoD acknowledgement is a SELF-attestation by deliberate design (enskild firma has no second person; the claude.ai approval flow needs stage+approve on one credential) — the control objective is informed consent + audit record, not dual control. - The acknowledge_sod=true path now emits a structured log.warn (api_key.sod_acknowledged with key id/prefix, conflicting scope, scopes, acknowledger, company) so the acceptance lands in the logging pipeline in addition to the sod_acknowledged_* columns (ASVS V16.1.1). - STAGING_SCOPES carries the documented system control (BFNAR 2013:2 systemdokumentation) for why agent:write is not a staging scope: memory tools write advisory agent context and cannot stage räkenskapsinformation. Dismissed as by-design/verified: hard-block and second-approver remediations (user decision: warn + acknowledge); scope-update gap (the [id] route only supports DELETE — scopes are immutable post-creation); session-auth concern (withRouteContext is cookie+MFA only; API-key auth exists only on /api/v1 and MCP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-trigger CI (Supabase Preview 502 infra hiccup) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8d2ff61599 |
feat(bookkeeping): agent attribution into the immutable ledger layer (P0-1) (#678)
* feat(bookkeeping): agent attribution into the immutable ledger layer Close the three attribution gaps left after 20260618120001 (which made commit_method record 'api_key' for MCP-relayed approvals): - journal_entries gains nullable committed_actor_type/committed_actor_label, stamped by commit_journal_entry in the same draft->posted UPDATE that writes commit_method. The RPC gains p_actor_type/p_actor_label (DEFAULT NULL; prior signature dropped first to avoid PostgREST overload ambiguity, same technique as 20260421140000). - write_audit_log now populates audit_log.actor_type/actor_label from transaction-local gnubok.actor_* GUCs set by the RPC (the established gnubok.allow_delete pattern). Unset GUCs COALESCE to 'user' — byte- identical to the column's previous effective DEFAULT for every pre-existing write path. - commitPendingOperation accepts opts.actor and runs the entire executor inside an AsyncLocalStorage runWithActor() scope read by commitEntry(), so EVERY journal commit an operation makes is attributed — closing the documented "commitMethod only reaches create_voucher" gap. MCP approve passes the api_key actor + key label; web single/bulk approve pass the user + email. Known limitation (documented): reverseEntry posts reversal vouchers via direct PostgREST writes, not the commit RPC — reversals keep NULL attribution until that path is RPC-ified (follow-up). pg-real coverage: lib/bookkeeping/__tests__/commit-actor.pg.test.ts (RPC param stamping, audit GUC read, transaction-locality, CHECK rejection, immutability of the new columns, single-signature guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): split actor-context so client bundles never see node:async_hooks CI core-only build failed: engine.ts is reachable from client component bundles (invoices/[id] page), and the static node:async_hooks import in actor-context.ts cannot be chunked for the browser. Split the module: - actor-context.ts (isomorphic): CommitActor type + a storage registry + getActor(). In a client bundle the registry stays empty and getActor() returns undefined — identical to the server-side no-scope default. - actor-context-node.ts (server-only): owns the AsyncLocalStorage, binds it into the registry on import, exports runWithActor(). Imported only by the approval paths (commit.ts), which are never client-reachable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bc61862e76 |
feat(agent): telemetry + CI-gate quick wins from the "AI systems that ship" audit (#677)
* feat(agent): telemetry completeness + durability, CI gates, commit_method provenance Quick wins from the "Building AI systems that ship" audit: - mcp.tool_called gains errorMessage (message_sv, truncated 500 chars) on all failure exits; new mcp.skill_loaded event on every gnubok_load_skill (all tiers) so atom usage is finally measurable - event_log: (event_type, created_at) index; cleanup cron keeps mcp.*/agent.* telemetry 180 days (delivery events stay 30) - CI: lint ratchet (npm run check:lint — 60 legacy errors baselined, fails only on NEW errors) and a pg-real coverage gate (migrations touching trigger/RPC/RLS/DEFERRABLE require a *.pg.test.ts change; escape hatch: -- pg-test: covered-by/skip) - journal_entries.commit_method CHECK widened with 'api_key'/'agent'; the MCP approve path records 'api_key' truthfully instead of 'user_accept' (agent_first_vision §8 P0-1). 'agent' is reserved — ALL MCP traffic (incl. claude.ai OAuth, whose access_token is a minted API key) authenticates as api_key today Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(import): derive opening balances from prior-year #UB when SIE lacks #IB (#675) SIE files exported without #IB 0 rows (only #UB -1) previously imported with zero opening balances. getEffectiveOpeningBalances() now derives IB from prior-year UB for balance-sheet accounts when explicit #IB is absent, surfaces the derivation as an info issue in the import preview, and excludes share-capital vouchers from opening-balance detection. Detection regexes are shared between parser and importer so the two checks cannot drift. 507 lib/import tests pass. (Authored in a parallel session in this checkout; included per request.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): address PR #677 bot findings — RoPA entry, execFileSync, gate scope note Triage of the compliance-swarm + Greptile findings: Applied: - .compliance/ropa.yaml: new mcp.telemetry processing activity declaring the 180-day mcp.*/agent.* retention, lawful basis, data categories, and the no-args/no-results minimisation (ISO A.8.10, GDPR Art.5(1)(c) — the retention split is now formally documented, referenced from the cron) - check-pg-test-coverage.mjs: execFileSync with argv array — no shell, so a hostile base-ref can't inject (ASVS V13.2.1); verified an injection attempt exits 2 without executing - check-pg-test-coverage.mjs: documented the PR-level (not per-migration) scope of the gate so reviewers know to check coverage per migration when a PR carries several risky migrations (Greptile P2) Acknowledged, no change: - errorMessage PII risk: messages are domain-mapped strings; event_log already persists far richer delivery payloads under the same RLS; now declared in ropa.yaml - cron error envelope: errorResponse maps to the canonical safe envelope and the endpoint is CRON_SECRET-gated - two-pass delete "partial state": TTL deletes are idempotent — the next daily run sweeps whatever a failed pass left behind - skill_loaded actorLabel/sessionId: mirrors the pre-existing mcp.tool_called payload; sessionId is the join key the analytics exist for Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7cd1b86e7 |
fix(import): preserve customized SIE #KONTO account names (#669)
* feat(import): add syncMappedAccounts helper for account create + rename Single home for the create-missing-accounts logic that exists in three near-identical copies (executeSIEImport, the SIE execute route, and the arcim-migration extension), plus a new rename pass that carries customized SIE #KONTO names into accounts that already exist (e.g. K1-seeded defaults). The file's name applies only to identity mappings (source === target); remapped targets keep their BAS/current name. With updateAccountNames=false the behavior matches the legacy code exactly. Not wired up yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): preserve SIE #KONTO account names; add updateAccountNames option Customer report: account names customized in Fortnox did not follow into Accounted via SIE import. The import always used BAS default names for accounts in the BAS reference and never touched accounts that already existed (the K1-seeded chart), so the file's names were silently dropped. executeSIEImport now routes account creation through syncMappedAccounts, which prefers the file's #KONTO name for identity-mapped accounts and renames existing accounts whose name differs (surfaced as a warning). New option updateAccountNames (default true) restores the old behavior when disabled. The duplicated pre-create blocks in the execute route and the arcim-migration extension are removed — executeSIEImport owns account sync on every path now, including the Fortnox re-sync (idempotent renames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(mcp): expose update_account_names on gnubok_import_sie Optional boolean on the tool schema, staged into the pending operation and threaded through commitImportSie to executeSIEImport. Defaults to true at both stage and commit time — the commit-side default also covers operations staged before the param existed (Boolean(undefined) would have silently flipped it off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): v1 SIE import generated no account mappings The route passed [] as mappings to executeSIEImport, which the mapping-coverage guard (added in #613) rejects for any real file — and before that guard, every voucher was silently skipped as unmapped. The route has never produced a working import for files with vouchers. Generate mappings server-side from the file's #KONTO records plus stored per-company overrides (same as the dashboard execute route), reject unmappable files with a clean 400 before the operation row is created, and expose options.updateAccountNames (default true). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(import): "Använd kontonamn från filen" toggle in import review step New switch (default on) controlling whether the SIE file's #KONTO names are carried into the chart of accounts. Helper text shows how many identity-mapped accounts carry names that differ from the BAS defaults. The page already serializes the whole options object to the execute route, so no further wiring is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): address PR #669 review — parallel renames, rename audit trail - Rename pass now runs UPDATEs concurrently in bounded batches of 25 (greptile P2): a re-sync with many custom names no longer serializes N round trips, and a pathological full-chart rename cannot stampede the API. Per-rename failures stay non-fatal via Promise.allSettled. - Persist the per-account rename detail (number, from, to) into sie_imports.migration_documentation as accountRenames — the behandlingshistorik record per BFNAR 2013:2 (swedish-compliance review); the result warnings only carry the count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f6ee0c2a82 |
Bug/customer invoice bug (#628)
* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift. Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(vat): report yearly VAT over the rakenskapsar, not the calendar year Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migration): resolve supplier invoice status from payment amounts The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enable-banking): only ingest booked transactions to stop re-import drift Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(gitignore): ignore local SIE test fixtures tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback feat(tests): add test for reverse charge rate handling on supplier invoice line items feat(fortnox): ensure paid status reflects zero balance for fully paid invoices chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
13be0c569a |
feat(mcp): expose multi-tx RPCs (match_batch_allocate + bulk_book_transactions) (#614)
* feat(bulk-book): manual booking mode + document inheritance Two pieces of user feedback from PR #606: 1. "How come it is only mallar? Is it not possible to have manuell bokfoering?" - BulkBookDialog was template-only. Added a Tabs primitive with Mall / Manuell tabs. Manual tab pre-fills lines from the selected txs (one line per tx on 1930 + counterparty placeholder on 3001/5800 by direction), then the user edits Konto / Debet / Kredit / Beskrivning. Live balance + bank-leg checks drive the confirm button - same invariants the RPC enforces server-side. 2. "Documents attached does not follow into the bookkeeping. And if there are two different documents attached, none of them follow." The bulk_book_transactions RPC now propagates each tx's document onto the target verifikat (new in Branch B, existing in Branch A) as verifikationsunderlag. Per BFL 5 kap 6§ + BFNAR 2013:2 kap 4 a verifikat may have multiple underlag; every receipt that justified a tx is now retention-protected on the combined entry. The dialog shows a small count chip ("N bilagor foeljer med") so the user sees what will inherit. Also dropped p_user_id from the RPC signature (round-3 hardening pattern applied consistently across all multi-tx RPCs after PR #607). Caller resolves from auth.uid() inside the function. Schema: BulkBookSchema is now a 3-way XOR (existing_journal_entry_id | template_id+mode | manual_lines), with manual_lines validated as accountNumber + nonNegativeAmount per line. pg-real tests: - doc inheritance into a new combined verifikat (mixed: 2 of 3 txs have docs - docs_linked should be 2, not 3) - doc inheritance into an existing posted verifikat (link branch) - manual lines path (no template expansion artifacts in the resulting JE - just the 2 user lines) - unbalanced manual lines still rejected by BULK_BOOK_UNBALANCED Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 review - pg-real signature, account allowlist, account-number validity Three review findings on PR #610: 1. pg-real failure: 2 link-existing tests still used 5-arg SELECT bulk_book_transactions($1::uuid[], $2, $3, $4, $5) after the userId removal. My earlier replace_all caught only the patterns that had ::jsonb on $3; the link-existing tests pass null for new_entry and used a bare $3 so they slipped through. (Greptile P1) 2. Manual lines bypassed chart_of_accounts validation. A typo or adversarial caller could post to a BAS account that doesn't exist in this company's chart, corrupting the hauptbok and breaking SIE export. Both compliance-swarm (OWASP V2.3) and swedish-compliance flagged this. Added a single-roundtrip allowlist check in the route: query chart_of_accounts for distinct account_numbers in manual_lines and reject with BULK_BOOK_INVALID_ACCOUNT if any are missing or inactive. 3. UI canConfirm guard missed invalid account numbers. Account input allows 1-3 digits and JS string comparison '193' >= '1900' is false, so a 3-digit entry escapes bankLineNet, the bank match could pass via other lines, and the server returned 400 only after submit. Added previewLines.every(l => /^\d{4}$/.test(l.account_number)) to canConfirm so the Confirm button stays disabled inline. (Greptile P2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 round 2 - RPC chart-of-accounts, doc tenant isolation, GRANTs Seven compliance findings from the round-1 bot reviews: Migration (20260602121000_bulk_book_round2_fixes.sql): - RPC chart-of-accounts allowlist (defense-in-depth): every line in p_new_entry.lines is now verified to be an active BAS account for p_company_id. Closes the gap where the template branch and direct DB callers (psql, future MCP) bypassed the route's manual-branch check. Returns BULK_BOOK_INVALID_ACCOUNT with the offending list. (OWASP V8.2.1 + SOC 2 CC6.3) - Document inheritance CTE: added "AND d.company_id = p_company_id" to the UPDATE join so the tenant isolation is enforced on both sides (tx + doc), not just the tx side. Four bots converged on this finding (V1.2.5, A.8.2, CC6.6, swedish-compliance). - Bank-leg range check: "length(account_number) = 4 AND account_number BETWEEN '1900' AND '1999'" replaces the bare lexicographic comparison. Lexicographic-on-4-digit is safe today; the length guard is defense-in-depth against schema drift. (swedish-compliance) - Explicit role grants: REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated on both bulk_book_transactions and match_batch_allocate. (SOC 2 CC6.1) UI (BulkBookDialog): - Manual-mode prefill no longer suggests a hardcoded 3001/5800 counterpart. Reason (swedish-compliance): a user accepting the prefill could submit a verifikat with no VAT line (26xx), under-reporting utgaaende moms. The bank side stays pre-filled (unambiguous); the counterpart row scaffolds blank for the user to choose. Schema (BulkBookSchema): - manual_lines.debit_amount + credit_amount bounded at 99,999,999 SEK per line. Catches typos before the RPC. (compliance-swarm V4.5) i18n: - docs_inherit_hint terminology: "bilaga" -> "verifikationsunderlag" and an explicit "sparas i 7 ar enligt BFL 7 kap" reminder. swedish-compliance flagged that "bilaga" risks users treating the files as deletable attachments rather than retention-bound raekenskapsinformation. Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): seed chart_of_accounts in bulk-book pg-real seedTenant The round-2 RPC fix added a chart_of_accounts allowlist check inside bulk_book_transactions, but the test fixtures don't seed COA — so every existing test that submits lines (1930, 3001, 2611, etc.) now returns BULK_BOOK_INVALID_ACCOUNT instead of the expected error code. Seed the 8 accounts the suite actually uses directly in seedTenant (cheaper than calling seed_chart_of_accounts which inserts the full BAS 2026 chart). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): expose match_batch_allocate + bulk_book_transactions as MCP tools Surfaces the multi-tx flows shipped in PRs #603/#606/#608/#610 so Claude Desktop/Code can drive them via chat. - migration 20260603120000: expand pending_operations.operation_type CHECK to include match_batch_allocate, bulk_book_transactions, plus undo_sie_import (which was missing from prior expansions despite being wired in risk-tiers.ts and the commit dispatcher). - types/index.ts: extend PendingOperationType. - lib/pending-operations/risk-tiers.ts: match_batch_allocate = medium (same tier as single-tx match), bulk_book_transactions = high (creates a verifikat with arbitrary lines, same surface as create_voucher). - lib/pending-operations/commit.ts: thin commit handlers that call the SQL RPCs and translate the structured error envelope. The RPCs themselves do all the locking, balance checks, JE creation, voucher number, payment/junction rows, and doc inheritance. - extensions/general/mcp-server/server.ts: two new tool definitions. Both stage via stagePendingOperation with period_status hint and pre-validate inputs (direction, sum-equals-tx-abs, same-date, not-already-booked) so the agent gets a clear error inline before the RPC runs. - payload-size.bench: bump from 30K to 31K tokens (with rationale). Two new tools earn the bump; descriptions already trimmed to fit the <=280-char description limit. Migration applied to remote and version aligned with local filename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 review - allocation guard, IDOR pre-check, currency + JE-date Round-1 review fixes on PR #614: - Greptile P1: per-allocation invoice_id / supplier_invoice_id guard. The inputSchema marks both as optional (they're mutually exclusive by kind), so JSON Schema can't express "X required iff Y=A". Added explicit check in the execute handler: customer_invoice rows must carry invoice_id; supplier_invoice rows must carry supplier_invoice_id. - OWASP V8.2.1: IDOR pre-check on match_batch_allocate. Verify every invoice / supplier_invoice referenced in the allocations belongs to this company BEFORE staging. The RPC re-checks (BATCH_INVOICE_NOT_FOUND), but failing fast at the MCP layer gives the agent a clear error. - OWASP V8.2.1: same pre-check on bulk_book_transactions for existing_journal_entry_id. Fetches the JE at stage time, verifies status=posted and company_id, throws if not found. - swedish-compliance: currency homogeneity check on bulk_book. Mixed SEK + EUR in one samlingsverifikat violates BFL 5 kap 6§ st 3 motpart clarity. Cross-currency batches go through match_batch_allocate instead (which handles FX diff on 7960/3960). - swedish-compliance: period-lock check on the link-existing branch now uses MAX(tx_date, JE.entry_date), not just tx_date. Otherwise a tx in an open period could attach to a verifikat in a locked period and the guard would miss it. - A.8.11 + CC7.2: sanitised RPC error logging. log.error now emits only { code, message } instead of the full error object — error.details can echo invoice IDs, amounts, and counterparty identifiers. Not actioned (PR-comment, no code change): - V2.3 double-validation in commit handler — RPC enforces balance, accounts, bank-leg via the chart_of_accounts allowlist (PR #610 round 2). Commit handler is a thin pass-through by design. - A.8.2 step-up approval for high-tier ops — architectural change affecting all high-tier ops, not PR-scoped. - V2.4 rate limiting on bulk endpoints — platform-level concern. - 0.005 epsilon / account-class allowlist — pre-existing patterns. - undo_sie_import storno requirement — separate RPC, this PR only backfilled the missing CHECK constraint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 2 - trust-boundary comments + balance pre-check + audit log Round-2 review fixes (compliance-swarm went 14 -> 9 after round 1; remaining HIGHs are all "do the same tenant check at multiple layers"). The bot itself offers the alternative: "or document and reference the specific RPC line that enforces this." Following that. - commit.ts: trust-boundary comment blocks on both commitMatchBatchAllocate and commitBulkBookTransactions, citing the exact RPC + migration where tenant isolation + chart_of_accounts allowlist are enforced authoritatively. The commit handler stays a thin pass-through by design; re-querying would triple the same check without adding security. (V8.2.1, A.8.2) - commit.ts: structured success-path log.info() on both handlers with companyId, operationType, journal_entry_id, and tx count. No raw amounts or IDs that could echo PII. (V16) - server.ts: balance pre-check on bulk_book create-new path. RPC enforces BULK_BOOK_UNBALANCED authoritatively, but failing fast at staging gives the agent a clear error before pending_operations is even touched. (V2.3 / swedish-compliance) Not actioned this round: - V2.2 oneOf/if-then-else in JSON Schema for mutual exclusivity — JSON Schema vocabulary support is shaky across MCP clients; runtime check in execute() is the canonical pattern across the existing toolset. - CC6.1 generic error string to caller — RPC error codes are user-actionable (BULK_BOOK_UNBALANCED, BATCH_INVOICE_NOT_FOUND); a generic string would degrade UX. - CC7.2 audit RPC RAISE messages for PII — separate audit; not PR-scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 3 — last 5 LOWs + salary_run/agi constraint backfill Compliance-swarm went 14 → 9 → 5 (all LOW). Cleaning the last 5 + the swedish-compliance findings. - migration 20260603121000: backfill create_salary_run + generate_agi into pending_operations.operation_type CHECK. Both have risk-tier entries and commit executors but were never added (same bug class as undo_sie_import). Production has no rows of either type today. (swedish-compliance) - server.ts: Number.isFinite guard in bulk_book balance pre-check. Number(x) || 0 silently treats NaN as 0 — a malformed amount could pass the balance check by accident. (compliance-swarm A.8.28) - server.ts: count-equality + missing-set assertion in match_batch_allocate tenant pre-check. Belt-and-suspenders so a null/undefined row in the Supabase JSON response can't pass silently. Same pattern on both invoice and supplier_invoice branches. (CC6.1) - server.ts: fix BFL paragraph citation in currency-homogeneity comment. Was "BFL 5 kap 6§ st 3", should be "BFL 5 kap 2§" (SEK denomination) read with 5 kap 6§ (valutakurs). (swedish-compliance) - server.ts: clarify 0.005 tolerance comment — it's for floating-point equalisation only, not a rounding allowance. RPC enforces exact balance to the öre. (swedish-compliance) - commit.ts: expand audit-log txId comment — included intentionally for trail-to-source join, scoped to companyId already logged. (compliance-swarm A.8.15/CC7.2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 4 — Swedish plural typo + balance comment parity + agent-routing hint Round-3 review caught: - swedish-compliance: \`kundfakturaor\` typo (real räkenskapsinformation defect under BFL 5 kap 7§). Swedish plural for \`kundfaktura\` is \`kundfakturor\` (drop the final \`a\`, add \`or\`), same for \`leverantörsfaktura\` → \`leverantörsfakturor\`. Fixed via slice(-1) + 'or'. - swarm A.8.28: match_batch_allocate balance tolerance check was missing the equivalent "RPC enforces exact balance" comment that bulk_book has. Added. - swedish-compliance: currency-mismatch error message now routes the agent to gnubok_match_batch_allocate for cross-currency allocations instead of letting it retry with hand-built FX lines. Not actioned (out of pattern / out of scope): - Integer arithmetic for balance checks (codebase pattern is float + epsilon; would diverge from match_batch_allocate, supplier-payment, invoice-payment, etc.) - DSD docs / runbook for txId-in-log and stripped-error.details trade-offs (out of PR scope; tracked separately) - Link-existing target verifikat description match (architectural; every link-existing op would need this) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): expose link_transaction_to_journal_entry as MCP tool The REST endpoint /api/transactions/[id]/link-journal-entry already lets the duplicate-payment UI attach a bank tx to an already-posted verifikat without creating new bookkeeping. Agents had no equivalent — closing that parity gap so users on Claude can match bank txs against vouchers they booked manually. The core link logic moves to lib/transactions/link-journal-entry.ts so both the REST route and the new commit handler share one implementation (preserves all structured-error codes, optimistic-lock invoice update, and compensating rollback). New 'link_transaction_journal_entry' op type wired through the risk tiers (medium), TOOL_SCOPE_MAP (transactions:write), and dispatcher. Bumps the tools/list payload-size ceiling 31K → 31.5K — same family bump PRs #603/#606 made when adding match_batch_allocate / bulk_book_transactions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 5 — bot findings on link_transaction_journal_entry Addresses the swedish-compliance + compliance-swarm findings on commit 5b884c3a: 1. **CHECK constraint backfill** — new migration adding 'link_transaction_journal_entry' to pending_operations.operation_type. Same bug class as the salary_run/agi backfill in 20260603121000; without it, every staged op would be rejected silently in production (BFL 5 kap 6–7§ audit-trail gap). 2. **Payment-date exchange rate** — invoice_payments.exchange_rate now uses transaction.exchange_rate (rate on payment date) instead of invoice.exchange_rate (rate on invoice date), per BFL 5 kap 2§ + ML 8 kap 21–23§. The full 3960/7960 posting still belongs to createInvoicePaymentJournalEntry by contract — this path only links to an EXISTING verifikat. 3. **voucherLabel format centralized** — exported formatVoucherLabel helper returns the canonical `A-12` format (with hyphen, matches gnubok_link_invoice_to_voucher and SIE #VER cross-references). Both the MCP staging preview and the committed service result import it, so the user can't approve one label and have a different one land in the audit trail. 4. **Rollback warn log restored** — txLog.warn-equivalent (IDs only, no PII) when the compensating rollback itself fails, surfacing partial-state gaps for reconciliation per GDPR Art.5(1)(f) / SOC 2 CC7.2. Lost in the refactor that extracted the shared service; now present in both rollback call sites. 5. **Commit-layer log.info** — structured success log mirroring commitMatchBatchAllocate / commitBulkBookTransactions (companyId, tx/JE IDs, settledInvoice boolean). No raw amounts or counterparty names. 6. **Data minimization on invoice fetch** — explicit column list replaces select('*, customer:customers(name)') in the shared service; the MCP staging pre-check now fetches only invoice_number + remaining_amount (drops total + paid_amount). voucher_description omitted from preview_data per Art.25. Test impact: existing route + dispatcher tests updated to expect `A-12` instead of `A12`. All 4308 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): correct FX bookkeeping + UI for match-invoice flow User report: matching a 230 SEK bank tx against a 140 USD invoice produced 1930 Dr 2 142,50 / 1510 Cr 2 142,50 — fictitious numbers that didn't match either the bank receipt or the booked AR. Root cause: the preview route called resolveSekAmount(tx.amount, null, INV.currency, INV.rate), treating the SEK tx number as if it were in the invoice's currency and multiplying by the invoice's stored rate. Both the preview and the commit then used the bogus number on both legs and silently dropped the FX gain/loss. A second issue surfaced in the same dialog: for a 1 250 SEK invoice with a prior 230 SEK partial, the comparison row showed "Differens: 250 kr" (off the original total) instead of "20 kr" (off the actual 1 020 kr remaining). This patch: 1. **New shared helper** lib/bookkeeping/invoice-payment-lines.ts - buildInvoicePaymentClearingLines(tx, invoice, description) → bank-leg, AR-leg, fx-diff, and a balanced line array. Bank-leg is always the actual SEK that hit the bank (resolveSekAmount with the TX's currency context, honouring tx.amount_sek when set). AR-leg is the SEK value of the customer-debt reduction at the invoice's stored rate. Diff posts to 3960 (gain) or 7960 (loss) so the verifikat balances per BFL 5 kap 4–5§. Mirrors the match_batch_allocate RPC's contract: when the tx is cross-currency, the single match fully clears the invoice's remaining amount. 2. **Preview route** uses the helper for the clearing branch — replaces the buggy resolveSekAmount call. Now byte-identical to what commit builds. 3. **Match-invoice POST** uses the helper + createJournalEntry directly for the clearing path, bypassing createInvoicePaymentJournalEntry on this single flow. mark-paid and other callers of that function still work as before (full payment + caller-supplied exchangeRateDifference). 4. **InvoiceMatchDialog** compares the bank tx against invoice.remaining_amount (not invoice.total) for both customer and supplier branches; cross-currency dialogs now show the different- currencies warning instead of a meaningless numeric diff. The dialog's invoice card also displays remaining_amount. 8 new unit tests cover same-currency full/partial, cross-currency gain/loss, exact match (no FX line), sub-öre tolerance, and USD-on-USD with pre- populated amount_sek. All 4316 tests pass. Scope note: this expands PR #614 beyond the original "expose multi-tx RPCs as MCP tools" since the same FX bug class affected the new MCP tool too (round 5 already addressed the invoice_payments.exchange_rate side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 7 — CI build + 4 HIGH bot findings Core Build was failing on e29a0ba2/5e9d4c3d due to a TypeScript type-cast error in linkTransactionToJournalEntry. Plus the swedish-compliance review flagged four substantive bugs in my recent commits. 1. **TS build error** — `invoice = invoiceRow as typeof invoice` inferred `never` because the LHS type included `null`. Switched to a named `FetchedInvoice` alias and `as unknown as FetchedInvoice`. 2. **TOOL_SCOPE_MAP missing two write-capable tools** (🟠 HIGH OWASP V8.2.1). `gnubok_match_batch_allocate` and `gnubok_bulk_book_transactions` (added in PRs #603/#606) were never registered, meaning any API key could invoke them regardless of scope. Backfilled both with `transactions:write`. 3. **`paymentExchangeRate` fallback wrong-date rate** (swedish-compliance). `transaction.exchange_rate ?? invoice.exchange_rate ?? null` falls back to the INVOICE date's rate when the tx rate is null. Per ML 8 kap 21–23§ the payment row must record the PAYMENT-date rate. Removed the fallback — `null` is correct when the tx is SEK; downstream lookups can populate it lazily from Riksbanken if needed. 4. **Currency-mismatch corrupts paid_amount** (swedish-compliance). The link path was accumulating `tx.amount` into `invoice.paid_amount` without checking that the currencies matched. A 230 SEK tx applied to a USD invoice would record "230 USD paid" silently. Added explicit LINK_TX_INVOICE_CURRENCY_MISMATCH guard (400) — cross-currency settlement must go through the match-invoice flow which routes through buildInvoicePaymentClearingLines. 5. **Cross-currency PARTIAL overstates FX gain/loss** (swedish-compliance, BFL 5 kap 4–5§). `buildInvoicePaymentClearingLines` was crediting the FULL invoice remaining to 1510 on every cross-currency match — zeroing the GL balance while the invoice row stayed at status=partially_paid, and booking a fake huge FX diff to 3960/7960. Fix: only book FX-diff when `bankSek >= arSekFullRemaining`. Partials default to 1930 = 1510 = bankSek, deferring the FX adjustment to the final settlement (or to a manual mark-paid with explicit exchange_rate_difference). Documented the helper as customer-invoice- only (supplier-side has different DR/CR polarity and goes through match_batch_allocate RPC). Test impact: 1 helper test updated to match the defer-on-ambiguous-loss behavior, 1 new test covers the partial-defers-FX path explicitly. All 4317 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 8 — close out remaining bot findings CI green on round 7 (4 of 4 checks), HIGH count 2 → 1. Round-8 closes the remaining HIGH and the smaller doc/guard items. 1. **PI1.3 risk acknowledgment restored** (SOC 2 HIGH). The shared rollbackTxLink helper already had warn-level logging on rollback failure, but the explicit PI1.3 reference comment from the original route was lost in the refactor. Added inline so the reconciliation- gap risk is visible to future maintainers. 2. **MCP currency-mismatch pre-stage check.** gnubok_link_transaction_to_ journal_entry now fetches invoice.currency and rejects cross-currency matches before staging, saving the user an approval round-trip when the commit handler's LINK_TX_INVOICE_CURRENCY_MISMATCH guard would fire anyway. 3. **fxDiffSek JSDoc clarified.** The sign convention (positive = loss, negative = gain) is correct for verifikat balancing but counter- intuitive at a P&L glance. Documented explicitly + pointed callers needing a "gain" number at `bankSek - arSek`. 4. **Reject both invoice_id + supplier_invoice_id** on the same match_batch_allocate row (V4.5). Extra IDs previously leaked into preview_data silently. 5. **Reject zero-amount tx** in bulk_book_transactions direction guard (A.8.28). A txs[0].amount === 0 would have mis-classified the batch as 'expense'. Mirrors the existing guard in match_batch_allocate. 6. **Reject debit=0 && credit=0 lines** in bulk_book new_entry (BFL 5 kap 6§ — every verifikat line must represent a real bokföringspost with a non-zero amount). 7. **Data-minimization comments** added on the match-invoice preview route (amount_sek + exchange_rate fetch is for the FX-fix bank-leg math) and on the bulk_book_transactions preview_data block (aggregate counts only — no per-tx PII). Mirrors the pattern already documented on gnubok_link_transaction_to_journal_entry. Skipped: - 1510 vs 1515 (osäkra kundfordringar) — future improvement, needs reading the original invoice JE's account, not a single-tool fix. - transaction_description PII masking in preview_data — needs product call on the truncation strategy and would degrade approval-UX. - "invoice.match_confirmed event removed" finding — false positive; the event is emitted at lib/transactions/link-journal-entry.ts:270-280. All 4317 tests pass; payload-size guard still under ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): PR #614 round 9 — block cross-currency in single match-invoice path Closes the swedish-compliance finding from round-8 review: a SEK bank tx matched against a USD invoice through /api/transactions/[id]/match-invoice would silently corrupt invoice.paid_amount (accumulator treats SEK as USD) and flip a 140 USD invoice to status='paid' after a tiny partial. The round-6/7 FX fix corrected the JOURNAL ENTRY lines but the invoice STATE update still ran the same broken accumulator. Proper cross-currency settlement on this path requires converting tx.amount to invoice.currency at the bank-date rate AND storing invoice_payments rows with the right (amount, currency) pair. That's a larger design call that belongs in its own PR. This change blocks cross-currency on the single-allocation path: - New MATCH_INVOICE_CURRENCY_MISMATCH structured error (400, bilingual) - Same-currency check inserted right after MATCH_INVOICE_NOT_OPEN - Mirrors the LINK_TX_INVOICE_CURRENCY_MISMATCH guard added to the link path in round-7 - Routes the user to the multi-allocation flow (gnubok_match_batch_allocate) which DOES handle 3960/7960 FX-diff postings end-to-end Same-currency (SEK→SEK or USD→USD) remains fully supported including partials; the buildInvoicePaymentClearingLines helper handles those correctly. For SEK tx → USD invoice the user now gets a clean 400 error pointing at the right flow, instead of silently corrupted ledger state. 1 new route test covers the guard. All 4318 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ea1bf01f1e |
Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6629964780 |
fix(mcp): unblock query_journal text search across line + entry descriptions (#605)
* fix(mcp): unblock query_journal text search across line+entry descriptions `gnubok_query_journal` issued a single PostgREST `.or()` filter mixing a base-table column (line_description on journal_entry_lines) with a column on the embedded inner-joined resource (journal_entries.description). PostgREST's flat comma OR syntax cannot span base and embedded resources and returned 400 "failed to parse logic tree" on every free-text search, so the agent could not look up history (e.g. "har vi bokfört Google innan?") via the MCP tool. Refactor: pull the common filter chain into a `buildBaseQuery()` helper and issue two parallel `.ilike()` queries — one on `line_description`, one on `journal_entries.description` — then merge by line id, re-sort, and slice to `limit`. Same pattern as `lib/invoices/duplicate-payment-candidates.ts`, with the same rationale (LIKE-DSL injection risk + cross-embed unsupported). Drops the obsolete comma-stripping pre-processing on the search term (only needed because the value was being injected into PostgREST's OR DSL); LIKE wildcard escaping (`%` and `_`) is preserved. Adds four tests covering: merge across both legs, dedup of cross-leg overlap, correct columns + escaped pattern sent to `.ilike`, and LIKE-wildcard escape end-to-end. Existing amount-filter / accounts-cap / truncation tests unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): query_journal PR review — honest truncation, wider leg, error sanitisation Address PR #605 review (Greptile + compliance swarm). - truncated math: previously dbMatched = byLine.count + byEntry.count inflated the counter every time a row matched both legs (e.g. an entry whose line description AND header description both contain "Google"). total_lines was off by up to 2× and truncated was forced true even when every distinct match was already returned, sending the agent into unnecessary narrow-and-retry loops. dbMatched is now merged.size — the honest distinct-row count among what we fetched. A new legCapHit signal drives truncated when either leg's fetch window filled, so we never lie the other direction either. - per-leg fetch widened to 2 × limit. Previously each leg was capped at `limit` independently, so when one column was much more selective than the other (e.g. 150 line matches vs 5 entry matches), the merge could drop globally-ranked rows from the chronologically newer tail. The final slice still caps at `limit`; the wider per-leg window just gives the merge a better tail to choose from. - text input length cap (200 chars). Defence-in-depth against pathological inputs even though .ilike() parameterises the value (compliance A.8.28). - error sanitisation in the text-search path. Raw PostgREST messages can surface internal schema details (table names, constraint names) to the caller. We now log details server-side via the existing module logger and throw a generic message (compliance V16). - comma-stripping rationale documented inline so the next reviewer doesn't re-add it. The previous defence was needed because the value was injected into PostgREST's OR DSL where `,` is the separator; the parameterised .ilike() path treats `,` as a literal and stripping it would mangle real-comma searches (compliance V1.2.5). Adds 4 regression tests: overlap-doesn't-falsely-truncate, leg-cap-hit flags truncated, text>200 rejected, raw schema name never leaked. All 14 query_journal tests pass; 266/266 MCP suite green; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): query_journal PR review round 2 — schema guards, leg cap, audit log Address compliance swarm findings on commit 3dfab46c. - text input: add `maxLength: 200` to the JSON Schema so the limit is enforced at the boundary by the MCP runtime, not only inside execute(). Inline guard kept as belt-and-suspenders (ASVS V2.2, ISO 27001 A.8.28). - limit input: add `minimum: 1, maximum: 500` to the JSON Schema (matches the existing Math.min cap inside execute()) so the bound is visible to agents and enforced declaratively. - legLimit hard cap: `Math.min(limit * 2, 500)`. Previously legLimit scaled with limit unbounded, so a maxed-out limit triggered 2×1000-row fetches. Cap is independent of the caller-supplied limit (ASVS V2.3). - audit log: include `userId` alongside `companyId` in both log.warn payloads so failed-query events can be correlated to the actor for incident investigation (ASVS V16, SOC 2 CC7.2). - new test: assert both parallel ilike legs in the text-search path issue `.eq('journal_entries.company_id', companyId)` — defence-in-depth against a future refactor accidentally dropping tenant scoping from one leg (SOC 2 CC6.1). Not addressed (with rationale): - ASVS V1.2.5 / SOC 2 CC7.2 "log.warn body contains raw PostgREST error text". The swedish-accounting-compliance reviewer explicitly cleared this on the same commit ("the schema-leak is plugged in both paths"). The thrown error is generic; raw detail lives only in internal structured logs (Sentry, etc.) where it is needed for incident triage. Same pattern is used by 20+ other tools in this file; scrubbing only this one is inconsistent. Defer to a project-wide logger sanitiser. - Swedish reviewer's "reversed mixed with posted in status=all": false positive — `status` IS surfaced per line in the response shape (see the lines.map projection in server.ts). - Swedish reviewer's "debit XOR credit not enforced": insert-time concern, not a read-tool concern. Out of scope. Tests: 15 query_journal cases (4 new this round); 267 MCP suite; 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> |
||
|
|
ccdfed5fea |
feat: voucher linking, recovery ops, and salary overrides (#591)
* feat: voucher linking, recovery ops, and salary overrides Adds reversible/correction-style write paths that customers and agents have been asking for, plus per-run salary employee overrides. Invoice → voucher linking - POST /api/invoices/[id]/link-to-voucher and GET /api/invoices/[id]/voucher-candidates - lib/invoices/voucher-matching.ts with full + pg test coverage - LinkVoucherPicker UI in PaymentBookingDialog - pending_operations.operation_type expanded with link_invoice_voucher (medium risk) and a (journal_entry_id, invoice_id) unique guard - MCP: gnubok_find_voucher_candidates_for_invoice and gnubok_link_invoice_to_voucher tools SIE undo - POST /api/import/sie/[id]/undo + undo_sie_import RPC - sie_imports.status gains 'undone' - ImportResultStep surfaces the action; structured error SIE_UNDO_FAILED Edit-recreate journal entries - POST /api/bookkeeping/journal-entries/[id]/edit-recreate - Bookkeeping detail page wires it into the existing edit flow Delete-last-voucher clears IB link - Trigger + pg test ensure deleting the last voucher of a period nulls the opening_balance_journal_entry_id link so a re-import lands cleanly Salary employee overrides - salary_run_employees gains per-run override fields + migration - lib/salary/effective-values.ts centralises resolved values; all payslip, payment, AGI, KU, and booking routes read through it - SalaryOverridePanel on the employee detail page Account classifier - lib/bookkeeping/account-classifier.ts + tests; AddAccountDialog uses it - backfill-import-accounts script updated Misc - toast: minor styling tweak - AGI generate-declaration: respect effective values - structured-errors: new LINK_INVOICE_VOUCHER namespace Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add link_invoice_voucher operation type to pending_operations * feat: refactor salary run calculations and update error handling for SIE imports * fix: PR review feedback on voucher linking and SIE recovery pg-real (blocking): - tests/pg/delete-last-voucher-ib: drop posted_at = now() from the seed UPDATE — journal_entries has no posted_at column. - lib/invoices/__tests__/voucher-matching.pg: seed the posted voucher before closing the fiscal period so enforce_period_lock doesn't block the INSERT during setup. voucher-matching error codes and rollback: - Add LINK_VOUCHER_DB_ERROR (HTTP 500) and return it on real invoice UPDATE / payment INSERT failures. Previously these returned LINK_VOUCHER_VOUCHER_NOT_FOUND (404) which the pending-op dispatcher auto-rejects on transient DB errors. - Log rollback failures explicitly so an invoice left in a half-linked state (advanced status, no payment row) surfaces for manual reconciliation instead of disappearing silently. resyncNextPeriodOpeningBalance ordering: - Create the new IB first, relink the period FK, then storno the old IB. Previously the storno ran first; if createJournalEntry failed the next period was left with a reversed IB and nothing to replace it, and executeSIEImport swallows the error as a non-fatal warning. replace_period_opening_balance_link: - Tighten role check to owner/admin (was owner/admin/member). Matches delete_last_voucher and undo_sie_import. Data minimisation: - /api/invoices/[id]/voucher-candidates and the matching MCP tools now project only the invoice and customer fields the matcher reads, instead of returning the full customer row. Schema bounds: - SalaryEmployeeOverrideSchema caps each numeric override at 10 MSEK to catch typos before they reach the ledger or AGI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): supply user_id when seeding voucher_sequences voucher_sequences.user_id is NOT NULL (per the multi-tenant refactor in 20260330130000). The previous test seed only set company_id / fiscal_period_id / voucher_series, which made the seed fail with a constraint violation on the latest pg-real run. Pass the same userId used elsewhere in the seed helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): scope delete-last-voucher RPC assertions inside the tx withUserContext always ROLLBACKs, so any DELETE the RPC performs is discarded when the callback returns. The previous test then queried journal_entries via a fresh getPool() connection that only saw the pre-RPC committed seed state — hence "expected '1' to be '0'". Move every post-RPC assertion (entry count, period FK clear, opening_balances_set flip, audit log entry, sie_imports clear) inside the same withUserContext callback so they observe the uncommitted state before ROLLBACK fires. Also fix the sie_imports INSERT: the column is `filename`, not `file_name`, and `sie_type` is NOT NULL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): assert against the IB-marker audit row directly DELETE on journal_entries fires two audit_log writes: the generic write_audit_log() trigger row ("Deleted journal_entries record") and the delete_last_voucher RPC's explicit "(was period IB)" entry. Both land at the same statement_timestamp(), so ORDER BY created_at DESC LIMIT 1 returned the trigger row non-deterministically in CI. Switch to a presence check with a LIKE filter on the IB marker so the test verifies what it actually cares about — that the RPC's IB-aware audit row exists. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(db): set company_id on delete_last_voucher audit_log rows 20260528120000_delete_last_voucher_clears_ib_link.sql inserts directly into audit_log without setting company_id. audit_log's SELECT policy filters company_id IN user_company_ids(), so those rows landed with company_id=NULL and were invisible to every reader — only the generic write_audit_log() trigger row remained visible. That broke BFL audit- trail intent: the "(was period IB)" provenance row was never readable. Republish delete_last_voucher with p_company_id populated on both audit_log INSERTs (draft path and posted path). Behavior is otherwise unchanged; the pg-real test for the IB-clear flow now sees the RPC-written marker row as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Emil <emilmattsson14@gmail.com> |
||
|
|
f53725b20a |
Agent v1 bundle: TIC v2 onboarding, in-app assistant gating, sidebar nav, MCP fixes (#584)
* fix(sie-import): accept tab as field separator (Bollbok exports) The SIE 4 spec allows either space or tab between fields, but splitSIELine() only treated space (0x20) as a separator. Bollbok exports tab-separated lines for every record except #RAR, which silently swallowed all #IB / #UB / #KONTO / #KTYP / #VER / #TRANS records — imports appeared empty even though the file was well-formed. Also adds a parser-side diagnostic that emits a warning when raw #IB or #VER lines are present in the input but parsing produced none. The previous silent failure is how this bug stayed hidden; the warning gives the import preview something visible to surface next time. Verified against two real reproducer files (Sean / Erik Hellqvist): erik h 2025.SE (UTF-8): 166 accounts, 66 IB, 4 UB, 11 RES, 95 vouchers, 198 TRANS. erik h 2026.SE (CP437): 166 accounts, 66 IB, 4 UB, 0 vouchers. Both now parse with zero warnings/errors. Tests: + 8 Bollbok-shape tab-separated fixtures (2025 + 2026 quoting variants). + 4 silent-failure diagnostic-warning tests. All 74 sie-parser tests pass; 155/155 in lib/import; 64/64 downstream callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sie-import): address PR #513 review — strip #KTYP quotes, suppress redundant aggregate warning Two non-blocking P2 findings from Greptile review on PR #513: 1. #KTYP handler stored fields[2] directly, so Bollbok 2026 exports (#KTYP\t1510\t"T") stored '"T"' with literal quotes instead of 'T'. Latent defect — accountType is unused downstream today, but my tab- separator fix made the quoted-value path reachable. Now routes through parseStringField so both Bollbok 2025 (unquoted T) and 2026 (quoted "T") land as 'T'. 2. The aggregate "kontrollera fältavskiljare och teckenkodning" warning fired alongside per-record 'error'-severity issues for malformed #IB / #VER records, producing a misleading hint when the parser had already pinpointed the structural problem. Now suppressed when an error-severity issue with the same tag already exists. Test coverage: + accountType asserted to be 'T' (not '"T"') in both 2025 + 2026 shapes. + VER aggregate-warning test now uses #VER lines without { } blocks (silent loss, no per-record error) — the canonical case the diagnostic is designed for. + New suppression test: bare #VER produces per-record errors AND the aggregate warning is absent. 75/75 sie-parser tests pass; 156/156 in lib/import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip: agent chat + composer + memory + document extraction In-progress work on this branch beyond the SIE-import fixes: - Specialized accountant agent (composer + intents + chat loop) - Persistent agent_conversations/messages, agent_profiles, agent_memory - /chat surface + /onboarding/agent + /settings/agent-memory - document-extraction extension with status hooks - MCP server staging refactor + new skills (atoms, bank reconciliation, customer onboarding, kreditfaktura) - pending_operations rejection feedback (category + reason) + realtime - TIC company profile cached snapshot on companies - 17 migrations (all additive — see prior conversation analysis) Parked while branch waits for review/merge. Migrations are already applied to prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(tic): migrate company-data client from api-core v1 to Lens v2 Swaps the seven TIC company-data endpoints we call from the api-core paths (`/datasets/companies/{companyId}/...`, `/search/companies`) to the Lens equivalents (`/companies/{id}/...`, `/search-public/companies`). Hard cutover; proxy pattern preserved. Schema shifts handled inside the extension so consumers (TicWorkspace, Step2CompanyDetails) don't need changes: - `/companies/{id}/bank-accounts` now returns Bankgirot only — map to the existing `{ type, accountNumber, bic }` shape, drop terminated. - `/companies/{id}/industries` returns a discriminated array — filter to `companyIndustryCodeType === 'sni2007'` to preserve v1 behavior. - `/companies/{id}/phone-numbers` renamed the field to `phoneNumberFormatted` (fall back to `e164PhoneNumber`). - `/companies/{id}/documents` replaces `/financial-report-summaries`; filter `type === 'annualReport'` and read nested `financialReportMetadata` to rebuild the legacy summary shape. - `isCeased` is now a top-level boolean; `activityStatus` is an enum. Translate enum -> 'ceased' for the workspace's existing check. BankID identity flow (id.tic.io) is untouched — separate TIC product. Note: deploy gated on the TIC proxy being flipped to lens-api.tic.io with an `x-api-key` Lens key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(tic): expose v2 onboarding & workspace data Adds six new Lens (v2) fetchers on top of the migration that already landed in this branch, surfacing the data through /lookup and /profile. New fetchers in lib/tic-client.ts: - getFiscalYears /companies/{id}/fiscal-years - getAccountingPeriods /companies/{id}/accounting-periods - getPayrolls /companies/{id}/payrolls - getSignatory /companies/{id}/signatory - getRepresentatives /companies/{id}/representatives - getCompanyStatus /companies/{id}/status /lookup gains a fiscalYear field (current fiscal-year configuration) so onboarding Step 2 can skip manual MM-DD entry. CompanyLookupResult extended with optional fiscalYear; consumers without it keep working. /profile gains five new sections on TICCompanyProfile: - fiscalYear + fiscalYearHistory current + deduped period list - signatory firmateckning descriptions - board + representatives board-composition summary + active officers (positionEnd in future) - payrolls payroll2 array newest-first, with deviation vs annual-report - statuses current+historical status entries with red/yellow/green/neutral color TicWorkspace renders the new data as four cards (Status, Fiscal year + Signatory, Board + Representatives, Payroll history) plus a Badge mapping for the traffic-light status color. Tests: 52 -> 60 passing. Added unit tests for the new fetchers' v2 paths, fiscal-year auto-fill in /lookup, and full v2 profile coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding,agent): lean on TIC v2 to skip Steps 1 & 3 and sharpen Opus Three small wins that unlock more of the v2 cutover. No new endpoints — the data was already in the snapshot, just not flowing where it should. Step 1 (entity_type) — deep-link path only: - /lookup now returns `legalEntityType` and `registrationDate` (added to CompanyLookupResult). - /onboarding/page.tsx does a server-side /lookup prefetch when ?org_number= is present (BankID picker path), maps "AB"/"EF" to the EntityType enum, and seeds Step 1's radio. Falls through silently for unsupported codes (HB, KB, …) and on TIC errors. - WelcomeOnboarding hydrates ticLookup state from the server prefetch so Step 2's debounced client fetch and Step 3's first-year inference both have data on first render — no flash. Step 3 (is_first_fiscal_year) — every path: - deriveFirstYearDefaults() parses ticLookup.registrationDate and returns { isFirstFiscalYear, firstYearStart } when registered <12 months ago. Step 3's initialData picks it up; the user only confirms the end date. - Settings value wins when present so existing users with a saved choice don't get overridden. Composer prompt: - redactTic allowlist was the bottleneck — it stripped beneficialOwners, signatory, board, representatives, payrolls, statuses, fiscalYear before Opus ever saw the JSON. Existing filterRedundantQuestions ownership logic was effectively dead because the data path was severed. Expanded allowlist to include those v2 sections; kept bankAccounts/ email/phone/fiscalYearHistory/financialReports out (token cost > signal). - SYSTEM_PROMPT now documents each v2 section and the rules Opus should apply: payroll signal switches from "registration.payroll" to "actual payrolls[] filings" (kills the false-positive swedish-payroll selection for newly registered employers); beneficialOwners[] becomes the authoritative ownership source (single owner → FMB modifier; multiple → multi-owner); statuses[] isCeased/red triggers an uncertainty_note. Tests: 4112 unchanged. Build: green. No schema or migration changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): onboarding polish + composer signal fixes from first-run feedback UX: - AgentOnboarding: drop the 10s "Hoppa över — fortsätt med standardval" escape hatch. The fallback path runs automatically on timeout; the manual skip just teased users into a degraded build. - ReviewCard step 2 title: "Stämma av detaljerna" → "Stäm av detaljerna" (imperative form matches the rest of the steps). - Drop em-dashes from user-visible Swedish strings in AgentOnboarding + ReviewCard (fallback labels, subtitles, placeholder, error message, final CTA). Em-dashes survive in code comments only. - "Fråga min revisor" → "Fråga min assistent" everywhere it surfaced: AgentTrigger, AgentSparkleButton, ReviewCard preview, ReviewCard fallback comment, general.help intent buttonLabel + prompt text. - AgentTrigger / AgentSparkleButton / EmptyState.AgentHelpLink / TransactionInboxCard ask-button all gated on identity.isVerified. Pre-onboarding users no longer see the floating FAB or per-page Sparkle buttons. AgentSheetProvider.identity gained an isVerified field; (dashboard)/layout.tsx selects agent_profiles.verified_at and passes it through. TIC verksamhetsbeskrivning: - tic/index.ts /profile: /companies/{id}/purposes returns every historical verksamhetsföremål filing. Picking [0] was returning the oldest "äga och förvalta" holding-company boilerplate for companies whose later filings narrowed the purpose ("tillhandahålla företagskrediter och finansiella teknologilösningar"). Sort the array by lastUpdatedAtUtc desc and take the most recent non-empty purpose. Composer banking signal: - loadBankingSummary now reads journal_entry_id alongside description/amount/date and returns per-counterparty `direction` ('in' | 'out' | 'mixed') and `has_unbooked` (any row not yet booked). Aggregate `unbooked_count` accompanies the rollup. - buildUserPrompt emits each counterparty as `Name: 12 345 kr (ut, OBOKFÖRD)` so Opus can tell income from cost on sight and tell which counterparties are still open questions. - SYSTEM_PROMPT now explicitly forbids verification questions about counterparties whose direction is unambiguous AND status is 'bokförd'. Should kill the regressions from the first agent build: * "Konsult, J 98 565 kr — intäkt eller kostnad?" when the amount is clearly negative. * "ALMI AB 493 000 kr — lån eller bidrag?" when the transaction is already categorized. Tests: 4112 unchanged. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent,ui): representation needs deltagare+syfte, drop duplicate doc icon Representation booking: - transaction-categorization prompt now requires the agent to capture participants (name + company) AND purpose before staging a representation categorization. SKV's representationsregler + ML 8 kap require the verifikation to document who attended and what the meeting was about; without that the avdrag is denied and the post should be booked as non-deductible / personalkostnad. - The agent confirms back in plain text (audit trail in the chat), writes the deltagare + syfte to gnubok_remember_fact (long-term), THEN stages. Saknas deltagare/syfte: explicitly tell the user the avdrag won't go through and offer the non-deductible alternative. - Known gap (followup, not this commit): the staged op's journal entry description doesn't yet carry the deltagare text. Until we add a `notes` field to gnubok_categorize_transaction, the audit trail lives in chat + agent_memory only. TransactionInboxCard duplicate attachment indicator: - Drop the FileCheck2 "open document" button from the trailing slot. TransactionAttachmentIndicator (Paperclip) next to the description already opens the underlag on click. Two icons doing the same thing was noise. Cleaned up the unused state (isOpeningDoc, hasAttachment, handleOpenAttachment) and dropped now-unused imports (FileCheck2, useToast). Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent,nav): notes on verifikation + redesigned sidebar Audit-trail notes for representation: - gnubok_categorize_transaction gains an optional `notes` string. Threaded through stagePendingOperation → commitCategorizeTransaction → createTransactionJournalEntry, which now appends notes to the entry's description (capped at 500 chars). The verifikation an external auditor reads now carries deltagare + syfte directly — not just chat history / agent_memory. - transaction-categorization prompt updated: representation flow now REQUIRES the agent to pass deltagare+syfte via the notes parameter. Without it the booking is non-deductible / personalkostnad per SKV. DashboardNav redesign: - Top section: flat, no header — Hem (/chat), Underlag (was Dokumentinkorg), Transaktioner, Granskning. Always visible; the inline badge on /pending shows the count when there are pending ops. - Mid section: four collapsible dropdowns (Försäljning, Inköp, Redovisning, Personal). Each auto-expands when the active route lives inside it. KPI moved from main to Redovisning. Extension nav items (TIC workspace, etc.) fold into Redovisning. - Bottom-left: new account popover (DropdownMenu, opens upward) holding CompanySwitcher, Inställningar, Hjälp, Support, Logga ut. Replaces the old top company-switcher card + the bottom Support/Logout block. - Mobile drawer mirrors the new structure: top items as flat list, same four dropdown groups, separate "Tillägg" section when extensions exist, "Mitt konto" section at the bottom. - i18n: invoice_inbox label renamed "Dokumentinkorg" → "Underlag" ("Documents" in en). New keys: mitt_konto, group_extensions. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nav): unhide Leverantörer under Inköp The /suppliers entry existed in navItems but was marked hidden — leftover from when the supplier list lived elsewhere in the IA. Removing the hidden flag puts Leverantörer in the Inköp dropdown alongside Leverantörsfakturor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nav): CompanySwitcher back to top-left, user account moves bottom-left The previous pass collapsed both concepts into the bottom popover. They mean different things: the company is the org context everything below operates against (top-of-sidebar, scannable); the user is the account-holder (bottom-of-sidebar, where settings/logout live). - (dashboard)/layout.tsx: fetch profiles.full_name alongside the existing identity queries; pass userName + userEmail into DashboardNav. - DashboardNav: restore CompanySwitcher at the top of the sidebar (pre-redesign placement). Bottom-left popover trigger now shows the signed-in user's name + single-letter initial (accountInitial helper falls back to email's first char, then "?"). Popover header carries full name + email; items unchanged (Inställningar, Hjälp, Support, Logga ut). CompanySwitcher removed from inside the popover — nested dropdowns were awkward and the top placement is where it belongs. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pending): trim the agent context strip The row-level AgentContextStrip on /pending was rendering the model name (eu.anthropic.claude-sonnet-4-6) and the full atoms array (horizontal/swedish-vat, vertical/konsult-it, …) inline, which made each row 60–80 chars of mostly-the-same metadata. Reviewers never scan that text; they scan amounts and decide approve/reject. Now the strip shows only the conversation deep-link (Konversation #<short id>) — the one piece that's actually useful for diving into context. Model + atoms remain available in agent_metadata for debugging surfaces; they're just not in the list view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): shared ground rules + paragraph breaks after tool calls Two regressions surfaced in real usage. Both are systemic. Shared agent ground rules: - /chat surface (general.help) was happily inventing four-digit BAS account numbers ("Debet 6212 - Molntjänster…", "Kredit 2614 - Ingående moms…") and proposing booking decisions on invoices it had never seen, with no follow-up questions about currency/scope/etc. - transaction-categorization had those rules baked into its prompt; general-help / bokslut-step / invoice-draft / supplier-invoice-review / verifikation-draft / vat-review never inherited them. - Extracted lib/agent/intents/shared-rules.ts with five cross-cutting rules: underlag first (check inbox + ask user to upload to Dokumentinkorgen when missing), ask follow-ups when ambiguous, never write four-digit BAS account numbers in chat (category names only), cite atoms / load skills (don't guess), check counterparty history before proposing. - Injected renderAgentGroundRules() into all six intents above. transaction-categorization left alone — it has more detailed inline rules tied to its specific underlag-flow. Paragraph break after tool calls: - text_delta from the model often resumes after a tool call without a leading newline ("kategoriseras." → gnubok_query_journal runs → "Inget historik hittades…" appended directly). Markdown rendered the concatenation as one paragraph. - AgentChat text_delta handler now inserts \n\n when (a) the buffer ends with text content, (b) the incoming delta starts with text content, (c) at least one tool call has run, and (d) the buffer doesn't already end with a blank line. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(nav): default-open dropdown groups; closing is per-user Dropdowns started collapsed which meant first-time users had to open each group to discover what's inside. Inverted the state: default open, user can collapse, active route still forces a group open. - manualExpanded → manualCollapsed (semantics flip) - toggleGroup unchanged externally; flips the bit - isGroupExpanded returns !manualCollapsed[g] || hasActiveChild Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent): rate-safe v1→v2 TIC upgrade, counterparty defaults, profile settings Three pre-ship quality wins. Rate-limit-safe TIC v2 upgrade: - The /profile endpoint fans out to ~13 Lens calls; the account has a ~3000/mo ceiling. Force-refreshing every pre-v2 (v1) snapshot across the customer base would blow the budget. - ensureTicSnapshot gains an `upgradeV1` flag. A cached snapshot still inside the 7-day window is re-fetched only when (a) the caller passes upgradeV1 AND (b) the snapshot is v1-shaped (missing the v2-only `statuses` key). Gated to the two agent-onboarding call sites — a deliberate, once-per-company action and the only consumer of the v2 sections. Workspace + signup keep the natural 7-day staleness, so the v1→v2 migration is lazy and bounded to companies actually building an agent. Known-counterparty defaults (shared-rules): - Agent now proposes a sensible default for well-known counterparties instead of asking the same question monthly: Almi → lån, Tillväxtverket/ Vinnova/EU-stöd → bidrag, Skatteverket → skatt/avgift or återbäring, Bolagsverket → avgift, Försäkringskassan → ersättning, EF private withdrawal → eget uttag. Stated as an assumption the user can correct, not a hard rule — underlag/history still wins. Företagsprofil settings page: - New /settings/agent-profile (Företagsprofil / "Company profile"): view + edit the agent's company profile after onboarding — assistant name + avatar, the profile summary the agent reasons from, and a read-only chip view of loaded specialities (atoms). Backed by the existing GET/PATCH /api/agent/profile. - New GET /api/agent/atom-titles?ids= resolves atom slugs → human titles for the chips (registry is globally-readable reference data). - Added to SettingsSidebar; i18n keys agent_profile (sv "Företagsprofil" / en "Company profile"). Note: /chat already redirects unverified users to / (chat layout guard), and / renders WelcomeGate → /onboarding/agent. No redirect work needed. AgentSetupBanner.tsx is orphaned dead code (WelcomeGate superseded it). Tests: 4112. Build: green. Both new routes compile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nav,agent): Hem=Översikt + separate Assistent button; memory dedup Nav restructure: - "Hem" now points to / (Översikt dashboard) again, not /chat. The agent chat gets its own top-level nav entry "Assistent" (Sparkles icon) → /chat. Mobile bottom nav mirrors this (Hem / Assistent / Transaktioner). - / restored to render DashboardContent (the Översikt) for built-agent users instead of redirecting to /chat. Users who haven't built their assistant yet still get WelcomeGate (the build-agent checklist); once verified, / shows the dashboard. Chat is reachable anytime via its nav entry. Restored main's dashboard data-fetch; added an agent_profiles verified_at probe to drive the WelcomeGate branch. - i18n: nav.assistant ("Assistent" / "Assistant"). agent_memory dedup (gnubok_remember_fact): - The agent re-remembers the same fact constantly (e.g. "Vercel = omvänd skattskyldighet" on every Vercel categorization), which would bloat agent_memory with paraphrases over months. - Before insert, compare the incoming fact against the 300 most-recent active memories by word-set Jaccard similarity (lowercased, punctuation- stripped, stopwords dropped). A near-duplicate (≥0.82) is treated as already-known: bump its relevance toward the new score + refresh updated_at instead of writing a new row. Embedding-free, zero added latency beyond one bounded SELECT. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent,nav): företagsprofil=Bolagsuppgifter, avatar nav icon, dedupe greeting Företagsprofil settings page (the right content this time): - Replaced the agent atoms/summary panel with CompanyProfileView — a read-only "Bolagsuppgifter" view of the cached TIC company snapshot (name, org-nr, form, address, F-skatt/Moms/Arbetsgivare, SNI, bank, verksamhet, employees, latest financials, status traffic-lights, fiscal year, firmateckning, företrädare). Server component reads the companies.tic_snapshot column directly — no extension import, stays inside the core-build boundary. - Route renamed /settings/agent-profile → /settings/company-profile. Removed the old AgentProfilePanel + the now-unused /api/agent/atom-titles endpoint. "Assistent" nav icon = the agent's chosen avatar: - DashboardNav reads agent identity from AgentSheetProvider and renders the onboarding-chosen avatar for the /chat ("Assistent") entry across desktop sidebar, mobile drawer, and mobile bottom nav. Falls back to the Sparkles glyph pre-onboarding (no avatar yet). Nav cleanup: - Dropped the beta badge from Underlag. - Filtered the TIC workspace (/e/general/tic, "Företagsprofil") out of the nav — the same Bolagsuppgifter now lives under Inställningar → Företagsprofil, so it shouldn't appear in two places. Doubled intake greeting fix: - /chat/intake fires an invoke with no conversation_id, then swaps the URL to /chat/[id] the instant the `conversation` event lands — which can beat the greeting being persisted. /chat/[id] then hydrated with 0 messages and, because the auto-fire guard keyed on (id && messages>0), fired a SECOND invoke on the same conversation → two greetings. Guard now keys on conversation-id presence alone: a set id means resume, never bootstrap. Closes the race. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): paragraph-break-after-tool split words mid-stream The earlier "insert \n\n when text resumes after a tool call" heuristic re-evaluated on EVERY text_delta (any delta not starting/ending with whitespace, once a tool had run). Streaming deltas arrive in sub-word chunks, so it injected breaks between fragments of the same word: "minnes\n\nno\n\nterna", "kund\n\nrep\n\nresentation". Replace the per-delta heuristic with a consume-once ref: - tool_use sets breakBeforeNextTextRef = true - the next text_delta consumes it: prepends \n\n exactly once (only when the buffer has content, doesn't already end in whitespace, and the delta doesn't start with whitespace), then clears the flag So the break fires once per tool→text resume, never mid-word. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): much shorter replies, representation headcount + VAT cap, dot separator Brevity (system-prompt Svarsformat — affects every reply): - Hard "korthet är regel nummer ett": aim for 2-4 sentences, lead with the answer/action, no warm-up ("Här är vad som gäller…"), don't derive VAT in prose, don't restate what the approval card shows, one question at a time. The agent was writing textbook-length essays. Representation rule now in shared-rules (so verifikation-draft, vat-review, etc. all get it — previously only transaction-categorization had it, which is why the verifikation flow guessed 25% VAT and skipped the cap): - Require ANTAL deltagare (headcount), not just one name — the moms deduction is per person (underlag cap 300 kr/person ex moms). - Use the receipt's ACTUAL VAT rate (usually 12% on food), never assume 25%. - Meal representation isn't income-tax deductible (post-2017); whole cost booked as non-deductible representation. Verifikation description separator: - createTransactionJournalEntry appended notes with an em-dash ("Utlägg Eatnam — Deltagare:…"), violating house style. Switched to a middle dot " · ". journal_entries has no separate notes column — the description IS the BFL verifikationstext / audit field, so deltagare + syfte correctly live there. Tests: 4112. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(settings): tidy Bolagsuppgifter — no status colours, clean firmateckning From first-look feedback on the Företagsprofil page: - Status: dropped the coloured traffic-light badges (red/yellow/green). Per the design system semantic colour is data-only, never chrome, so status now renders as plain label + date. Also filtered to dated entries only — Bolagsverket emits flags like "Har aldrig varit verksam" with no date that read as noise next to the real status. Ceased status gets muted destructive text (the one chrome colour the system keeps). - Firmateckning: the source text carries ">" list markers and crams several rules onto one line, and repeats "Firman tecknas av styrelsen" across rows. cleanSignatory() strips the markers, normalises whitespace, splits run-on "Firman tecknas …" clauses onto separate lines, and the render dedupes — so each rule reads as its own sentence. Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): inbox items expose all terminal links + processed flag The Eatnam receipt was booked against its bank transaction (so the inbox row had matched_transaction_id + created_journal_entry_id set), yet the agent reported it as loose/unmatched and a duplicate risk. Root cause: gnubok_list_inbox_items only selected and returned matched_supplier_id + created_supplier_invoice_id — the supplier-invoice path. The transaction-match and direct-journal-entry paths were invisible, so any receipt cleared via /transactions looked unprocessed. - list_inbox_items now selects + returns matched_transaction_id and created_journal_entry_id alongside the supplier fields, plus a derived `processed` boolean (true when ANY of the three terminal links is set). - New unprocessed_only=true input filters to items with no terminal link — the "what still needs handling" view that prevents the agent from flagging already-booked docs as duplicates. (Fetches a wider window then filters client-side so limit applies post-filter.) - Description updated to document the processed semantics, within the 280-char tool-description budget. The DB linkage itself already worked: /transactions attach-document sets matched_transaction_id, and commitCategorizeTransaction stamps created_journal_entry_id. This was purely a read/surface gap. Tests: 4112 (+ MCP description guard). Build: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): repair stage-but-never-commit tools + consolidate tool surface - post_annual_depreciation AND reverse_entry were never in the pending_operations operation_type CHECK, so both staged then died with check_violation at INSERT. Add the CHECK migration, a commitPostAnnualDepreciation executor (reusing commitAnnualPostings), risk tier, and the PendingOperationType union member. - Salary tools de-risked: calculate_salary_run calls runSalaryCalculation() directly (no self-fetch/forged cookie); create_salary_run uses a transactional create-run helper with compensating delete; generate_agi actually generates + persists the declaration. - import_sie parses + validates at stage time with a content-rich preview (company, fiscal year, voucher/account counts, balance) instead of a blind byte count. - batch-match-invoices passed user.id where companyId was expected (silently matched zero). - VAT report+widget merged behind render_ui; gnubok_search_tools ranks by relevance; gnubok_feedback readOnlyHint corrected; tools/list instruction text fixed; income decision-tree + GL/query_journal cross-refs added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent): load skill atom bodies from the DB so they survive the build Skill bodies were read from disk at runtime (.claude/skills/**/SKILL.md); on Vercel the dynamic readFile path isn't traced into the lambda and on Docker .claude/ is excluded, so atoms loaded EMPTY in production — a despecialized agent. Inline the bodies into agent_atom_registry instead: - Migration adds body + mcp_exposed columns; a build-time generator (scripts/generate-skill-bodies.ts) emits a deterministic dollar-quoted seed migration with a content-hash manifest + --check CI guard. - Read sites (mcp-server atoms.ts, chat system-prompt.ts, composer prewarm) read body from the DB, with a dev-only disk fallback. mcp_exposed curates which atoms the MCP exposes (swarm-* never become atoms). - The seed script + generator share scripts/lib/atom-discovery.ts; estimated_tokens now reflects SKILL.md only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent): safe the in-app assistant — gating, FAB de-confliction, rate limit, friendly errors - Hide all agent entry points until verified_at: the Assistent nav tab (sidebar + mobile) and the agent-memory settings tab now match the floating FAB's gate. - FAB de-confliction: /kpi -> kpi.explain and /bookkeeping/year-end -> bokslut.step so the floating button opens the SAME assistant as the page button (no two-agents-on-one-page). - Generous per-user rate limit (30/min, 1000/day) on /api/agent/invoke, /onboarding/stream, /composer via a new agent_rate_counters table + check_and_increment_agent_quota RPC; fails open. Bounds runaway Bedrock spend without touching normal users. - Friendly errors: Bedrock 429/timeout/5xx normalized to Swedish (friendlyModelError) in run-turn + the invoke route; the chat client surfaces the server's friendly message instead of a raw HTTP status. - /chat/new validates ?intent= against the registry so bad deep-links fall back to general.help instead of rendering a broken-looking error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): keep /chat read-only — redirect categorization + swap the "categorize" suggestion for a VAT-report question general.help (the /chat assistant) is read-only, but it still gave per-transaction bokföringsförslag in prose and asked "godkänner du dessa?" — an analysis the user can't act on (no write tool, no per-tx underlag). Strengthen the prompt to redirect categorization/bokföring to the per-transaction flow (open the transaction -> "Fråga om denna transaktion", where the agent sees the underlag and stages a real ApprovalCard); a short overview is still allowed. Add a guard test locking in no-write-tools + the redirect language. Swap the /chat empty-state "Hjälp mig kategorisera" chip (which lured users into exactly this dead-end) for a VAT-report question the read-only assistant can actually answer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(pending): declutter the review queue rows + header Fold the conversation deep-link onto the actor label (drop the separate "Konversation #xxxx" strip and its icon), hide the quick-pick when there's only one operation type (it duplicated "Markera alla"), and drop the "(0)" from the disabled bulk-approve button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(vat): enhance VAT handling by integrating document validation and improving error messaging * feat(settings): add assistant knowledge surface + consolidate settings tabs Expose the agent's skill atoms (agent_atom_registry) in a read-only surface beside the existing memory view, and tighten the settings tab bar from 14 to 10 tabs. - New GET /api/agent/skills + AgentSkillsPanel: lists active, mcp_exposed atoms grouped by tier (Kärnkompetens / bransch / bolagssituation), flags which are active for the company from agent_profiles, and lazy-loads each SKILL.md body on expand. - New /settings/assistant tab with a Minne/Kompetens toggle (?view=skills); /settings/agent-memory and /settings/agent-skills redirect into it. - Merge Företagsprofil (TIC snapshot) into the Företag tab via CompanyProfileSection; /settings/company-profile redirects. - Merge Skatteverket-anslutningen into the Skatt tab — OAuth returnTo and the callback toast now target /settings/tax; /settings/skatteverket redirects. - Drop the Säkerhetsbackup tab (already under Importera/Exportera). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(inbox): keep booked underlag out of the unmatched queue + widen match window - categorize: after booking an inbox underlag onto a verifikat, backfill the inbox row's matched_transaction_id + created_journal_entry_id so it stops showing as unmatched (mirrors the /attach-document paperclip path). - TransactionMatchPicker: bias the candidate window forward (60d before → 180d after the invoice date) so late payments aren't dropped before scoring, and widen the ranking date tolerance to 120d so the true match floats to the top instead of collapsing to "Svag match". Fix "okatigoriserade" typo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip: bundle in-progress branch work + agent onboarding chat optimizations Captures the uncommitted work-in-progress on this branch so it lives on the remote. Heterogeneous changeset — bundled as one commit since the work was already entangled across files. Headline change in this commit (from this session): - Remove the double interview in agent onboarding. Phase B's verification- question form stepper is gone — the Phase C chat (onboarding.intake) now owns the entire interview and reads the composer's verification_questions server-side as its question bank. - ReviewCard collapses from 3 steps to 2 (meet → review-and-confirm) with value-first ordering: profile + "vad jag kan hjälpa dig med" + facts + optional seed note. CTA reads "Möt {namn}" to signal the chat follows. - ChatIntakeStarter handoff subcopy updated to match reality (assistant greets first; user can leave anytime). - Stamp agent_profiles.intake_completed_at server-side in app/api/agent/invoke/route.ts on the first user-typed reply in any onboarding.intake conversation (idempotent IS NULL guard, best-effort). Closes the previously dead-write column and unlocks the opportunistic- follow-up hook the migration anticipated. Plus in-progress branch work being carried forward (not introduced here): agent runtime + intent prompts, composer + atom-discovery scripts, MCP server skills surface, onboarding flow components, dashboard/inbox tweaks, two new agent_atom_registry migrations, additional agent-chat tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent): drop inline "Fråga assistenten" affordances — rely on the FAB The bottom-right "Fråga {namn}" FAB (AgentTrigger) is already route-aware and picks the right intent per page, so duplicating it as inline page- header buttons and empty-state links is noise. Removed: - EmptyState `agentHelp` link ("Eller fråga {namn} hur du kommer igång") + the AgentHelpLink component + agent_default_name/agent_ask_link i18n keys + the agentHelp props on EmptyInvoices/EmptyCustomers/EmptyTransactions. - AgentSparkleButton on /bookkeeping (verifikation.draft) and /kpi (kpi.explain) page headers. The FAB stays — when verified, it appears on those routes and routes to the right intent automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): gate the last two ungated "Fråga assistenten" affordances Both surfaces previously called useAgentSheet directly without checking identity.isVerified, so they appeared pre-onboarding (everywhere else the FAB / sparkle buttons / /chat / Assistent nav are all gated on verified_at). - Settings page header: remove the "Fråga {namn}" pill entirely. The FAB covers /settings routes route-aware (settings.help) — no need for a duplicate inline trigger. - Invoice inbox transaction picker: hide the "Fråga assistenten" button when the agent isn't built. Done at the parent (InvoiceInboxWorkspace) by passing onAskAssistant only when identity.isVerified is true; the child renders the button only when the callback is present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(tic,onboarding,agent): single-call TIC lookup + director-aware narrative voice - TIC: collapse the company lookup from 6 endpoint calls to 1 (search-public already exposes sniCodes, bank accounts, emails, phones, and registration flags). Derive fiscal-year MM-DD from mostRecentFinancialSummary; newly-registered companies fall through to the client's first-year defaults. - Onboarding: BankID picker no longer auto-provisions companies. Every pick routes through the wizard with orgnr (and entity_type via the CompanyRoles match) prefilled; F-skatt/VAT/address get confirmed in steps 2-4 instead of being auto-fetched. createCompanyFromOnboarding reuses CompanyLookupResult and adds a defensive top-level catch so server-action errors surface to the UI instead of being redacted. - Agent composer: loadUserDirectorship() checks BankID CompanyRoles for a director-like position (ceo/boardMember/chairman/externalSignatory, active) before the narrative uses second-person ownership voice ("Du driver…"); unknown users get neutral third-person voice so we never put ownership words in the user's mouth. Tests cover loadUserDirectorship, narrative voice, tic-fetch path, onboarding page, and updated TIC client + lookup/profile suites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tic): extend agent-onboarding TIC budget to 10s + backfill stranded org_numbers The 5s TIC fetch timeout aborted client-side before the upstream Lens fan-out (~13 calls) could complete, but the in-flight upstream calls still counted against quota — actions.ts already documents ~530 wasted calls from this in May. Same bug still applied to the agent-onboarding stream path. Adds an optional `timeoutMs` to `ensureTicSnapshot` so deliberate wait-screen callers (agent onboarding stream) can run with 10s while background/dev callers stay on the conservative 5s default. Page-level server fetch (page.tsx) intentionally stays at 5s to avoid blocking TTFB without a visible progress affordance. Backfill migration mirrors `company_settings.org_number` to `companies.org_number` for the 105 cases where it's safe (after dedup + conflict filtering). 56 of those are on active companies — unblocks duplicate guards, SIE/SRU exports, and TIC fallback chain. Zero TIC API calls — pure data move. Idempotent. Also sweeps a pre-existing SSRF guard on the stream route's origin derivation that was sitting unstaged in the working tree — it lives in the same diff hunks as the TIC budget change and couldn't be split cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip: bundle in-progress branch work Sweep up uncommitted agent/MCP/RLS work-in-progress so the branch is fully backed up to origin. Not reviewed in detail — committed as-is to preserve working state alongside the TIC fixes in the previous commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): tag the "Bygg din bokföringsassistent" CTA as Beta Adds a Beta badge next to the assistant-setup heading on the dashboard banner, dashboard inline card, and onboarding checklist row. Also drops the stale "Gratis i 30 dagar" subline from the dashboard card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build,migrations): PendingOperationType salary ops + resolve migration version collisions PR #584 went red on three things: 1. core-only build / Vercel: `lib/pending-operations/commit.ts:2666` switched on 'create_salary_run' and 'generate_agi' but `PendingOperationType` was missing both literals. Add them to the union. 2. Supabase preview: migration version 20260526120000 collided with main's newly-merged 20260526120000_fix_replace_sie_import_hard_delete.sql. Bump the branch's pair to 20260526120050 / 20260526120051 — still ahead of 20260526120100_restvardeavskrivning so ordering is preserved. 3. 20260527170000 was used twice on this branch (_agent_rls_with_check + _journal_entry_no_doc_required). Bump the second to 20260527170100 so the pair stays orderable and Supabase doesn't choke on the duplicate schema_migrations PK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): reword comment so core-only guard stops flagging it The "Check no core imports from extensions" step greps for the literal \`from '@/extensions/\` across lib/, app/api/, components/. A comment in lib/agent/composer/tic-fetch.ts quoted the exact pattern verbatim to explain *why* the file does a self-fetch instead of importing the TIC extension directly — which the grep matched even though no actual import exists. Rewrite the line to keep the same meaning without the literal pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Emil <emilmattsson14@gmail.com> |
||
|
|
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 |
||
|
|
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> |
||
|
|
aeadc8bb1d |
feat: enhance chat approval flow for pending operations with pre-fill… (#566)
* feat: enhance chat approval flow for pending operations with pre-filled commit details * fix: update message validation to be case-insensitive and simplify approve schema * fix: enhance approval guidance for high-risk operations and improve message clarity |
||
|
|
50e5520b6d |
Add/preview invoice image (#565)
* feat: enhance invoice preview functionality with mock customer support * feat: update invoice preview logic to handle mock customers and improve error handling |
||
|
|
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 |
||
|
|
e4488a900b |
feat: add user locale preference to user_preferences table (#555)
* feat: add user locale preference to user_preferences table
- Introduced a new column 'locale' in the user_preferences table to store per-user UI language preferences.
- Added a CHECK constraint to ensure only supported locales ('sv', 'en') are allowed.
- Triggered a schema reload notification for the changes.
chore: declare CSS module support in TypeScript
- Added a declaration for CSS modules in globals.d.ts to enable TypeScript support for importing CSS files.
* feat: add Swish as an invoice payment method in company settings
|
||
|
|
39204cc0de |
UX polish bundle: Enable Banking lookback + sync progress, invoice inbox, matching previews (#548)
* fix(import): dedup opening-balance rows when account numbers differ only in whitespace The parser's merge map keyed on the post-strip account_number, but rows like "1930", " 1930 " and "1.930" could leak as separate entries when the upstream string contained non-breaking spaces or zero-width chars that the old .replace(/[^0-9]/g, '') ran on already-stripped output. Strip those explicitly in the raw string and use /\D/g for the digit extraction. Also adds defense-in-depth dedup inside OpeningBalanceEditStep so any duplicates that survive the parser collapse before the user sees them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(enable-banking): anchor lookback picker to fiscal year, not days Replaces the 90/180/365 days dropdown on the account-selection screen with three explicit modes: - "Senaste 90 dagar (snabbt)" — fastest path, matches PSD2 ceiling - "Sedan räkenskapsårets början" (default) — resolves via fiscal_year_start_month, surfaces the literal date inline - "Anpassat datum" — free date picker OR "Föregående räkenskapsårets start" When the resulting range exceeds 90 days, the picker now surfaces a quiet helper that points users at the SIE/bankfil import for older history, so they don't waste an account-selection round-trip discovering that banks usually cap at ~90 days. The PATCH /accounts handler accepts initial_lookback_from_date alongside initial_lookback_days; the new helper getCurrentFiscalYearStart() in lib/company/fiscal-year.ts is reused. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(enable-banking): dedicated sync progress modal replaces silent spinner After the user confirms account selection, transactions fetch in the background for 30–60 seconds. Previously this showed only the Spara-button spinner with no indication of duration or what was happening — users described being stuck on the page. The new BankSyncProgressDialog opens immediately on Save, lists the enabled accounts being synced, and disables manual close until the PATCH resolves. On completion it shows the imported count and the actual date range the bank returned, plus an amber escape hatch to SIE/bankfil import when the returned range was truncated by >7 days from what was requested. Failure path surfaces in the same modal rather than as a destructive toast that disappears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(invoice-inbox): drop duplicate Skapa leverantör button The inbox detail panel had its own supplier-creation button that fired /api/suppliers + match-supplier. The same action is reachable from the supplier-invoice form's "Skapa & välj" card (showAISupplierHint), which also prefills more fields. The duplicate button is gone; a quiet inline hint replaces it so the user still knows why no supplier matched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(invoice-inbox): surface currency and totals above the long metadata tail Move Valuta / Totalt / Moms in FIELD_DEFS so they sit immediately under Leverantör / Org.nr / VAT-nr. These are the fields the user reads first when triaging an inbox item; burying them after nine metadata fields forces unnecessary scrolling on every single invoice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(invoice-inbox): accept .eml forwards and log rejected attachments Gmail's "Forward as attachment" packages the original email as message/rfc822, which our MIME allowlist silently dropped. Adds mailparser so we can unwrap the inner attachments and ingest them under the inner email's subject/from. Also persists every rejected attachment as an invoice_inbox_items row with status='error', so users can see what was dropped instead of guessing why nothing showed up in their inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): redirect back to inbox after creating from invoice-inbox When the leverantörsfaktura form is opened from an invoice-inbox item, every successful create previously kicked the user out to /supplier-invoices or the just-created invoice's detail page — derailing the "process the next document" workflow. The Tillbaka button likewise routed to the supplier-invoice list rather than the inbox they came from. Adds an afterCreate helper that lands inbox-originated submissions at /e/general/invoice-inbox and preserves the original target everywhere else. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(pending): show transaction/document context for match-and-attach reviews The granskning page previously rendered attach_document_to_transaction and match_transaction_invoice operations through the generic key/value preview, so reviewers saw "document file name: Faktura.pdf / transaction amount: -216 USD" without any visual indication of which two things were being paired. The MCP tool already returns enriched preview data; we just needed dedicated layouts. Adds: - AttachDocumentPreview — two-card layout (Transaktion | Dokument) with a "Visa dokument" button that fetches a signed download URL on demand - MatchTransactionInvoicePreview — same layout (Transaktion | Faktura) - DocumentViewButton — reusable signed-URL opener Also tightens the matching tools' descriptions so AI clients are nudged to verify human-readable context before staging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address PR #548 review feedback - invoice-inbox: hoist mailparser to a static import. The extension system generates a static import tree via setup:extensions and disallows dynamic imports — await import('mailparser') worked in dev but could fail in production standalone builds. - enable-banking AccountPickerDialog: guard the Save path when "Anpassat datum" + "Specifikt datum" is selected with an empty date. Without this, lookback.body resolves to null and the PATCH silently falls back to the backend's 120-day default, ignoring the user's intent. - enable-banking BankSyncProgressDialog: drop the empty-body useEffect. Close-prevention is already handled inline via the onOpenChange guard + onPointerDownOutside + onEscapeKeyDown handlers. - lib/company/fiscal-year: pin both operands of daysBetween() to UTC when parsing ISO date strings. Mixing a UTC-parsed date with new Date() (local time) drifts by one day in any timezone east of UTC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address compliance swarm + Swedish review feedback Three actionable items from the post-fix compliance scan; the rest were false positives or out of scope. - enable-banking PATCH /accounts: reject future initial_lookback_from_date with 400 instead of silently falling through to the 120-day default. Compliance V2.2. - AttachDocumentPreview: promote the overwrite warning to a destructive banner with BFL 7 kap context when the existing document is marked as räkenskapsinformation. A muted footnote was too easy to skip past for a verifikationsunderlag replacement. - MatchTransactionInvoicePreview: surface transaction_date + invoice_date in the staged preview so reviewers can spot date drift before approving (BFL 5 kap 6§ — verifikation date must align with affärshändelse). Also shows a quiet hint when the two dates differ by > 31 days. Tool's SELECT + stage payload extended accordingly. Skipped (with rationale): - V5.3 inner.filename path traversal — lib/core/documents/document-service.ts already sanitizes filenames before constructing storage paths. - V5.2 magic-number MIME — pre-existing pattern for all email attachments; scope is codebase-wide. - V1.2 att.id composite ID — only used as a DB column value, never a path. - V13.1 / CM-8 SBOM/SCA — repository-wide policy, not this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address second-round compliance + Swedish review feedback Compliance Swarm (defense-in-depth + valid finds): - invoice-inbox: sanitise .eml inner attachment filenames and content-types before they flow into uploadAndExtract or the raw_email_payload JSONB. document-service already strips bad chars before constructing storage paths, but the swarm flagged the upstream input as unsanitised — easier to add a thin sanitiseFilename/sanitiseMime layer than to argue about defense-in-depth. Caps lengths too. - DocumentViewButton: validate documentId as a UUID before interpolating into /api/documents/:id — staged preview_data is Record<string, unknown> on the wire, so refusing junk early gives a clearer error and keeps the internal API from seeing oddly-shaped path segments. (Compliance V1.2.) Swedish review: - MatchTransactionInvoicePreview: drop the BFL 5 kap 6§ citation from the date-drift hint — that section governs verifikationsinnehåll, not a 31-day tolerance. The hint stays (the practical concern is real) but no longer pretends to quote a legislated threshold. - fiscal-year: document the implicit assumption that entity_type reflects the company's current tax-year status, not a mid-conversion state. Skipped (with rationale): - V5.2 magic-number MIME — pre-existing pattern across all email attachments. - A.8.12 signed URL via window.open — pre-existing pattern shared with JournalEntryAttachments.tsx; refactor to server-side redirect is broader scope. - A.8.15 logRejection failure path — pre-existing console.error pattern. - CC9.2 mailparser vendor review / SBOM — out of PR scope. - CC6.1 IDOR — /api/documents/:id already enforces company_id; false positive. - Swedish #1 räkenskapsinformation flag origin — server-side already derives the flag from document_attachments.journal_entry_id in the staging tool; not caller-trusted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): fail-safe BFL warning + preserve merge validation errors Two findings from the third compliance pass; both worth addressing. - AttachDocumentPreview: treat an absent existing_document_is_rakenskapsinformation flag as räkenskapsinformation rather than as "safe to overwrite". The MCP staging tool sets the flag deterministically from document_attachments.journal_entry_id today, but a future code path that forgets it would silently downgrade the BFL 7 kap warning. Only an explicit `=== false` from the server keeps the muted note path. - Opening-balance merge: union validation_errors when collapsing duplicate account_number rows, both in the parser and the EditStep useState initializer. Previously a warning that fired on row 5 (e.g. BAS-class mismatch) was silently dropped if row 2 of the same account had no error, risking misclassified IB data downstream. Added a parser test covering the union behaviour for two rows of a class-3 (resultatkonto) account. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
e4d4d8e4dc |
feat: enhance OAuth scopes and UI for agent-driven approval process (#544)
* feat: enhance OAuth scopes and UI for agent-driven approval process * refactor: update OAuth scopes to enforce explicit user consent for write and approval actions |
||
|
|
239261a0be |
Bug/mcp bas lag (#543)
* fix(mcp): update workflow descriptions for transaction categorization and approval processes * feat: implement account validation for transaction categorization to handle inactive accounts * fix(tests): stub findMissingAccountsMock to ensure no missing accounts during batch-categorize tests * fix(errors): ensure deterministic sorting of account numbers in AccountsNotInChartError |
||
|
|
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. |
||
|
|
fd4274787c |
feat(bokslut): Phases 4-7 — accruals + årsredovisning + EF + MCP tools (#509)
* feat(bokslut): Phases 4-7 — accruals + årsredovisning + EF + MCP tools Builds on the Phase 1-3 PR (#508). Same K2-AB scope decisions hold (iXBRL deferred, K3 deferred, koncernredovisning out of scope) — what ships: PHASE 4 — Periodiseringar - lib/bokslut/accruals/accrual-detector.ts: auto-proposes the vacation- liability change (delta on 2920 against 7090 + 31,42 % avgifter on 7519/2940) by reading lib/reports/vacation-liability.ts. Manual prepaid (17xx) / accrued (29xx) / audit-fee builders for entries the heuristic can't derive — supplier-invoice service_period detection is deferred until the data model grows the field. - /api/bookkeeping/fiscal-periods/[id]/accruals (GET + POST) — posts each accrual as a separate manual-source journal entry with the next-day reverse date embedded in the description. - New AccrualsStep wizard step between Preflight and Dispositions. - Auto-reversal cron is follow-up infra; for now reverses_on is metadata + a visible UI badge. PHASE 5 — Årsredovisning PDF + signing - lib/bokslut/arsredovisning/{types,build-data,arsredovisning-pdf,signature -service}: pre-fills flerårsöversikt from prior 3 fiscal periods, eget- kapital-förändring from journal data, K2-minimum noter with avskrivningstider auto-derived from the asset register and medelantal anställda from the employees table. PDF via @react-pdf/renderer mirroring the income-statement/pdf pattern. - Migration 20260516170000: arsredovisning_signature_requests with RLS, signed-immutability trigger, and DELETE policy that blocks signed rows. Signature-service exposes list/create/markSigned/isFullySignedOff. - BankID call itself is not wired here — the table + service make the request layer available so a follow-up can hook lib/auth/bankid.ts to the sign action without rework. - /api/.../arsredovisning (data + pdf + signatures) endpoints. - /bookkeeping/year-end/arsredovisning page with editable narrative, flerårsöversikt table, signer slots, PDF download, and Bolagsverket Mina Sidor link. Explicit warning about the FY2026 iXBRL mandate. PHASE 6 — Enskild firma NE-bilaga UI - lib/bokslut/enskild-firma/{egenavgifter,rantefordelning,periodiseringsfond -ef,expansionsfond}-calculator.ts. All declaration-only — never produce a journal entry. Egenavgifter 28,97 % / pensionärssats 10,21 % / passive SLP 24,26 %, schablonavdrag 25/10/20 %. Räntefördelning SLR+6 / SLR+1 with the -500 000 negative threshold. P-fond EF cap 30 % (vs 25 % för AB). Expansionsfond 125,94 % av kapitalunderlag, 20,6 % skatt. - EfDeclarationSection mounted inside DispositionsStep when entity_type is enskild_firma — live recompute as the user adjusts kapitalunderlag, prior-year amounts, p-fond desired, expansionsfond change. Each card shows the NE-bilaga ruta the number lands in. NE-bilaga preview link. PHASE 7 — Agent-native MCP tools - gnubok_propose_dispositioner: read-only AB dispositions proposal - gnubok_propose_accruals: read-only accruals proposal - gnubok_propose_annual_depreciation: read-only depreciation per asset - gnubok_post_annual_depreciation: stages depreciation commit (high-risk) - gnubok_preview_arsredovisning: structured K2 ÅR preview - gnubok_preview_ef_declaration: EF skattemässiga justeringar preview - All ≤280-char descriptions, additionalProperties:false, conform to STAGED_OPERATION_SCHEMA for write tools. Read tools return the same shapes as their HTTP counterparts so agents and the UI share a contract. - The existing GET /bokslutsdispositioner endpoint now calls the shared buildDispositionsProposal helper that the MCP tool also uses, removing the duplicate logic that lived in both. Verification - 149 unit tests pass (was 125 on Phase 1-3; +24 across accruals, EF calculators, expansionsfond, periodiseringsfond-ef) - Zero lint or typecheck errors on any new file - Migration 20260516170000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bokslut): address PR #509 round-1 — CI blocker + 3 P1s + compliance bundle CI BLOCKER (output-schema test) - The 5 new read-only MCP tools (gnubok_propose_dispositioner / _accruals / _annual_depreciation, _preview_arsredovisning / _ef_declaration) were missing outputSchema, which the existing strict-schemas guard reads as a hard failure on core-only CI. Added a permissive { type: 'object', additionalProperties: true } outputSchema to each — the return shapes are the same as their HTTP counterparts and trying to mirror them inline would duplicate the type tree across two boundaries. P1 — vacation accrual: two real bugs in one entry - Delta was anchored on the OPENING balance of 2920, so any mid-year postings (partial accruals, reversals) were ignored. Now anchors on the current closing balance via tb.rows.find(2920).closing_credit - closing_debit. Updated the computation field label to current_2920. - More importantly: 2920 is a balance-sheet carry-forward (semesterlöneskuld persists until the actual vacation is paid). The original implementation set reverses_on to Jan 1 of the next year, which would zero the liability on day 1 of the new year — a known Swedish bookkeeping error. The vacation proposal now ships with reverses_on = '' to suppress the reversal badge, the API route emits a "Bokslutsjustering" description instead of "Periodisering (vänds …)", and AccrualsStep renders "Rullas vidare (ingen vändning)" so the user knows the liability carries forward. P1 — signature_requests immutability gap (security) - The existing trigger only guarded role / signer_name / signed_at / status on signed rows, leaving bankid_signature_data, signer_personnummer_encrypted, and signer_personnummer_hash mutable. An UPDATE on a signed row could silently alter the BankID proof. New migration 20260517090000 replaces the trigger function to cover the full audit-critical column set (plus fiscal_period_id and company_id as belt-and-braces). P1 — narrative edits never reached the PDF - The /bookkeeping/year-end/arsredovisning page let the user edit description / important_events / resultatdisposition but the download link pointed at a plain GET that regenerated boilerplate. Wired the PDF endpoint to accept description / events / disposition as query params (length-capped) and the page now constructs the download URL with the current narrative state — only fields the user actually changed are included, keeping the URL short for the unchanged-defaults case. Compliance quick wins - Added period_lock check to gnubok_post_annual_depreciation MCP tool (matches the existing accruals POST guard). - Added explicit fiscal-period ownership pre-check to the signatures POST route (RLS would reject anyway; the route layer just makes the 404 envelope cleaner). - Replaced free-text role on the signatures schema with an enum allowing only Styrelseledamot / Styrelseordförande / VD / Verkställande direktör. - Added Cache-Control: no-store + Pragma: no-cache to the ÅR PDF response so the document (officer names + financials = personal data) isn't cached by any intermediary. - Sanitized period_end in the PDF Content-Disposition header to dodge header-injection via stray chars (defensive — period_end is a date, but the cost is one regex). - Softened the iXBRL warning text on the ÅR page: digital filing is proposed by Bolagsverket but not yet enacted; PDF is still valid today. False positives I'm intentionally not chasing on this round - Greptile P2 `sate` typo in the address city field — the rename would touch the type and every consumer; defer. - Greptile P2 "computation field label" — already addressed as part of the P1 vacation fix above. - Compliance V2.2 "silent skip" in accruals POST — the silent skip is in the UI's empty-row filter, not in the server route. Server validation already returns 400 via Zod for any invalid item. Verification - 154 unit tests pass (was 149; +5 from re-running the MCP strict-schemas + output-schema suites that now include the new tools) - Zero typecheck errors on any touched file - Zero lint errors on any touched file - Migration 20260517090000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bokslut): address PR #509 round-2 — 3 real bugs + 5-item polish 3 real bugs from the round-1 Swedish review + Compliance Swarm re-eval: - Soliditet inflated by obeskattade reserver. build-data.ts filtered equity with startsWith('20') || startsWith('21') — but 21xx (periodiseringsfonder, överavskrivningar) are partially deferred tax, not eget kapital. Splitting them out per K2 / ÅRL. Filter is now startsWith('20') only. - Resultaträkning omitted bokslutsdispositioner + skatt rows. K2 RR must include 88xx (dispositioner) and 89xx (skatt) before "Årets resultat" per ÅRL 3:2 — without them, the printed RR doesn't reconcile to BS 2099 and the document is non-compliant for any AB that posted bolagsskatt or periodiseringsfond. flattenIncomeStatement now splits the financial sections on title (Bokslutsdispositioner / Skatter och årets resultat) and emits the K2-required intermediate subtotals: "Resultat efter finansiella poster" → dispositioner → "Resultat före skatt" → skatt → "Årets resultat". - Accruals POST had no idempotency. Re-running the wizard (or a retried POST after a flaky network) would create duplicate accrual entries that distort both the balance sheet and trial balance. New findExistingAccrualEntry helper queries the period for an existing posted entry whose description matches the kind's stable prefix (or for manual prepaid/accrued, the user-supplied description). Duplicates land in a new `skipped` array in the response with a reference to the existing entry id, rather than producing a second posting. 5-item compliance polish: - signer_name on the signatures schema now has .max(200) per GDPR Art.25.2 data-minimization. Swedish personal names are well under that — the bound is a defense against an unbounded-string injection. - Audit-fee accrual: 6420 is BAS-specific to lagstadgad revision. Bokslut fees for a non-revisionspliktigt bolag (liability_account = 2991) now debit 6590 (övriga externa tjänster) instead — Skatteverket may query a 6420 debit when there is no revisor i bolaget. - Räntefördelning ne_ruta label: was 'R30 / INK1 kapital' (confusing — INK1 is a separate form). Positive now reads 'R30 (avdrag i näringsverksamhet)' with the INK1 T4 cross-reference moved to the description; negative reads 'R30 (tillägg till resultat)'. - gnubok_post_annual_depreciation MCP tool now checks the caller's company_members role and throws on viewer. RLS would reject the underlying INSERT anyway; failing fast here produces a cleaner error than the cascaded RLS rejection. Mirrors the HTTP route's { requireWrite: true } guard. - Signature DELETE policy now blocks both 'signed' AND 'declined' rows. A declined signature is auditable evidence (board member refused) and is material under ABL 8 kap. New migration 20260517100000. Bot-flagged items I'm deliberately not chasing on this round: - V8.2.1 × 2 cross-tenant findings on ÅR routes — same false-positive class I've responded to repeatedly: buildArsredovisningData internally filters by company_id, bot can't see past the route handler. - V4.5 / V2.2 MCP arg Zod redundancy — MCP server's central handler validates against each tool's inputSchema. - Narrative-in-URL GDPR concerns — proper fix is POST + body or server-side persistence; tracked as follow-up. - `sate` typo (Greptile P2) — type-wide rename, deferred. - Vacation avgifter age-tier rate split — inherited from upstream generateVacationLiability which doesn't expose age; needs upstream work. - Medelantal anställda proper monthly average — needs salary-run aggregation across the year, follow-up. - K2 noter aktiekapital + fastställelseintyg blocks — real K2 gaps, tracked as follow-up. Verification - 94 tests pass (subset for bokslut + MCP suites; full Phase 1-7 suite unchanged in scope) - Zero typecheck errors on any touched file - Zero new lint errors on any touched file (the 2 server.ts warnings are pre-existing) - Migration 20260517100000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
980f29dae8 |
Bug/momsdeklaration skv (#449)
* fix(salary): show birthdate in masked personnummer, hide the 4-digit suffix Flip the personnummer display format from XXXXXXXX-NNNN to YYYYMMDD-XXXX so the sensitive 4-digit suffix is hidden while the (public) birthdate stays visible. Affects the employees list/detail, salary run, payslip PDF, payslip email, and the MCP server tools (list_employees, get_salary_run). Each call site now decrypts the stored personnummer before masking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): allow deleting unbooked transactions from "Alla transaktioner" The history list only let users delete via the inbox card; once a category or mall was picked but the verifikation hadn't been created, the row showed "Ej bokförd" with no way to remove it. The API already permits delete while journal_entry_id is null, so the gap was purely a missing UI affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(vat): populate ruta 20-24 for reverse charge + dishonest "Validera OK" Three connected issues caused Skatteverket to reject momsdeklarationer with FK004 even after our local "Validera"-knapp returned OK. 1. supplier-invoice-entries booked fiktiv moms (2614/2624/2634 + 2645/2647) on reverse-charge invoices but never the underlying basbelopp on 44xx/45xx. Ruta 30-32 filled up at SKV while ruta 20-24 stayed at 0 — SKV's FK004 ("silent netting prohibited", ML 13 kap kräver båda sidor). Fix: generateReverseChargeBasisLines in vat-entries.ts emits parallel 45xx/44xx debit + 4598 motkonto credit per rate group. Engine calls it from registration, cash, and credit-note paths. Skipped when the user booked the expense directly on a basis account to avoid double-counting. 4598 added to BAS reference (no migration needed; account_number is plain text on journal_entry_lines). 2. rutorToMomsuppgift rounded each ruta independently but computed summaMoms from the unrounded ruta49. SKV recomputes the sum from integer rutor on their side, so fractional öres caused ±1 SEK drift and SKV rejected with FK009. Fix: derive summaMoms from the already-rounded VAT-amount rutor. 3. "Validera"-knappen only confirmed SKV's internal arithmetic — a declaration with ruta 30-32 populated and ruta 20-24 empty validated fine until /utkast hit FK004. Users got a false green light. Fix: vat-declaration-checks.ts runs locally before the SKV call, blocks Validera/Spara when ERROR-level findings exist, and surfaces them in a separate "Lokala kontroller"-section. Success message reworded so SKV's OK is no longer presented as filing-ready. Tests: 4535/4536/4531/4425 lines + 4598 motkonto on EU/non-EU/byggtjänster RC, credit-note reversal, fractional-öres summaMoms, all four pre-flight codes (RC_BASIS_MISSING, RC_OUTPUT_MISSING, RC_INPUT_VAT_MISMATCH, SUMMA_MOMS_DRIFT). Backfill for already-posted entries follows in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add skattekonto matching functionality - Enhance TransactionInboxCard to display a warning for potential 1930↔1630 transfers. - Implement match suggestions for skattekonto transactions in the backend. - Create SkattekontoMatchDialog component for linking skattekonto rows to existing journal entries. - Develop SkattekontoInboxCard component to handle skattekonto transactions in the inbox. - Introduce skattekonto-match utility functions for candidate matching and linking. - Update types to include match suggestions and enriched transaction responses. * refactor: reorganize skattekonto types and implement bank counterpart matching logic * docs: update CLAUDE.md to streamline integrations and clarify architecture details * refactor: enhance reverse charge logic to handle non-basis accounts and prevent double-counting --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
e77423d099 | feat(transactions): add tool to list transactions without documents and update scope map (#403) | ||
|
|
4131db2894 |
chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes (#402)
* chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes MCP server gains six intent-shaped tools that collapse multi-call agent flows into one: vat_close_check, query_journal, auto_match_period, create_supplier_invoice_from_inbox, audit_package, year_end_readiness. Tools wired into TOOL_SCOPE_MAP and OPERATION_RISK_TIERS as appropriate (create_supplier_invoice_from_inbox at medium tier — reversible until approve, but stages a leverantörsskuld). BankID enrichment now persists to a dedicated bankid_enrichment table keyed by user_id. extension_data has been company-scoped (NOT NULL company_id) since the multi-tenant refactor, so every BankID signup has silently been failing the enrichment upsert. Select-company picker reads from the new table. delete_last_voucher (BFNAR 2013:2) needs to clear document_attachments.journal_entry_id before deleting the entry, but the new document immutability trigger blocks that UPDATE. Added the same gnubok.allow_delete transaction-scoped bypass pattern used by the journal-entry/line/retention triggers. pg-real tests cover the happy path, the unauthorized direct UPDATE, and the swap-to-different-entry attempt under the bypass flag. fiscal_periods.no_overlapping_fiscal_periods exclusion was scoped to user_id from before multi-tenant — rebound to company_id so the same user can have overlapping fiscal years across companies they own/are member of. Also adds scripts/seed-demo-account.ts for end-to-end demo seeding (two companies, full FY2025, active FY2026 with mixed state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pr-402): address review feedback Migrations - Drop 20260506140000_document_journal_entry_immutability_delete_bypass.sql: redundant with 20260506140000_document_journal_entry_immutability_bypass.sql that landed on main while this branch was open. Both share the same gnubok.allow_delete pattern; main's version is what the DB actually has. - Rename 20260506150000_bankid_enrichment_table.sql → 20260506160000_bankid_enrichment_table.sql to clear the timestamp clash with 20260506150000_protect_document_journal_link.sql on main (Supabase branch preview was failing on schema_migrations PK collision). Tests - Drop the swap-under-flag test from delete-last-voucher.pg.test.ts: main's bypass returns NEW unconditionally when gnubok.allow_delete='true', so the swap is permitted. Drop the duplicate happy-path test (already covered by 'clears journal_entry_id on attached documents and deletes the voucher'). Keep the unauthorized-direct-UPDATE test. - Add bankid-enrichment.pg.test.ts covering the SELECT RLS policy: user reads own row, cannot read another user's row, INSERT denied for authenticated. gnubok_query_journal - amount_min/amount_max is applied post-fetch (PostgREST can't OR abs(debit) and abs(credit) cleanly), but PostgREST's count is computed pre-filter. Reporting that as total_lines mislead agents into paginating a tail that was already filtered out. When the amount filter is applied, anchor total_lines and truncated to the filtered set and surface db_matched_pre_amount_filter + amount_filter_applied_post_fetch separately. - Escape `_` in the free-text LIKE filter so a search for "2_441" doesn't match "2X441". VAT close check - Reverse-charge blocker no longer fires on ruta 30 (seller-side domestic omvänd skattskyldighet) — the seller books no VAT, the buyer does, so missing ruta 48 is expected. Now scoped to ruta 31/32 (EU acquisition) where the buyer must book both calculated output (2615) and matching ingående moms (2645). - High-value receipt threshold no longer reads journal_entries.total_amount (column doesn't exist; check silently never fired). Sums debits across the entry's lines, which equals the gross for ordinary purchase entries — comparing a gross figure against the BFL/ML 4 000 SEK threshold per ML 17 kap 26–28 §. seed-demo-account.ts - Require an explicit email argument; refuse to run with the previously hardcoded fallback that would silently target a real user. Ensure email is non-undefined for downstream typing. - Type the supabase fiscal_periods insert result locally so tsc no longer reports 'fp implicitly any' from the loose untyped client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): adjust fiscal-period-start-day pg test for per-company overlap The pg-real failure on PR #402 was a latent bug surfaced by this branch's fiscal_periods exclusion constraint flip from user_id to company_id (migration 20260506140100). The test was inserting periods that overlapped seedCompany's default 2026-01-01..2026-12-31 period; the previous constraint slipped past it because the test's INSERT didn't set user_id (NULL escapes the WITH = match), so two same-company overlapping periods silently coexisted. Now that the constraint correctly fires per company, pick years that don't overlap with the seeded 2026 period. The trigger's behavior under test (allow mid-month start when no earlier period exists, allow back-dated SIE imports, reject mid-month start when an earlier period exists) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(vat-close-check): correct reverse-charge/import blocker rutor Rutor 30/31/32 are the buyer's calculated utgående moms on reverse- charge purchases (domestic byggtjänster/electronics → 2614 → ruta 30; EU goods → 2624 → ruta 31; EU services → 2634 → ruta 32). The buyer must also book matching ingående moms (2647 inhemskt / 2645 utlandet → ruta 48). The previous fix removed ruta 30 on the basis that it was seller-side; that's incorrect — domestic-RC sellers book no VAT at all (they report only beskattningsunderlag on ruta 41), so 2614 only sees buyer-side entries. Restore ruta 30. Also extend the check to import rutor 60/61/62 (non-EU import VAT declared via momsdeklaration since 2015 — 2615/2625/2635). Same mechanic: importer books output VAT on these rutor and deducts the input side via ruta 48. SaaS-from-AWS / OpenAI / Vercel companies hit this path; without including 60/61/62 the blocker would silently miss their misbookings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): expose ruta 60/61/62 (import VAT) on the local VatReportResult The vat-close-check fix referenced vatReport.rutor.ruta60/61/62 but the MCP server's local VatReportResult type only carries ruta 05-49. Build broke on tsc. Extend the MCP server's slim VAT report to also project import VAT — 2615 → ruta 60 (25%), 2625 → ruta 61 (12%), 2635 → ruta 62 (6%) — and fold those into ruta 49 (att betala/återfå). Mirrors the BAS-to-Ruta mapping in lib/reports/vat-declaration.ts. Output schema and required list updated accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |