70e893b8d4c2d5348e7c6f7ab7407fa1b2eabbed
593 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70e893b8d4 |
fix(skattekonto): stop conflating live-call auth failures with "inte anslutet" + surface SKV connection first in tax settings (#887)
The sync endpoint returns 401 for six distinct auth states (handleSkvError): NOT_CONNECTED, SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE, TOKEN_CORRUPTED, TOKEN_REVOKED. The page treated every 401 as "Skatteverket är inte anslutet" — directly contradicting Inställningar, which reads the stored token metadata and truthfully shows "Ansluten" for all but the first. - syncNow now checks the structured error code: only NOT_CONNECTED flips to the not-connected empty state. Every other auth failure renders the server's actual Swedish message as a persistent banner with an "Anslut igen" CTA, keeping the stored saldo/transactions visible. - SkatteverketConnectPanel moves to the top of /settings/tax — the skattekonto and momsdeklaration pages send users there specifically to (re)connect, and below the tax form it sat out of view. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9f7c842a33 |
feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs (#885)
* feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs Every tab in the Skatt & bokslut nav group now tells an unconfigured user what is missing and where to fix it, instead of dead-ending or rendering zeros: - Momsdeklaration: gates on vat_registered with a settings CTA; auto-fetches the configured period on load and on every period change (no more "Hämta" button); one period control (räkenskapsår picked inline for helårsmoms, shell selector + back link dropped via new ReportDescriptor.standalone); raw <select>s replaced with the Select primitive; banner when moms_period is missing; BankID connect returns to the page instead of the report library. - Deadlines: callout (sv+en) when no system-generated tax deadlines exist — they are derived from tax settings, so point at /settings/tax rather than presenting an empty manual todo list. - Årsbokslut: "no räkenskapsår yet" (CTA to bookkeeping settings) is now distinguished from "nothing to close yet". - Skattekonto: non-auth fetch failures render an error card with retry instead of the misleading "inget saldo hämtat ännu" empty state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatt): address bot review — res.ok guard, blocking moms_period gate, panel hidden while räkenskapsår unresolved - Auto-fetch treats non-2xx or unparsable responses as errors (with retry) instead of rendering undefined data. - momsPeriodMissing now blocks the declaration (EmptyState + settings CTA) rather than fetching a guessed quarterly period behind a banner — a declaration submittable for the wrong period type is a hazard, not a convenience. - SkatteverketPanel is not rendered while yearly mode awaits a fiscal period, so its actions can never target an unconfirmed period. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
100a4d1291 |
feat(ux): create salary runs, employees & recurring schedules in modals (#883)
* feat(ux): create salary runs, employees & recurring schedules in modals The last three full-page create flows move to ?new=1 URL-driven dialogs, matching the verifikat/invoice pattern (#861): - Salary run: 4-field form on /salary — creation was pure interruption before landing on the run-detail workspace. - Employee: the last register entity still page-based after customers, suppliers, and articles. - Recurring schedule: consistency with the invoice modal it feeds. Old /new routes survive as redirects so bookmarks and agent intents keep working. Dialogs close explicitly (header X / Avbryt) so half-typed forms survive stray Escape or backdrop clicks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move DECISIONS.md to repo root dev_docs/ is gitignored, so the decision log was invisible to other developers. Root matches the existing convention (CONTRIBUTING.md, SECURITY.md). CLAUDE.md pointer updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lint): ignore Claude Code worktrees in eslint walk .claude/worktrees/ holds full repo copies; without the ignore, local npm run lint / check:lint walks them until the ratchet's 64 MB JSON parse buffer overflows. CI is unaffected (no worktrees there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
237b77a366 |
feat: custom inbound mail domains, rot/rut payout file, invoice email texts, security hardening (#878)
* fix(security): guard MCP test keys, RLS role gate + voucher RPC guards, /api MFA gate, deps - MCP: force dry-run / block writes for test-mode API keys in tools/call (extensions/general/mcp-server) - DB: current_user_can_write role gate on write policies (40 tables) + tenant guards, SET search_path, REVOKE anon on commit_journal_entry / next_voucher_number / detect_voucher_gaps (migration 20260702093000) - Middleware: MFA (AAL2) gate on cookie-authenticated /api routes via apiPathSkipsMfaGate - Deps: npm audit fix clears mailparser/linkify-it/nodemailer/svix/uuid highs; xlsx -> SheetJS 0.20.3 Adds unit + pg-real tests. Does not touch in-progress ROT/RUT or invoice-email-texts work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): rot/rut begäran om utbetalning — HUS XML (V6), payout tracking + settlement, MCP tool Generates Skatteverkets begäran-om-utbetalning file (schema V6) from paid ROT/RUT invoices — no submission API exists, the file is uploaded manually at skatteverket.se. Headless by design for now: API routes + MCP tool (gnubok_generate_rot_rut_file), no UI surfaces. - lib/invoices/rot-rut-file.ts: pure XML generator with deterministic per-invoice blockers (hours, work type, personnummer, property info, mixed rot+rut, XSD limits) + 31 January deadline warnings - rot_rut_payout_requests(+items) tables: one active begäran per invoice (DB triggers incl. reactivation guard), RLS, audit, pg-real tests - Settlement: POST /settle books debit 1930 / credit 1513 via the engine (source_type rot_rut_payout); partial payouts → partially_paid - Work-type lists corrected against Begaran.xsd: IT-tjänster is rut-only, snöskottning/tillsyn/tvätt added (schablontjänster utfört-only) - Fix: invoice-level fastighetsbeteckning was validated but never persisted — now stamped onto rot lines in build-invoice-write; API accepts bostadsrätt pair (lägenhetsnr + BRF orgnr, editor UI deferred) - invoice_items.brf_org_number migration + MCP scope invoices:write Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): per-company editable invoice email texts Add an "E-posttexter" section under Settings -> Fakturering where the subject, greeting, body and sign-off of the standard invoice email can be customized per company in Swedish and English. Fields pre-fill with the standard texts and only diffs from the standard are stored (company_settings.invoice_email_texts JSONB), so future improvements to the stock wording still reach companies that have not customized. Each field has a reset-to-standard button; cleared fields snap back. Texts support a fixed placeholder set (invoice number, customer name, first name, company, due date, amount) substituted at send time in a single pass; unknown placeholders stay literal. Custom texts are HTML-escaped after substitution, newlines become <br> in the HTML variant, and subject lines are flattened to a single header line. Overrides apply to standard invoices only - credit notes, proforma and delivery notes keep the stock texts. All send paths (UI, v1 API, MCP approval, recurring) pick the texts up via the existing settings row. The Zod schema half of this change (InvoiceEmailTextsSchema in lib/api/schemas.ts) was inadvertently included in 8291f745. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(documents): accept PDFs with preamble before %PDF- header, surface content rejections as 400 detectFileMagic required the %PDF- signature at byte 0 (BOM aside), rejecting genuine PDFs that carry a leading newline or junk bytes — files every ISO 32000 reader opens fine. Now scan the first 1024 bytes for the signature, matching real-reader behavior. Image types stay strict at offset 0 to keep the anti-placeholder defense tight. Magic-byte rejections were also mislabeled as DOC_UPLOAD_STORAGE_FAILED (500 'Filen kunde inte sparas'), blaming storage for a client-side file problem. Both upload routes now map them to a new DOC_UPLOAD_INVALID_CONTENT (400) with an accurate message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): full keyboard flow for manual journal entry Enter now drives the whole verifikat flow: verifikationstext drops into the first row missing an account, konto commits advance to debet, Enter on an empty debet hops to kredit, and an entered amount jumps to the next row. Once the voucher balances, Enter opens the review (unchanged gate) and the auto-focused confirm posts it — including through the no-underlag warning dialog. Escape in the inline review goes back to the form. Also fixes an Enter footgun in AccountCombobox: a bare Enter on a freshly focused field no longer selects the first account in the list — selection now requires typing or arrow navigation; otherwise Enter re-commits the current value or bubbles to the form-level handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add custom inbound domains management for companies - Implemented functionality to allow companies to claim and manage their own inbound email domains via Resend's API. - Created a new table `company_inbound_domains` to store domain information, including status and DNS records. - Added necessary RLS policies to restrict access based on user roles (owner/admin). - Developed functions for domain normalization, validation, claiming, verification, and removal. - Implemented webhook handling for domain status updates from Resend. - Added comprehensive tests for RLS, constraints, and triggers related to the new domain management feature. * fix: address PR #878 review findings and CI failures - migrations: drop the ai_usage_tracking policy block from the role-gate migration — the table was removed by 20260504120000_remove_ai_subsystem and only lingers on staging as drift; a from-scratch chain (pg-real, Supabase preview) failed on it - invoice-inbox: never flip a custom domain to verified off a domain.updated webhook alone — confirm the receiving capability with Resend first (fail-closed); normalize both sides of the orphan-adoption domain match - rot/rut: block files where begärt belopp exceeds what the buyer paid (DEDUCTION_EXCEEDS_PAYMENT); tighten brf_org_number validation to real orgnr shapes; parameterize the settlement bank account (19xx, default 1930) - rot/rut routes: log acting user on financial mutations, stop swallowing item mirror errors, narrow response projections (no customer ids through the invoice join); document the deliberate inline-XML decision - documents: stop echoing raw storage-layer error messages to clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: round-2 CI + compliance findings on PR #878 - migrations: the role-gate migration targeted automation_webhooks, which 20260515170000_webhooks_v2 renamed to webhooks on the canonical chain (staging kept the old name — drift); gate public.webhooks instead, dropping legacy schema-sync policy names defensively. Restore the 20260623130000 owner fallback in next_voucher_number that the stale copied-verbatim body silently reverted (caught by engine.pg locally). Full migration chain verified from scratch against supabase/postgres:15. - mcp: bump the tools/list payload ceiling 44K -> 45K — main's #877 qualified-identifier schemas plus this branch's rot/rut tool crossed the ceiling only in combination; documented in the test's history log. - rot/rut: refuse partial settlement before Skatteverkets beslut is recorded (would bypass the PATCH lifecycle and strand the request); block zero-kronor ärenden (ZERO_DEDUCTION); require sekelsiffra 16 on 12-digit brf orgnr in both schema validation and normalizeBrfOrgNr Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: rename branch migrations off main's colliding versions After the merge with main, two versions were shared by two files each (20260702100000: rot_rut_payout_requests vs company_settings_dimensions_ enabled; 20260702130000: invoice_email_texts vs pending_operations_add_ create_dimension_value). psql-based CI applies by filename and doesn't care, but Supabase branching records migrations by version (PK) — the second file with the same version breaks the preview with a schema_migrations_pkey duplicate. Neither branch migration is version- recorded on staging or prod, so renaming to fresh 20260703 versions is safe; nothing between the old and new positions depends on these objects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): scope the /api MFA-gate bypass to real Bearer-auth surfaces Any Authorization header — attacker-controlled — used to skip the AAL2 gate for every /api route, so a stolen-password AAL1 cookie session could reach cookie-authenticated routes (which ignore the header) by attaching `Authorization: x`. The skip is now scoped to the surfaces whose auth contract IS the header (/api/v1 API keys, the MCP endpoint's OAuth tokens); pure Bearer callers elsewhere (cron secret, signed webhooks) carry no cookie session and were never touched by the gate, which only fires for cookie users. Superagent P2 on PR #878. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: normalize path separators in dimension statutory guard scan The route scan compared walked file paths against a POSIX-path allowlist, so the suite failed on Windows (backslash separators) while passing on Linux CI. Normalize the scanned paths to forward slashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
678f2ccffd |
feat(mcp): P2 hygiene — honest category suggestions, skill-reference lint, cadence copy (#882)
* feat(mcp): counterparty-tied category suggestions + no_signal (P2-1)
suggest_categories padded every transaction with a company-wide
category-frequency fallback at <=0.5 confidence — an identical four-way
spread on 20+/24 items that agents correctly reported as pure noise
(agent.feedback). Real signal came from memory atoms and query_journal.
- History is now counterparty-keyed: buildMerchantHistory groups past
categorized transactions by normalized merchant; the engine only
surfaces history for THIS transaction's merchant, with provenance
('Bokförd N gånger tidigare för denna motpart') and occurrence-scaled
confidence (0.56 at 1x, capped 0.85). No global padding — an empty
list is the honest answer.
- The MCP tool returns no_signal_transaction_ids for transactions where
NO source matched, steering agents to investigate (query_journal)
instead of pattern-matching on unrelated rows.
- Both callers (REST suggest-categories route + MCP tool) share the new
helpers, so web UI and agents improve together.
Part of dev_docs/mcp_optimization_plan.md (P2-1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): dangling-reference validation in skills:check + fix 10 dangling links (P2-2)
skills:generate/check now fail when an atom SKILL.md links a
references/*.md that does not exist on disk — a dangling pointer ships
a 404 to every agent that follows it (the weekly-booking-check
incident, agent.feedback).
The validator immediately caught 10 live dangling links in 4 atoms,
three distinct flavors:
- filename typo: swedish-asset-accounting/references/depreciaton.md
renamed to depreciation.md (the link was right, the file misspelled)
- link mismatch: swedish-e-invoicing linked market-providers-pricing.md;
the file is market-provider-pricing.md (link fixed)
- unauthored plans: single-shareholder-ab-fmb TODOs and reklambyra's
'planerad utbyggnad' section used resolvable references/ paths for
files that were never written — rephrased as plans without paths
Seed migration regenerated (4 atoms bumped, renamed reference child).
Part of dev_docs/mcp_optimization_plan.md (P2-2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(events): align agent-feedback review cadence copy (P2-4)
gnubok_feedback replies 'we aggregate signal weekly'; the event-log
handler comment said quarterly. One of them was lying — weekly wins
(the mcp_optimization_plan triage is the living example).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b27f6cdb04 |
fix(db): NULL-safe tenant guards via caller_is_company_member + mechanical sweep (#881)
The house guard 'p_company_id NOT IN (SELECT public.user_company_ids())' skips the deny branch on UNKNOWN (NULL on either side). Not exploitable today (company_members.company_id is NOT NULL) but a NULL p_company_id passes the guard, and the shape fails silently under change. 9 live functions carried it (link_*_to_voucher, reserve/release_voucher_range, mark_entry_as_opening_balance, retag_line_dimensions, ensure_company_dimensions, company_has_capability, rotate_company_inbox). - caller_is_company_member(uuid): NULL-safe membership predicate (NULL -> false, always). - Mechanical rewrite: every public function carrying the raw pattern is re-created via pg_get_functiondef with the guard swapped — deliberate over hand-copying 9 bodies (the stale-copy hazard behind the 07-03 constraint clobber). Probe-validated locally: pattern swapped, NULL denied. - pg-real ratchet: after full replay no public function may contain the raw pattern (also blocks future reintroduction); detector self-test; helper semantics (member/foreigner/NULL). Existing tenant-guard suites re-assert deny semantics on the rewritten functions in CI. Part of dev_docs/mcp_optimization_plan.md (P2-3 follow-up, PR #872 review). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea236cbcdf |
fix(reconciliation): Bankavstämning phase 0 — correctness + feedback batch (+ nav IA regrouping) (#879)
* feat(nav): interaction-mode sidebar grouping — Arbeta/Analys/Data/Skatt & bokslut Nav IA redesign phase 0 (dev_docs/nav_ia_redesign.md): same routes, regrouped by what the user is doing. CLAUDE.md restructured around Hard Rules (doc references updated); pending-page explainer removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): correctness + feedback batch for Bankavstämning (phase 0) Engine: fetchAllRows pagination on status/run/RPC fetches (silent 1000-row cap corrupted totals), optimistic-lock guards on manualLink + apply, unlink audit rows attributed to the acting user (was: company UUID), selected_matches partial apply intersected with a fresh match run. View: silent in-place refresh instead of a full-page skeleton per action, checkbox-gated apply with confidence badges (fuzzy unticked) in chunks of 500, honest result toasts, dry-run errors surfaced, ranked per-row picker candidates pinned to the applied date window, currency-correct amounts (bank side in account currency, GL side SEK), voucher links, translated source types, colored differens, dirty-date-filter guard. Discovery: year-end preflight 404 href fixed (/reconciliation/bank never existed), ⌘K palette entry, real links from the transactions page. v1: status registry schema now matches the actual ReconciliationStatus payload, errors documented as a count, false ~0.85-threshold pitfall replaced, route test mocks the real shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59ecaee650 |
feat(mcp): qualified identifiers in all tool output schemas (P1-2) (#877)
Agents grabbed the wrong id when list rows exposed a bare 'id' next to qualified ids (journal_entry_id) with no type distinction, got NOT_FOUND, and had to re-derive (agent.feedback). Additive sweep of all 11 bare-id sites: every output identifier now ships a fully qualified sibling (transaction_id, fact_id, atom_id, company_id, dimension_id, dimension_value_id); the bare 'id' stays as a deprecated alias for compatibility. New ratchet test (qualified-ids.test.ts): a shrinking grandfathered list carries the deprecated aliases; any NEW bare 'id' in an output schema fails CI, every remaining alias must ship alongside its qualified sibling, and stale grandfather entries must be pruned. Convention documented in .claude/rules/mcp-server.md. Full unit suite: 6,577 green. Part of dev_docs/mcp_optimization_plan.md (P1-2). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
21512db81a |
feat(mcp): unify missing-document surfaces on one predicate (#876)
* feat(mcp): unify missing-document surfaces on one predicate (P1-3) The two MCP surfaces told different truths: the transactions tool keyed 'has underlag' on transactions.document_id while the verifikat tool keyed on document_attachments — and neither respected the source-type semantics, version chains, or journal_entry_no_doc_required waivers that lib/worklist's canonical count applies. Measured on prod: 22,046 waived verifikat still listed to agents, 2,370 doc-exempt source types listed, ~87 docs attached to transactions but never propagated to the verifikat, 1,100 transactions flagged missing-receipt although their verifikat HAS the underlag. One predicate now lives in SQL — posted, needs-doc source type (mirrors NEEDS_DOC_SOURCE_TYPES), no current-version doc, no waiver: - verifikat_without_documents RPC v2 adopts the canonical predicate. - New transactions_without_documents RPC: the bank-driven subset of the same predicate, joined through transactions.journal_entry_id — a strict subset of the verifikat surface by construction. Rows expose qualified transaction_id (P1-2 forward-compat); bare id deprecated. - Both tools become thin RPC wrappers; descriptions state the actual set relationship. - lib/worklist countVerifikatMissingDocument delegates to the RPC (previously three full-table pulls set-differenced client-side) — badge count and agent surfaces can no longer drift. - Backfill: propagate transaction-attached docs to their verifikat where the attachment was never linked (open periods only; never steals a doc linked to another verifikat). pg-real: fixture matrix (no-doc/with-doc/waived/stale-version/ doc-exempt-source/import), strict-subset assertion, per-source-type pin of the SQL list against the TS constant, tenant guard. Part of dev_docs/mcp_optimization_plan.md (P1-3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(mcp): explicit grants restated + count-call comment (#876 review) - Restate REVOKE/GRANT on verifikat_without_documents so the migration is self-contained (CREATE OR REPLACE preserves the 20260703130000 grants — verified on prod: authenticated + service_role only). - Comment on the p_limit:1 count call: total_count is computed over the full filtered set, independent of page size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
250cc7c450 |
feat(mcp): always-explicit retryable on structured errors + transient inference (P1-1) (#875)
Agents could not distinguish 'keep retrying' from 'stop, this is
broken' (agent.feedback): retryable was emitted only when a registry
entry declared true — absent otherwise, including for genuinely
transient DB/network failures whose SQLSTATE is lost when tools wrap
them as Error('Database error: ...').
- StructuredError.retryable is now a required boolean. Registry
declaration wins; otherwise isTransientFailure() infers from Postgres
SQLSTATEs (40001/40P01/57014/08xxx/53xxx/55P03), upstream HTTP
statuses (408/429/5xx), and message signatures that survive wrapping
(deadlock, serialization, statement timeout, fetch/socket failures).
- Unclassified transient failures surface as stable code
TRANSIENT_ERROR (new registry entry, retryable: true).
- categorize_transaction accepts idempotency_key — the tool agents
blind-retry after client-side approval-elicitation drops; the key
makes that retry replay-safe instead of double-staging.
- Contract documented in .claude/rules/mcp-server.md. The planned
'kind' field was dropped: the code registry already encodes it;
retryable is the agent-actionable bit.
Every tool error already flows through the single dispatch point
(toToolError -> getStructuredError), so coverage is universal without
per-tool migration. Full unit suite: 6575 tests green.
Part of dev_docs/mcp_optimization_plan.md (P1-1).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
001de3c844 |
feat(mcp): structured supplier-resolution failure with candidates in create_supplier_invoice_from_inbox (#873)
* feat(mcp): structured supplier-resolution failure with candidates in create_supplier_invoice_from_inbox When supplier resolution failed (no match on id, org_number, or exact name), the tool threw an opaque error — a dead end for the inbox pipeline on small ad hoc vendors, which are most of the backlog (agent.feedback). The error prose did mention supplier_id_override, but gave the agent no candidate ids and no machine-readable next step. Resolution failure now returns the staged-operation envelope with staged:false: preview carries the extracted supplier identity and near-miss candidates (normalized-name + org-digit matching — catches punctuation/legal-suffix OCR variants like "Polarn o Pyret" vs "Polarn O. Pyret AB", and formatted org numbers '556677-8899' vs '5566778899'); next hints either retry-with-override on the best candidate or a prefilled gnubok_create_supplier. Fuzzy scores never auto-resolve — the agent confirms against the underlag. Part of dev_docs/mcp_optimization_plan.md (P1-4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): review fixes — EF org-number forms, candidate-pool truncation flag, override tenancy validation Addresses the three substantive review-bot findings on #873: - orgNumberKey(): canonical 10-digit key. Swedish orgnr is exactly 10 digits; enskild firma org numbers are personnummer that appear in both 10- and 12-digit forms — exact digit-equality missed that legitimate match, and >= 10 accepted non-orgnr garbage lengths. - No silent caps: preview.candidate_pool_truncated + message note when the 500-supplier candidate pool is hit. - The defaults fetch now validates supplier existence in THIS company on every resolution path and rejects a bad supplier_id_override with a clear error — the unresolved next-hint actively promotes overrides, so a bogus id must fail at staging, not opaquely at commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
28827c2613 |
feat(dimensions): voucher-level retro-tagging workbench — reversal pairs hidden by default (#874)
* feat(dimensions): voucher-level retro-tagging workbench, reversal pairs hidden by default UX rework of the BulkTagWorkbench (follow-up to #867): the verifikat is now the unit of work, matching how users think ('that invoice belongs to project X') and how every Swedish bookkeeping tool presents entries. - /api/dimensions/tagging/lines returns voucher-grouped results via a two-step query: filters select QUALIFYING vouchers (line-level predicates become 'voucher has such a line' through the inner join), then the complete line set for each — tagging a voucher always covers the whole verifikat, never the filtered subset of one - reversal pairs are EXCLUDED by default: an annulled entry and its storno net to zero in every dimension bucket as long as both sides carry the same tag, so retro-tagging them is a no-op with an asymmetry foot-gun attached; 'Visa annullerade' opts them back in, and the blocking motverifikat confirmation survives only in that view (correction rebooks remain taggable — only the original+storno pair is hidden) - voucher rows: label, description, date, line count, distinct tag chips + 'Delvis taggad' state, single total amount (no debit/credit columns); chevron expands to per-line rows (signed amount, per-line checkboxes) for the mixed case (a voucher split across projects) - selection stays line-id based under the hood (the retag RPC is per line); shift-click ranges operate on vouchers; apply groups by resulting map and now chunks to the apply route's 500-line cap; failed vouchers auto-expand with their Swedish RPC errors inline - 'endast otaggade' now means 'vouchers with at least one untagged line'; the cap counts vouchers (default 150, max 300) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pull counter-vouchers into the annulled view even outside filters (review) Without this, a pair leg whose counter fell outside the date range would show no motverifikat warning at all — one-sided tagging would slip through silently, exactly the Srf U 14 skew the guard exists to prevent. Also scope the line fetch explicitly through the parent company filter (defense in depth). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
45438d4d64 |
fix(mcp): SQL-side filtering and pagination for list_verifikat_without_documents (#872)
The tool applied min_amount in memory after the PostgREST .range() page: total_count ignored the filter, and next_offset advanced by the filtered row count while the DB page consumed 'limit' rows — consecutive pages overlapped and full backlog coverage could not be proven (reported via agent.feedback). gross_amount is an aggregate over journal_entry_lines that PostgREST cannot filter on, so filtering/counting/pagination move into a new verifikat_without_documents RPC (SECURITY DEFINER with the PR #625 tenant-guard pattern; id tiebreak for total ordering; filter-respecting total_count). Also indexes document_attachments.journal_entry_id, which the anti-join and the link tools both hit. pg-real invariants: disjoint + complete pages under min_amount, filter-respecting totals, since filter, doc/draft exclusion, tenant guard both ways. Part of dev_docs/mcp_optimization_plan.md (P0-2). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b5fe9ee57 |
fix(mcp): add link_document_to_voucher to pending_operations CHECK constraint (#871)
The gnubok_link_document_to_voucher tool shipped with its executor and risk tier but its operation type was never added to the pending_operations_operation_type_check constraint. Every real staging INSERT failed with check_violation while dry_run previews were clean (the INSERT is skipped). Reported 3 times by 2 companies via agent.feedback; blocked Bokio attachment migration. Adds a pg-real audit test that extracts every op type staged in server.ts plus all OPERATION_RISK_TIERS keys and asserts each is accepted by the constraint, so a staging tool can never again ship without its constraint expansion (and a stale expand-types migration can no longer silently drop a type). Part of dev_docs/mcp_optimization_plan.md (P0-1). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fb3f0a9cee |
feat(dimensions): PR9 cutover — cost_center/project become GENERATED columns, dual-write removed (#870)
The dual-write window ends (dev_docs/dimensions_implementation_plan.md PR9): journal_entry_lines.cost_center/project are now GENERATED ALWAYS AS (NULLIF(dimensions->>'1'/'6','')) STORED — divergence from the bag is impossible by construction instead of by convention. - migration 20260702230000: drift pre-flight (refuses cutover on inconsistent data; prod verified 0 drift across 593k rows), column swap (DROP metadata-only + one-rewrite ADD pair), and atomic redefinition of the two SQL writers — retag_line_dimensions (SET dimensions only) and bulk_book_transactions (INSERT names the bag only) - TS writers stripped of the mirror spread: engine buildLineInserts (covers create/update/reversal), storno-service (reversal + correction), SIE import bulk insert, sandbox seed - lineDimensionColumns() removed from dimension-resolver — nothing derives mirrors in TypeScript anymore; normalizeLineDimensions + the deprecated cost_center/project INPUT aliases stay (API contract, they normalize into the bag); JournalEntryLine ROW type keeps the fields (generated columns still SELECT) - immutability carve-out unchanged BY DESIGN: its whole-row diff already subtracts dimensions/cost_center/project on both sides, which is exactly what makes it correct with generated columns (BEFORE-trigger NEW carries not-yet-recomputed mirror values) - audited every reader (v1 journal-entries, MCP query_journal filters + group_by, rc-basis-gaps) — reads are untouched; no index, view, or constraint referenced the TEXT columns, so DROP COLUMN cascades nothing - new pg suite: generated derivation, explicit-mirror-write rejection, draft-update recompute; existing retag/substrate/bulk-book suites updated to bag-only writes (their mirror assertions now exercise the generation expression) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
163fbd8222 |
feat(dimensions): PR8 salary — employees.default_dimensions, per-employee cost lines, aggregation re-key (#869)
Employees carry a default dimensions bag and the salary booking puts each
employee's cost on their kostnadsställe/projekt
(dev_docs/dimensions_implementation_plan.md PR8):
- employees.default_dimensions (migration 20260702220000; jsonb DEFAULT
'{}' + object CHECK)
- salary-entries: the one-line-per-account aggregation is re-keyed to
account+bag — P&L cost lines (löner incl. line items + base remainder,
arbetsgivaravgifter, semesteravsättning + dess avgifter, pension, SLP)
split per employee bag while every balance-sheet/settlement leg (2710,
1930, 2731, 29xx, 2740, 2514) stays aggregated; liability credits equal
the sum of the rounded debit buckets so entries balance by construction;
dimension-less runs book byte-identically to before. Replaces the dead
SalaryRunEmployee.cost_center/project pair (never wired)
- both book routes (dashboard + v1) read the bag via the employees join —
read-at-book, so the run review shows exactly what will book
- employee form (new + edit) gets a gated Kostnadsställe/Projekt card;
run review shows per-employee dims chips; run GET + v1 employee
routes/schemas + MCP list_employees carry the field
- pre-merge audit: all salary reports (salary-journal, AGI,
avgifter-basis, vacation-liability) read salary_run_employees — not
journal lines — and every ledger consumer sums per account, so the
line split breaks nothing; SIE export + dimension P&L pick the split
up as intended
8 new engine propagation tests + book-route dims flow test.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
755e0f7e47 |
feat(dimensions): PR7 producers — auto-tagged documents (invoices, supplier invoices, bulk-book, templates, MCP) (#868)
* feat(dimensions): PR7 producers — invoices/supplier invoices carry dims, generators propagate, BulkBook + templates + MCP bags
Source documents now carry dimension tags and every entry generator
propagates them onto journal lines (dev_docs/dimensions_implementation_plan.md PR7):
- invoices/supplier_invoices.default_dimensions + per-item dimensions
(migration 20260702200000; jsonb DEFAULT '{}' + object CHECK)
- invoice-entries: issuance/payment/cash/credit propagate — item bags merge
over the invoice default per revenue line (account+bag aggregation
identity), payment vouchers re-propagate the linked invoice's bag onto
every leg incl. FX result lines; ROT/RUT 1513 carries the item bag
- supplier-invoice-entries: registration/payment/cash/privately-paid/credit
propagate with the same merge rules (expense buckets keyed account+bag)
- bulk_book_transactions RPC persists per-line bags + derives
cost_center/project mirrors in SQL (migration 20260702201000; malformed
bags rejected with BULK_BOOK_INVALID_DIMENSIONS); route merges the header
default into template/manual lines
- counterparty templates: LinePatternEntry.dimensions learned from SIE
voucher history (kept only when every occurrence agrees), applied to
business lines on booking; QuickReviewDialog shows a dims badge
- categorize: staged dimensions bag tags business lines only (bank/VAT
untagged); credit/convert/inbox copy paths carry bags forward
- propose-payment/send-lines stamp the invoice default so the editable
payment grid books what the preview shows; mark-paid override lines
accept dimensions
- UI: InvoiceEditor + NewSupplierInvoiceForm header KS/Projekt pair with
per-row override; BulkBookDialog header default pair (both tabs)
- MCP: default_dimensions/items[].dimensions on create_invoice +
create_supplier_invoice_from_inbox, dimensions on categorize_transaction,
per-line bags on bulk_book_transactions — resolve-don't-select via the
shared registry helpers, resolutions echoed
32 new propagation unit tests + 4 pg-real tests for the RPC migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: use roundOre in new dims rounding assertions (ratchet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: copy dimension bag per payment line, document dimensionsBagKey normalization contract (review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
816b1769c8 |
feat(dimensions): PR6 retro-tagging — audited retag carve-out, BulkTagWorkbench, staged MCP tool (#867)
* feat(dimensions): PR6 retro-tagging — audited retag carve-out, workbench, staged MCP tool
Tier-2 retro-tagging (founder decision №1, approved 2026-07-02): posted
entries in OPEN periods can have their dimension tags changed through ONE
audited path — everything about the verifikat itself stays immutable.
Carve-out (migration 20260702170000): the line-immutability trigger gains a
single narrow branch — while the transaction-local GUC set by the RPC is
active, an UPDATE of a posted line is admitted iff every non-dimension
column is unchanged, enforced by a whole-row to_jsonb diff (any future
column is protected by construction; mirrors cost_center/project are in the
changeable set because they are derived views of dimensions['1']/['6']).
Precedent: mark_entry_as_opening_balance (20260613120000).
retag_line_dimensions RPC: tenant guard (20260619130100 pattern), writer
gate (viewers rejected), posted-only, open period + company lock date
enforced, every code validated against the ACTIVE registry, immutable
dimension_retag_log row (before/after/actor/reason, INSERT-only via its own
trigger, no FKs so the trail survives hard-deletes) written BEFORE the
carve-out UPDATE. Idempotent no-op without a log row. Untag ({}) supported.
Legal position per the plan: dimensions are internredovisning metadata, not
BFL 5 kap 7§ verifikat content — this is strictly more conservative than
Fortnox/Visma (dimension-only diffs, open periods only, immutable log,
storno past locks — Tier 3 has no exceptions).
Mandatory pg suite (11 tests): GUC-less updates still blocked; amounts/
description can never change even under the GUC (transaction-local);
closed/locked/lock-date, role, registry, draft and cross-tenant rejections;
log immutability; gnubok.allow_delete bulk path unaffected.
UX (all writes through the ONE RPC): pencil on posted-voucher lines in
bookkeeping/[id] ("Påverkar endast internredovisningen, inte verifikatet")
+ retag-history card; BulkTagWorkbench at /dimensions/tagging (filters,
shift-select, merge vs "Ersätt tagg" replace mode, reversal-pair warning
with "Inkludera motverifikat" auto-selection, per-line failure display).
MCP: gnubok_tag_journal_lines (bookkeeping:write) — filter block resolved
via resolve-don't-select, ≤500 lines, staged via pending_operations (new
op type migration 20260702171000, medium risk tier, shared Zod validation
boundary between staging and commit; executor loops the RPC per line with
partial-success aggregation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address #867 review — SQLSTATE classification, blocking storno confirm, documented divergence
- Retag route classifies RPC errors by SQLSTATE instead of message-regex:
P0001 (every rule violation in the RPC) → 409 verbatim, 42501 (tenant
guard) → 403, anything else → logged 500 with a generic message. No more
substring sniffing.
- The workbench's storno-pair warning escalates to a BLOCKING confirmation
naming the unselected counter-vouchers before apply (Srf U 14 gross
reporting — one-legged retags silently skew project P&L; the banner alone
was advisory).
- The empty-bag divergence is now documented on both schemas as intentional:
the direct dialog/workbench path allows {} (human untags phantom codes,
logged with reason), the MCP staged path rejects it (agents never
bulk-clear history).
Triage notes: the log's missing FKs are the point (behandlingshistorik must
survive undo_sie_import hard-deletes — a cascade would erase the trail);
SIE exports are generated fresh on demand, never cached, so post-retag
exports carry the new object lists automatically; date-scoped registry
values are deliberately not enforced at retag because entry creation does
not enforce them either — enforcing in one path only would be incoherent
(both belong to the PR10 rules engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
fb3fe82a56 |
feat(dimensions): PR5 SIE round-trip — lossless dimension import + undo lockstep (#866)
* feat(dimensions): PR5 SIE round-trip — lossless dimension import, registry upsert, undo lockstep
SIE import previously parsed and silently DISCARDED all dimension data
(object lists at sie-parser.ts:651-654, #DIM/#OBJEKT in the ignore list at
:689). Import is now lossless — the dimensions plan PR5 milestone.
Parser: #TRANS object lists ({1 "KS01" 6 "P001"}) land on the line as an
SIE-dim-no → code map (canonical numeric keys, quoted codes, malformed
pairs warn); #DIM/#UNDERDIM/#OBJEKT parse into registry records. OIB/OUB
stay ignored (dimension reporting is P&L-only in v1).
Importer (lib/import/sie-dimensions.ts): upserts missing dimensions/
dimension_values rows — never renames existing ones (ON CONFLICT DO
NOTHING); undeclared reserved numbers synthesize their SIE-standard names
(mirroring the export's orphan synthesis); codes violating the registry
CHECK are skipped with a warning but survive verbatim on lines (documented
legacy-free-text exception). Bulk voucher insert now writes the dimensions
jsonb + cost_center/project mirrors via the sanctioned dual-write helpers
(no trigger suppression needed — the immutability trigger guards
UPDATE/DELETE, not INSERT). Import auto-enables dimensions_enabled with a
result-card notice (pre-authorized by the column comment). arcim-migration
provider syncs inherit all of it via the shared parser/importer.
Undo lockstep (migration 20260702154500): created_by_import_id provenance
on both registry tables (ON DELETE SET NULL); undo_sie_import deletes the
values/dimensions the undone import introduced when no remaining
posted/reversed line references them — user-created rows and rows other
bookkeeping references are untouched. The registry guard triggers act as
backstop. replace_sie_import deliberately skips the lockstep (re-import
re-upserts the same codes). Six pg-real tests cover the lockstep.
Round-trip pinned by test: parse → import state → export → parse preserves
declarations (#UNDERDIM parent links included), values, and per-line object
lists — including synthesis of referenced-but-undeclared values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): restate function-local statement_timeout on undo_sie_import
CREATE OR REPLACE resets proconfig, so the 290s timeout from 20260629160100
was silently dropped — regressing service-client bulk deletes to the
authenticator role's 8s limit. Caught by sie-import.replace.pg.test.ts in CI.
Full pg-real suite green (483/483, TZ=UTC).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): surface OIB/OUB drops and dimension presence as parse-level info (#866 review)
Dropping object-balance records must never be silent — one info issue counts
the skipped #OIB/#OUB rows (object-level balances are P&L-out-of-scope in
v1), and a second announces dimension data before the user executes the
import (the preview step renders parse issues), so the auto-enable notice is
no longer purely post-hoc.
Triage notes for the remaining findings: the RPC's opening SELECT is the
company-ownership check the swarm asked for; registry writes are RLS-bound;
line-verbatim codes are the documented legacy-free-text exception; export
emits no #KSUMMA so there is nothing to recompute; SIE dims 3–5 are
"reserved for future use" with no standard names, so generic synthesis is
spec-correct; ON DELETE SET NULL is deliberate — provenance is operational
metadata for undo, not räkenskapsinformation (the guarded journal lines
are), and RESTRICT would block legitimate post-retention housekeeping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c7f0db00c8 |
fix(invoices): allowlist forwarded params on the /new redirects (#861 review) (#864)
Only inbox_item_id survives the redirect to the list-page modal — the list page reads nothing else, and arbitrary caller params must not pollute its query string. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
01dbef4015 |
feat(dimensions): PR4 reports — dimension-filtered P&L + Resultat per projekt/kostnadsställe (#862)
* feat(dimensions): PR4 reports — dimension-filtered P&L everywhere + Resultat per projekt/kostnadsställe The Project P&L milestone of the dimensions plan (dev_docs §7 PR4). One choke point lights up everything: generateTrialBalance gains options.dimensions (SIE dim → code map) pushed down as jsonb containment (dimensions @>, served by idx_jel_dimensions_gin) on both line queries, with company-wide opening balances dropped when filtered (they cannot be dimension-scoped; P&L-safe by whitelist). Resultatrapport, resultaträkning, huvudbok, monthly-breakdown and the TB drill-down inherit the filter; the KPI route filters only its P&L-side inputs (income statement, months, expense composition) — never cash/VAT. New report lib/reports/dimension-pnl.ts — "Resultat per projekt/ kostnadsställe" (Fortnox Resultatrapport projekt): value-as-column matrix over one dimension with an explicit "(Utan dimension)" bucket computed as the residual against the same trial-balance pass resultatrapport uses, so every row and the Totalt column reconcile with the unfiltered resultatrapport by construction. Registered in REPORT_CATALOG (visible only when dimensions_enabled), slug-routed view + xlsx export. UI: DimensionFilter (dimension + value picker, persistent "Filtrerad — ej fullständig rapport" chip) mounts in FocusedReport for catalog entries flagged dimensions: true; huvudbok rows show line dim codes. Statutory exclusion pinned by TEST, not convention: lib/reports/__tests__/dimension-statutory-guard.test.ts fails if the filter parser leaks into balance sheet, balansrapport, kassaflöde, VAT, SIE or full-archive routes/generators, or if the catalog whitelist widens. MCP: new gnubok_get_dimension_pnl (reports:read); dimensions filter arg on get_trial_balance/get_income_statement/get_general_ledger with resolve-don't-select (names → registry codes, resolution echoes); query_journal totals fixed to aggregate the FULL match set (was silently slice-scoped while claiming otherwise) with an honest totals_scope field, plus group_by / group_by_dimension aggregation. Also: voucher-detail dim-6 badge now uses the registry name instead of the non-standard "PR" abbreviation (#859 review follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dimensions): address #862 review — export disclosure, prior-column suppression, period-label honesty, route hardening - Filtered XLSX/PDF exports now carry the partial-view disclosure past the file boundary (BFNAR 2013:2): filename suffix (-dim6-p001), a "Filtrerad … — ej fullständig rapport" row on every sheet, and a header note/title line in the PDFs. - Resultatrapport drops the prior-year column when a dimension filter is active — project codes are time-limited under K2/K3, so "this code last year" may be a different project (same rule as narrowed date ranges). - dimension-pnl no longer accepts fromDate: the matrix is cumulative from period_start by design (closing-balance semantics), and the period label now states exactly that instead of echoing a lower bound that was never applied. Routes/MCP tool updated to toDate-only. - dimension-pnl routes 404 on an unknown/foreign period id and cap dim_no to 4 digits (matching the MCP tool's PostgREST-path guard, which the generator now also enforces itself). - Statutory-guard test's generateTrialBalance call-site scan is paren-aware instead of a 300-char window; added fully-untagged and injection-guard test cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86071334cb |
feat(invoices): create customer & supplier invoices in modals, matching the verifikat pattern (#861)
Invoice and supplier-invoice creation now open as pop-up dialogs on their list pages instead of navigating to standalone form pages — the same UX as NewJournalEntryDialog (capped-height scroll, explicit-close-only so a half-typed invoice survives stray Escape/backdrop clicks). - InvoiceEditor gains a `bare` variant (page chrome stripped, inline actions replacing the fixed mobile bar, live document-type title kept) hosted by the new NewInvoiceDialog (sm:max-w-5xl). Draft editing pages unchanged. - The 2,057-line supplier form moves out of the route page into components/supplier-invoices/NewSupplierInvoiceForm.tsx with bare/inboxItemId/onCreated/onCancel props, hosted by NewSupplierInvoiceDialog (sm:max-w-4xl). - Modals are URL-driven (?new=1): header buttons, empty states, command palette, and the reports CTA all open the same dialog; browser back closes it. /invoices/new and /supplier-invoices/new survive as redirects (bookmarks, agent intents, /expenses/new alias, inbox deep links). - The invoice-inbox "Skapa leverantörsfaktura" action opens the modal in place and refreshes the inbox on success instead of navigating away. No new i18n keys; dialog titles reuse existing strings. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
11126d6d56 |
feat(dimensions): PR3 tagging — voucher-form pickers, MCP dimension tools with resolve-don't-select, engine soft validation (#859)
Phase 3 of dev_docs/dimensions_implementation_plan.md. Companies with dimensions_enabled=false see zero change; existing free-text API writers keep working (validation is toggle-governed). Engine (soft validation): - validateEntryDimensions() in dimension-resolver: zero queries for untagged entries; toggle off → passthrough; toggle on → one settings fetch + two registry queries, rejects unknown dims/codes and archived values with Swedish per-code messages (DimensionValidationError, 400, details.issues). Wired into createDraftEntry + updateDraftEntry before any insert; reversal/ storno paths untouched (verbatim copies). Fails open on transient registry errors — soft validation must never block bookkeeping. MCP (agent write path): - New tools: gnubok_list_dimensions, gnubok_list_dimension_values (fuse.js fuzzy), gnubok_create_dimension_value (STAGED via pending_operations — agents never silently mint reporting values; new op type + CHECK migration + executor with duplicate-idempotency). - create_voucher/correct_entry: per-line dimensions bag + default_dimensions, resolve-don't-select server-side (code OR natural-language name; exact → fuzzy ≤0.30 with ≥0.15 runner-up margin; non-exact resolutions echoed with confidence; ambiguous → ranked candidates, no auto-create). - gnubok_get_agent_briefing gains a dimensions block (enabled, dims, top values) — omitted when registry empty. - TOOL_SCOPE_MAP entries; risk tier low for staged value creation. UI: - JournalEntryForm (manual voucher + TransactionBookingDialog embed): header "+ Kostnadsställe/Projekt" progressive disclosure (gäller alla rader with documented inheritance rule) + per-row tag popover + compact KS·PR badges; gated on dimensions_enabled. - Voucher detail: display-only dimension badges with registry-name resolution. - EditDraftEntryDialog carries line dimensions so editing a draft no longer strips tags. categorize/bulk_book dims deferred to PR7 (needs the bulk_book RPC migration). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6be6510d73 |
fix(entitlements): gate paid AI document OCR server-side (free-tier leak) (#852)
* fix(entitlements): gate paid AI document OCR server-side (free-tier leak) Free/manual-tier companies could trigger paid Bedrock OCR (extractInvoiceFields) with no `ai` capability check, on every transport: - invoice-inbox HTTP paths — /upload + email /inbound (shared uploadAndExtract), /items/:id/attach-document, /items/:id/retry-extraction (4 call sites, zero capability refs); - the gnubok_upload_document MCP tool — absent from MCP_TOOL_CAPABILITY_MAP, so a free-tier API key (incl. the claude.ai connector's minted gnubok_sk_ key) got unlimited AI extraction. This disproved the keys.ts "no MCP tool invokes AI" comment. Fix (money-blocker for the free/paid tier cutover): - Gate the 3 inbox call sites on hasCapability(CAPABILITY.ai). Upload + attach degrade gracefully (document still stored; extraction skipped with reason `no_ai_entitlement`, highest priority in the existing skipReason chain). Retry is an explicit "run AI now" action, so it hard-blocks with 403 capabilityBlockedResponse. - Register gnubok_upload_document -> CAPABILITY.ai in MCP_TOOL_CAPABILITY_MAP; the dispatcher already enforces the map. Correct the stale keys.ts comment and the misleading "deterministic field extraction" tool description + manifest copy (the extension migrated regex -> AI OCR). Tests: no-AI upload/attach skip + retry 403 (sandbox-skip-extraction), retry 403 (retry-extraction), and the MCP map contract + refined dispatch<->commit parity (capability-maps: upload_document is dispatch-only, no commit counterpart). Self-hosted stays all-on (hasCapability short-circuits). No migration. Follow-up (not in scope): capability-blind DashboardNav (free/paid rails identical) and /chat gated on isVerified not `ai` — see dev_docs/nav_ia_redesign.md Part 4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): assert gnubok_upload_document is ai-gated at dispatch The gnubok_upload_document handler runs extractInvoiceFields (Bedrock OCR) inline rather than through the entitlement-gated uploadAndExtract, so the central MCP_TOOL_CAPABILITY_MAP dispatch check is the only paywall on that transport. Lock it with a test (flagged by PR review as an untested money path) so a free-tier connector key can never reach paid OCR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f675ae6565 |
fix(loops): committed playbook path + Sentry-first error source (#851)
* fix(loops): move playbook to committed .claude/loops.md; Sentry-first error source
dev_docs/* is gitignored ('internal reference, not published'), so the loop skills'
'read dev_docs/loops.md first' reference never resolves on main / in a fresh cloud
clone. Relocate the playbook to .claude/loops.md (committed) and repoint all 4 skills.
Rewrite loop-vercel-errors to use the wired Sentry API (the Vercel MCP is not in the
cloud routine tool allowlist) and document the required token scope (event:read,
project:read) + cloud-env secrets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(loops): Vercel loop is Vercel-native + local (Sentry is not wired up)
Sentry is not integrated in this codebase (no @sentry/* dep, no config, no
instrumentation, zero source refs) — the SENTRY_* names in .env.local and CLAUDE.md
are leftovers. Rewrite loop-vercel-errors to source errors from Vercel (Vercel MCP
locally; Vercel API with VERCEL_TOKEN in cloud) and reclassify it as a LOCAL loop,
since the Vercel MCP is only available locally and there is no error-aggregation
service. The cloud trigger stays disabled. Only GH_TOKEN is needed to provision the
cloud loops (1 & 3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8bb49c07a2 |
feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry (#858)
* feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry
Phase 2 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false (default) see zero change.
API:
- Dashboard CRUD: GET /api/dimensions (lazy-seeds system dims 1/6 via the
ensure_company_dimensions RPC), PATCH /api/dimensions/[id] (is_system
rename blocked), POST/PATCH/DELETE values (code immutable after creation;
strict Fortnox code format ^[A-Za-z0-9ÅÄÖåäö_+\-]{1,20}$ at the API layer;
retention-trigger deletes surface the Swedish "arkivera istället" message
as 409 DIMENSION_VALUE_REFERENCED).
- POST /api/dimensions/import-existing — scans journal_entry_lines.dimensions
for unregistered codes and mints inactive placeholder registry rows.
- v1 public API: GET dimensions + POST values (Idempotency-Key, dry-run),
registered in the OpenAPI spec (102→104 endpoints).
- dimensions_enabled boolean on company_settings (new migration,
UI-visibility only, never correctness-bearing) exposed through the
existing settings read/update path.
SIE export (lib/reports/sie-export.ts):
- Reads the new dimensions/dimension_values registry; legacy
cost_centers/projects tables now have zero readers (drop migration next).
- Fixes the latent Visma-rejection bug: #OBJEKT now declared for INACTIVE
values referenced by lines.
- Generic-N: #DIM/#UNDERDIM loop sorted by sie_dim_no; #TRANS object lists
serialize from the line JSONB map (sorted, '01'→'1' collapse); orphan
codes/dims synthesize declarations from the SIE reserved-number seed —
every referenced (dim, code) pair is guaranteed declared.
UI:
- /dimensions register (Register-recipe): tabs per dimension, search,
sortable table, value dialog (code immutable on edit, projekt dates on
dim 6), archive-not-delete affordances.
- DimensionCombobox shipped (mounts in the tagging PR).
- Settings toggle "Aktivera kostnadsställen & projekt" — toggle-on runs the
import-existing scan and links to the register.
- Nav row in redovisning, rendered only when dimensions_enabled (same
mechanism as pays_salaries).
- dimensions.* i18n namespace (51 keys, sv/en parity).
- Sandbox seed: demo dims + values, revenue line tagged {"1":"BUTIK","6":"P001"}.
Verified: 6328/6328 unit tests, guard + coverage gate green, tsc parity with
main (210=210), production build passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): PR2 review round — atomic archived-create, UNDERDIM ordering, import robustness, date semantics
- POST values accepts is_active so "create as archived" is atomic; the UI's
fragile create-then-PATCH fallback is deleted (PR Agent finding 1).
- DimensionCombobox blur revert reads the committed value/values through refs
so a selection landing inside the 150ms window always wins (finding 2).
- import-existing sanitizes candidate codes like the PR1 backfill and upserts
with ignoreDuplicates — one bad/duplicate code can no longer abort the
batch; created counted from returned rows (finding 3).
- SIE export emits all root #DIM before any #UNDERDIM so a parent always
precedes a lower-numbered child (SIE4 declaration order — Swedish review);
synthesized placeholder declarations now log one structured warning
(BFNAR 2013:2 behandlingshistorik) + defence-in-depth comment.
- Value dates rejected (400 DIMENSION_VALUE_DATES_NOT_ALLOWED) when the
parent dimension is flow-period (resets_annually=true); explicit null
still clears (Swedish review).
- Sandbox seed logs seeded dimension codes; GET /api/dimensions documents
the deliberate absence of dimensions_enabled gating (UI-visibility flag,
not a security boundary — compliance-swarm V8.2.1 rejected by design).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8cc2efb083 |
feat(dimensions): PR1 substrate — SIE-native registry + dimensions JSONB on journal lines (#857)
* feat(dimensions): substrate — SIE-native registry + dimensions JSONB on journal lines (PR1)
Implements phase 1 of dev_docs/dimensions_implementation_plan.md:
- New company-native registry tables: dimensions (= SIE #DIM/#UNDERDIM,
seeded is_system 1=Kostnadsställe / 6=Projekt via ensure_company_dimensions,
nullable bare firm_id) and dimension_values (= #OBJEKT), full RLS incl.
DELETE, audit + updated_at triggers, guard triggers (system dims undeletable,
sie_dim_no immutable, values referenced by posted lines archive-not-delete).
- journal_entry_lines.dimensions jsonb NOT NULL DEFAULT '{}' as the single
source of truth ({sie_dim_no: object_code}), CHECK object-typed, GIN
(jsonb_path_ops) + partial expression indexes on dims 1/6. Inherits posted-
line immutability from the existing trigger with zero new triggers.
- Backfill: representation copy of legacy cost_center/project text into the
JSONB map (trigger-disabled, schema_sync precedent); legacy cost_centers/
projects registry rows copied into dimension_values; inactive placeholder
values for orphaned free-text codes.
- Dual-write: engine buildLineInserts + storno/correction/date-move now derive
cost_center/project mirrors from the map via lib/bookkeeping/dimension-resolver.ts
(normalizeLineDimensions / lineDimensionColumns); reversal copies dims.
- CreateJournalEntryLineInput + shared Zod line schema gain a dimensions bag
(cost_center/project stay as deprecated aliases); pending-ops voucher lines
coerce it.
- CI ratchet: direct-jel-insert check in no-new-antipatterns.mjs — inserts into
journal_entry_lines outside sanctioned writers fail CI.
- pg-real suite: registry RLS/guards/retention, ensure_company_dimensions
tenant guard, dims frozen on posted lines, CHECK enforcement (13 tests).
Non-breaking: companies without dimensions see zero change; no UI yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address review findings — canonical keys, boundary-validated staged bags, migration guidance
- normalizeLineDimensions canonicalizes numeric keys ('01' -> '1') so
leading-zero keys can't split values or miss the cost_center/project mirrors
(PR Agent finding).
- New coerceDimensionsBag() in dimension-resolver is the single boundary
validator for untyped staged payloads, enforcing the same constraints as the
Zod line schema (string-only values, 1-40 chars, no SIE-framing chars,
canonical keys). pending-operations normalizeVoucherLines now uses it —
staged payloads can no longer bypass API-layer validation via numeric
coercion (compliance-swarm V2.2/V1.2.5/PI1.1, Swedish review finding 4).
- Migration backfill comment now spells out the exact conditions under which
the trigger-disable pattern is defensible (BFL 5:5 / BFNAR 2013:2) and what
a future reviewer must verify before reusing it (Swedish review finding 2).
- 10 new resolver tests incl. reversal-parity (empty bag + aliases ==
alias-only) proving the reverseEntry and storno paths normalize identically
(PR Agent finding 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): round-2 review — shared Zod schema, transactional backfill, empty-string guard
- DimensionsBagSchema now lives in dimension-resolver as the single source of
truth; CreateJournalEntryLineSchema and coerceDimensionsBag both delegate to
it, so the API layer and the staged pending-operations path provably cannot
drift (compliance-swarm V2.2). coerceDimensionsBag switches to whole-bag
semantics: any invalid entry rejects the bag, exactly like the API schema.
- Migration backfill now runs DISABLE TRIGGER / UPDATE / ENABLE TRIGGER inside
one transaction — the ACCESS EXCLUSIVE lock from ALTER TABLE holds until
COMMIT, so no concurrent writer can slip an unguarded line write into the
window during a live apply (compliance-swarm V1.2, Swedish review finding 1).
- NULLIF guard: empty-string legacy mirrors can no longer mint {"n":""}
entries the resolver would interpret as "cleared" (PR Agent round-2 edge).
- COMMENT ON dimensions.resets_annually documenting the SIE4 #IB/#OIB
semantics the PR2+ export path must honour (Swedish review finding 2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f63d3e3100 |
Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects We never sent auth_method to Enable Banking, so it fell back to the ASPSP's visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate* PSUs the redirect flow does not support Mobile BankID, so authorization failed right after the user approved in the BankID app. Mobile BankID at Handelsbanken is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses when requested explicitly. Resolve the bank's preferred auth method before /auth: query the ASPSP's auth_methods and pick the DECOUPLED (Mobile BankID) method when present, otherwise leave auth_method unset so banks that already work are untouched. The method name is read dynamically per psu_type, so it is robust across sandbox/production naming. - api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods field name (was available_auth_methods, never populated), add getPreferredAuthMethod(), thread optional authMethod through startAuthorization - index: resolve authMethod in /connect and pass it on both fresh + reconnect - tests: cover method selection and request-body shaping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(invoice-inbox): clean up bulk-selection toolbar UI Redesign the selection toolbar shown when inbox items are checked: one solid primary "Bokför valda" button with outlined secondary actions ("Fråga assistenten", "Ta bort") and a plain selection count. Removes the redundant "Avmarkera" button (users uncheck the still-visible box), fixes label clipping, and gives the toolbar more breathing room. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(entitlements): bypass paywall in local development Add isPaywallBypassed() so all gated capabilities are testable locally without a subscription. Fires only on NODE_ENV=development (npm run dev) or an explicit DISABLE_PAYWALL=true escape hatch — production builds run under NODE_ENV=production and the entitlement suite runs under 'test', so both keep exercising the real gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): implement categorize core for bank transaction categorization - Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations. - Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing. - Implemented fiscal period validation and duplicate booking detection. - Enhanced logging and error handling for transaction categorization. feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata - Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken. - Outputs metadata for business and personal PSU types, including default authentication methods. fix(migrations): increase statement timeout for SIE bulk delete operations - Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports. feat(migrations): add bulk book inbox items to pending operations - Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`. - Supports bulk booking of matched inbox items against bank transactions. test(pg): add tests for replace_period_opening_balance_link RPC - Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow. - Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries. * fix(sie-export): update journal entries and lines handling in SIE export tests * fix(migrations): resolve version collision on 20260629160000 The SIE bulk-delete statement_timeout migration shared version 20260629160000 with journal_entries_list_series_filter (merged from main via #798/#823), causing a schema_migrations_pkey duplicate key error on apply. Rename the branch's migration to 20260629160100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compliance): resolve compliance-swarm + review findings - opening-balance/correct: compensating rollback for the non-atomic storno+rebook so a mid-sequence failure never leaves two posted OB entries (ASVS V2.3); durable audit event on every failure path (V16); reference the original verifikationsnummer in the corrected entry per BFL 5 kap 5§; document that requireWrite already enforces write-role + membership (V8.2.1 was a false positive) - reports sources routes: validate the cursor date component as ISO (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2), applied to both the VAT-declaration and trial-balance routes - AgentSessionList: await the rename PATCH, revert the optimistic title and toast on failure (ASVS V4.5) - bank booking: exclude same-batch siblings from the booking-time duplicate guard so bulk-booking distinct same-(date,amount) transactions no longer false-positives; pre-existing duplicate detection is preserved - BulkBookInboxDialog: drop the unsafe currency-based reverse_charge default, add an omvänd skattskyldighet advisory, and type VAT options to the backend VatTreatment union - OpeningBalanceRowEditor: hold onChange in a ref (synced in effect, not during render) so an unstable callback can't cause a render loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2da9c71eb3 |
UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns: - style(ui): badge audit + cleanup across 45 files — real-status chips use Badge variants (raw Tailwind colors dropped), non-status count/type/label chips demoted to muted text, clustered badges consolidated; 20 unused imports removed. - feat(bookkeeping): Kontoplan moved to a dedicated /chart-of-accounts route (nav + command palette wired); /bookkeeping shows the journal list only. - chore(skills): loop-* automation skills + design-scan workflow under .claude/. fix(reports): restored the destructive count badge on blocking errors in the periodisk sammanställning (EC Sales List) — a genuine status cue the audit had wrongly flattened; flagged by the PR reviewer and Swedish compliance bot, now clean. All CI green; compliance bots report no findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d63d2aecf0 |
feat: UI slop cleanup, invoice icon/header polish + year-end in Rapporter, journal-list DataList refactor (#847)
UI cleanup: removed AI-slop (redundant suppliers subtitle, decorative Sparkles glyph), decluttered the article-detail header (single status badge + muted type · #number), standardized the invoice icon Receipt→ReceiptText (no $ in a SEK app), and matched ReportExportMenu trigger size to the primary CTA on list pages. Bookkeeping: surfaced year-end closing in Rapporter (catalog descriptor) and dropped the redundant header button; refactored JournalEntryList to DataList primitives + chunked /api/documents/counts in 50-ID batches (large pages previously 400'd); added optional fraction-digit overrides to formatCurrency. The fiscal-year lock indicator is preserved as a labeled Låst/Stängt badge in FiscalYearSelector. All PR-bot findings triaged as false positives (unused import, formatCurrency öre, lock indicator) or intentional design (year-end placement, empty-state messaging). CI green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
db843a7a5b |
fix(entitlements): gate paid MCP tools (send_invoice/agi_submit/vat_declaration_submit) server-side (#846)
The HTTP routes call requireCapability at every paid chokepoint, but the MCP/agent path bypassed the paywall entirely: the three external-service tools stage operations whose commit calls the email / Skatteverket services directly, with no capability check. After the 2026-07-07 trial cutover a trial-connected non-payer using the gnubok MCP connector could still send invoice emails and file AGI/VAT. Close the gap with two layers, mirroring the existing TOOL_SCOPE_MAP gate: - Dispatch gate (mcp-server/server.ts): MCP_TOOL_CAPABILITY_MAP, checked right after the scope check, blocks a non-entitled company before any pending op is staged. Emits errorKind='capability_denied' telemetry. - Commit-time gate (commitPendingOperation): PAID_OPERATION_CAPABILITY_MAP, checked before the atomic claim. The real external-service chokepoint — applies to the MCP approve tool AND the UI approval path, and closes the trial-connected-token window (the grant has expired by commit time). A blocked op stays 'pending', so it is re-approvable once the company subscribes. Adds a transport-free capabilityBlockedError() helper (shared bilingual copy) and locks both maps with tests (maps, dispatch gate, commit gate). Only the three write/submit tools are gated; SKV read/local tools stay free per the statutory carve-out. No DB/migration change; self-hosted stays all-on. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5df6199bd1 |
fix(invoices): remaining_amount + invoice.paid on agent mark-paid path (#825) (#845)
* fix(invoices): book remaining_amount + emit invoice.paid on agent mark-paid path (#825) The agent/MCP commit path commitMarkInvoicePaid flipped status to 'paid' and set paid_amount = total but never wrote remaining_amount (left at the original total) and never emitted invoice.paid — so partial state and webhooks diverged from the dashboard and v1 mark-paid routes. Route the agent path through the shared planInvoicePayment helper (the source of truth introduced in #841): compute paid/remaining/status with the overpayment guard BEFORE booking the JE (so a rejected payment never burns a voucher number), persist remaining_amount + the partially_paid transition, and best-effort emit invoice.paid for webhook parity. Adds lib/pending-operations/__tests__/mark-invoice-paid.test.ts covering the state + event behaviour of this path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): address review feedback on agent mark-paid path (#825) - CAS-guard 409 message now reflects the expanded payable states: the UPDATE filter accepts partially_paid (reachable via a concurrent settle race), not just sent/overdue. - Derive the settle amount from total − paid_amount when remaining_amount is null (legacy rows) instead of falling back to the full total, so a prior partial payment is not double-counted into a false overpayment rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): derive remaining from total − paid_amount on all mark-paid surfaces (#825) The dashboard and v1 mark-paid routes defaulted the settle amount to invoice.total when remaining_amount was null, which over-settles a legacy invoice that has a prior partial payment recorded in paid_amount (false overpayment / AR over-credit). Align both with the agent path (commit.ts): remaining_amount ?? total − paid_amount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
46039f14f4 |
fix(reports): valid two-file NE-bilaga SRU submission (#318, #319) (#844)
* fix(reports): generate valid two-file NE-bilaga SRU submission (#318, #319) The NE-bilaga "Ladda ner SRU" export produced a file Skatteverket rejects: it was served as UTF-8 text/plain (å/ä/ö mojibake, #319) and was structurally invalid — a single blob with #PRODUKT KONTROLLUPPGIFTER (the KU code), no INFO.SRU/BLANKETTER.SRU split, a #SKAPAT typo, no #FIL_SLUT, and suspect field codes 7310–7350 (#318). Rewrite the generator to mirror the working INK2 generator: a two-file INFO.SRU + BLANKETTER.SRU submission, ISO 8859-1 encoded and zipped, with #PRODUKT SRU, #DATABESKRIVNING_*/#MEDIELEV_*, #BLANKETT NE-<år>P<x>, #IDENTITET <personnummer12> <date> <time>, and #FIL_SLUT. Field codes use the authoritative BAS NE_EJ_K1 coupling table (R1→7400 … R10→7505, R11→7440; period dates 7011/7012). Enskild-firma identity is the owner's 12-digit personnummer (birth-century prefix, not INK2's juridisk-person "16"). - Extract the shared ISO-8859-1 encoder to lib/reports/sru-encoding.ts (was inline in the INK2 route). - Extend the NE engine/types to carry address/postort/email for INFO.SRU. - Frontend: NE SRU download uses the INK2 blob pattern; fix a pre-existing param bug in EfDeclarationSection (fiscal_period_id → period_id, +format=sru). - Add generator tests (structure, BAS field codes, zero-omission, ISO-8859-1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): address review feedback on NE-bilaga SRU generator (#318) - getZipFilename uses the income year (fiscal year END) so the filename matches the blankett type/identity for broken fiscal years. - Refuse to generate a submission when the personnummer is missing/invalid (compute + validate the 12-digit identity once in generateNESRUSubmission and throw) instead of silently emitting a placeholder #IDENTITET that Skatteverket would reject after upload. - validateBlanketterSru now asserts the mandatory räkenskapsår date fields (#UPPGIFT 7011/7012) — their absence is a level-2 rejection. - 10-digit personnummer century is inferred from adult age (≥18, <110) at the income year, fixing the e.g. 1924-born/yy=24 edge that mapped to 2024. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f8504f3bd0 |
fix: audit batch — pagination truncation, MFA/dead-code cleanup, mark-paid fail-closed (#841)
* fix(reports): paginate 8 more report/ledger queries (1000-row truncation) Raw .select() without fetchAllRows() silently caps at PostgREST's 1000-row limit, producing wrong statutory output for high-volume companies. Following #806 (trial-balance/VAT), wrap the remaining offenders in fetchAllRows + a stable .order('id') + dedupeBy: - ink2-engine / ne-engine: INK2 & NE-bilaga tax declarations under-counted - ar-reconciliation (1510/1513), supplier-reconciliation (2440): phantom "Ej avstämd" gaps - full-archive-export: 7-year DR archive (added a unique total order so rows are not silently skipped/duplicated across pages) - avgifter-basis, currency-revaluation, vat-declaration Adds a regression guard test asserting >1000 ledger lines are summed, not truncated at 1000. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): close extension-dispatcher MFA gap, scope /api/events to API key, sweep dead code Security/correctness: - ext/[...path] dispatcher now uses requireAuth() instead of inline supabase.auth.getUser(), enforcing MFA (AAL2) on hosted across the whole enabled-extension surface (banking sync, document upload/booking, supplier invoices, migration). Ratchets antipatterns-baseline raw-route-auth 168->165. - /api/events now filters by the API key's bound company_id instead of the user's active company (was a cross-company read with a scoped key). - enable-banking OAuth callback calls ensureInitialized() at module load so the PSD2 consent audit event (ASVS V16 / GDPR Art.30) isn't dropped on a cold-start instance. Dead-code sweep (all confirmed zero importers): - delete lib/tax/calculator.ts, lib/salary/engangsskatt.ts (+test), lib/email/resend.ts, lib/salary/salary-transaction-matcher.ts, lib/webhooks/diff.ts, lib/salary/effective-values.ts, lib/bookkeeping/template-prompt.ts - trim unused lib/vat/eu-countries.ts helpers (keep EU_COUNTRIES) - remove dead getAutomaticStatus() and the abandoned Activepieces CSP entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): fail closed when a payment journal entry doesn't post Three mark-paid paths (legacy route, v1 API, agent commit) diverged on the "mark paid but the JE failed" case — two would flip the invoice to paid (or leave an orphaned posted voucher) with no booking, silently diverging the GL from the AR/AP sub-ledger. Unify on fail-closed: - legacy + v1 + agent commitMarkInvoicePaid: never mark paid without a posted voucher; on a null/failed JE return INVOICE_PAID_BOOK_FAILED before any state mutation (v1 mirrors the match-invoice strict mode). - agent path: add the .in('status',[...]).select('id') CAS guard and cancel the orphaned voucher (cancelOrphanedPaymentEntry) on a lost race or update error, matching the web route. - legacy route: cancel the orphan on a non-race update error too (was only handled on the race branch). - supplier mark-paid: stop swallowing a failed supplier_invoice_payments insert — that row drives the reversal amount in payment-sync; roll back the status flip and cancel the voucher instead. - pending-ops orchestrator: error-check the terminal 'committed' write so an op stranded in 'committing' (the expire sweep only targets 'pending') is at least logged loudly. Adds a guard test for the legacy fail-closed path. Full unit suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): unblock core build + address compliance-review findings - avgifter-basis.ts: fix the core-build TypeScript error — PostgREST's type-level select parser models the salary_run embed as an array, which wasn't assignable to the object-typed generic. Type it `unknown` (rows are read via an explicit cast), making it robust across postgrest-js versions. - /api/events: add a non-null companyId guard before the event_log query (defense-in-depth for the API-key-bound scope) — addresses ASVS V8.2.1 / ISO A.5.15. - supplier mark-paid: add a CAS guard (.eq('status', newStatus)) to the payment-insert-failure rollback so a concurrent settlement can't be clobbered — addresses ASVS V2.3. - dispatcher: add an AAL2 regression test asserting a non-MFA session is rejected (403) and the extension handler never runs — addresses the GDPR Art.32 review ask for the single extension chokepoint. Verified deletions are safe: effective-values.ts was a dead duplicate — the live AGI/payslip path inlines the same `?? override` coalescing (generate-declaration.ts), so AGI correctness is unaffected. next build: exit 0. Full unit suite: 6147 passing. ESLint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b800dcd403 |
style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup Multi-agent scan of all 404 UI files against the locked design system, then 141 verified surgical fixes across 109 files (net -32 lines): - Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale bounce, hover:shadow on list items, transition-all -> transition-colors. - Drop font-medium from single-weight Hedvig display headings/numerals. - Replace raw rainbow Tailwind status colors with Badge variants / brand tokens / neutral surfaces (achromatic chrome, semantic colors stay data-only). - Route raw dates through formatDate(), hand-rolled currency through formatCurrency(), add tabular-nums to financial figures; text-gray-* -> text-foreground tokens. - Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale. - Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total, collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake), correct mismatch-password toast, correct supplier currency-field label. - Remove PII-leaking debug console.log on register, stray console.logs. Verified: tsc clean on all changed files, eslint clean, production build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): sanitize residual error logs in register flow Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the remaining console.error calls in the register flow passed raw error objects, which Supabase may populate with PII (email) in nested fields. Log only sanitized message strings instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8072f6423 |
feat: realtime updates for dashboard transactions (rebased reimplementation of #757) (#833)
* Add realtime transaction subscriptions to dashboard and transactions page Introduce a shared browser Supabase hook so client components can create a stable realtime-capable client once and reuse it across multiple dashboard surfaces. Dashboard navigation now keeps the uncategorized transactions badge in sync through a company-scoped postgres_changes subscription on public.transactions, and the transactions page now subscribes to the same table so it can refresh its list and uncategorized count live without a full page reload. The transactions page keeps the initial server-driven load path intact, but once mounted it listens for inserts, updates, and deletes on the active company's transactions. When a change arrives it refetches the list and total uncategorized count using the same RLS-scoped filters that already power the page, then hydrates the visible rows in the same way as the initial load. The refresh path is intentionally coalesced so bursts of realtime events do not trigger overlapping database reads. The dashboard nav and the transactions page both use the new shared hook instead of creating browser clients ad hoc. This keeps the realtime client setup consistent, avoids duplicate client construction logic, and makes it straightforward to add more realtime dashboard consumers later without re-implementing the same Supabase plumbing. A Supabase migration is included to add public.transactions to the supabase_realtime publication. Without that publication entry the browser subscription would be correct but silent, so the migration is required for hosted environments as well as local resets. Also included in this commit is the current package-lock.json drift present in the staged set. Signed-off-by: Esaias Westberg <esaias@westbergs.se> * fix(migration): retimestamp transactions realtime publication to clear collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql on main. Renamed to a unique timestamp after main's latest. SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dashboard): fix indentation in collapsed-nav badge block --------- Signed-off-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f8aef335c9 |
fix(ci): persist-credentials: false on the privileged compliance-review checkout (#831)
Stage 2 holds a write token + AWS secrets and never needs git push creds; don't leave the token persisted in .git/config for the steps that process the untrusted diff artifact. Closes the Superagent P2 follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a68123bbe8 |
fix(ci): fork-safe compliance review (two-stage workflow_run) — safe alternative to #829 (#830)
* fix(ci): fork-safe compliance review via two-stage workflow_run Replaces the pull_request_target approach (which would run untrusted fork code with the AWS Bedrock secrets in env) with the GitHub-recommended split: - swedish-compliance-diff.yml (pull_request, no secrets, read-only token): computes the diff and uploads it as an artifact. Never runs project code. - swedish-compliance-review.yml (workflow_run, has secrets + write token): checks out ONLY the base repo (trusted script + skills), downloads the diff artifact, feeds it to the model as DATA, and posts the comment. Never checks out or executes fork PR code. scripts/swedish-compliance-review.mjs reads the diff from DIFF_FILE/FILES_FILE when set, with a fallback to git diff for same-repo runs. Safe alternative to #829. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): pin workflow actions to commit SHAs (Superagent P1) Pin actions/checkout, setup-node, upload-artifact, download-artifact and the peter-evans comment actions to immutable 40-char SHAs with version comments, closing the two Superagent supply-chain findings. Matters most here since the review stage holds AWS Bedrock secrets + a write token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): full base fetch in compliance-diff so merge-base works when branch is behind The --depth=1 base fetch left git merge-base with no reachable common ancestor once main advanced past the PR branch, failing the prepare job under bash -e. checkout already uses fetch-depth: 0, so a full base fetch makes merge-base reliable regardless of how far base has moved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): harden compliance review per security audit Stage 1 (swedish-compliance-diff.yml): pass github.base_ref + PR number via env instead of interpolating ${{ }} into the run: shell (template-injection antipattern); add set -euo pipefail; printf over echo. Stage 2 (swedish-compliance-review.yml): pin @anthropic-ai/bedrock-sdk@0.31.0 and add --ignore-scripts — the privileged job (write token) must not run a floating @latest or dependency lifecycle scripts. set -euo pipefail on the PR-number guard. Script: frame the untrusted diff/files with a per-run unguessable random sentinel (not a code fence a hostile diff could close) plus an explicit 'treat as data, ignore embedded instructions' system-prompt guard and output constraints (no images/@-mentions/links/HTML). Legacy getDiff now uses execFileSync (argv array, no shell). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1cd8863958 |
fix(transactions): resolve bank account from cash_account_id in booking dialog (#769)
* feat(transactions): expose cash_account_id in list API response Add cash_account_id to the transactions list API select so that components can resolve the bank account from the transaction instead of hardcoding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * feat(cash-accounts): extract resolveAccount to shared utility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * refactor(transactions): use shared resolveAccount in MatchVoucherDialog Replace the local resolveAccount function with the shared utility from lib/cash-accounts/resolve-account, reducing code duplication and improving maintainability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): resolve bank account from cash_account_id in booking dialog Replaces the hardcoded '1930' bank leg in TransactionBookingDialog with the actual ledger_account of the transaction's cash account. Companies with multiple bank accounts (e.g. 1930 + 1940) now get the correct account pre-filled in both the blank and template-based booking flows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): cancel stale cash-account fetch on dialog re-open Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): prevent form remount discarding edits during bank account fetch Hold JournalEntryForm render until the /api/cash-accounts fetch resolves by changing bankAccount state to string | null (null = pending). This prevents the form from mounting with key '…-1930', then immediately remounting with the correct account key and losing any user edits made in the sub-100ms window. Also adds r.ok guard before parsing and sets '1930' as explicit catch fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): cancel stale cash-account fetch in MatchVoucherDialog Pass a signal object into loadCandidates and return a cleanup from the useEffect so a stale in-flight fetch (from a previous transaction) cannot call setAccountNumber/setAccountFallback/setGlLines/setSelected after the dialog re-opens for a different transaction. Also adds r.ok check before parsing /api/cash-accounts response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
5b4cefe8ab |
feat(api): v1 endpoints to stamp invoice inbox items as consumed (#767)
* feat(api): v1 endpoints to stamp invoice inbox items as consumed
Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link
(best-effort stamp on the originating invoice_inbox_items row) and a new dedicated
POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping
independently of the document link — both use documents:write scope and require
Idempotency-Key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
* fix(api): wrap stamp response in dataEnvelope and register route in load-routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
---------
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
7cd1a7388f |
fix(customers): add columns the system-migration importer needs (address_line2, default_payment_terms, vat_number_validated) (#780)
* fix(customers): add address_line2 + default_payment_terms columns The customers table lacked address_line2 and default_payment_terms, yet the suppliers table has both and three code paths write/read them on customers: the arcim-migration importer, the built-in customer CSV import, and the customer CSV export. Inserts failed with PostgREST "Could not find the 'address_line2' column of 'customers' in the schema cache", so customer imports landed 0 rows while suppliers imported fine. Add the two columns to match suppliers (text, integer default 30). Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): add missing vat_number_validated boolean column The customers table was missing the boolean vat_number_validated flag that types/index.ts and ~40 code sites depend on (getVatRules/getAvailableVatRates, the customers-list validated badge, the v1 customers API select), yet no migration ever created it — only the vat_number_validated_at timestamp exists. It went unnoticed because normal customer creation writes the flag only when a VIES check runs, and reads degrade to undefined when the column is absent. The arcim-migration importer writes it unconditionally (false), so customer and sales-invoice-stub inserts failed with PostgREST "Could not find the 'vat_number_validated' column of 'customers' in the schema cache". Same class of gap as the address_line2/default_payment_terms columns added in the previous commit on this branch. Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): retimestamp migrations to clear version collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql (merged to main via #797) and with #757. Renamed to unique timestamps ordered after main's latest (20260629160000). SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
837f354d81 |
fix(sie): add ?encoding=cp437 for legacy bookkeeping software (#810)
* fix(sie): add ?encoding=cp437 option for legacy bookkeeping software SIE spec mandates CP437 (#FORMAT PC8) but accounted generates UTF-8. Most modern cloud tools (Fortnox, Bokio) accept UTF-8 fine, so UTF-8 remains the default. Pass ?encoding=cp437 to get a properly encoded CP437 binary with #FORMAT PC8 in the header, required by desktop software such as Visma Administration and BL Administration. Removes the spurious #FORMAT PC8 tag from the default UTF-8 output since declaring CP437 while serving UTF-8 caused mojibake on import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(sie): wrap Uint8Array in Buffer.from so NextResponse accepts it Uint8Array is not directly assignable to BodyInit in the Next.js NextResponse constructor — wrapping with Buffer.from() satisfies the type without changing the byte content. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
37ee125b9b |
fix(mcp): paginate trial-balance and VAT aggregations (1000-row truncation) (#806)
* fix(mcp): paginate trial-balance and VAT aggregations (1000-row truncation) The gnubok_get_trial_balance tool and computeVatReport each ran an unbounded journal_entry_lines aggregation. PostgREST caps an unpaginated .select() at 1000 rows, so any period with >1000 entry lines silently truncated: wrong per-account sums and a false "not balanced" trial balance, and an under-reported momsdeklaration for yearly or busy quarterly VAT periods. - get_trial_balance now delegates to the canonical generateTrialBalance (lib/reports), which paginates via fetchAllRows and rolls opening balances forward, also fixing a latent bug where the tool ignored IB. - computeVatReport now paginates its line fetch via fetchAllRows. The library fixed this class of bug in #79; these two MCP paths kept their own copies that were never updated. Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(mcp): non-null assert periodId in generateTrialBalance call Fixes the core-only TS build error (string | undefined not assignable to string). periodId is guaranteed defined by the !period guard above; mirrors the existing periodId! call later in the file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): support .range() in computeVatReport mock for paginated query computeVatReport now fetches journal_entry_lines via fetchAllRows (.range), but the hand-rolled mock terminated at .lte(). Move the terminal to .range() so the 8 VAT-aggregation tests exercise the paginated path. Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e4a9fdb4a6 |
fix(reports): add missing allLines mock to SIE OB-exclusion test (#828)
#824 moved per-entry line fetching into a single paged journal_entry_lines join query. #809's OB-exclusion test was branched before #824, so its mock queue lacked that response — every later mock shifted by one and the #OBJEKT projects loop read journal-line objects (no .code), throwing TypeError on main. Adds the journal_entry_lines (allLines) mock with lines keyed by journal_entry_id. Test-only; no production code change. Restores green main. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b4bf63fec |
fix(reports): sort trial balance source lines by date then voucher_number (#763)
.order({ foreignTable }) in Supabase/PostgREST sorts the embedded
resource's rows, not the parent result set. Journal entry lines in the
trial balance drill-down were therefore returned in database insertion
order rather than chronological order.
Sort in JavaScript after fetching — mirroring the approach in
generateGeneralLedger — to guarantee entry_date ASC, voucher_number ASC
ordering regardless of what the database returns.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
5e9aa52dea |
feat(mcp): add gnubok_link_document_to_voucher tool (#804)
Links an uploaded document directly to a posted verifikation (journal entry) via the staged-operation pattern. Covers imported/manual vouchers that have no bank-transaction row — the gap left by gnubok_attach_document_to_transaction. - New MCP tool gnubok_link_document_to_voucher (bookkeeping:write scope) - New pending-operation type link_document_to_voucher (medium risk) - Commit executor with WORM guard: refuses to re-link a doc already pinned to a different posted JE (BFL 5 kap 6 §); allows overwriting a draft-JE link; maps period-lock throws to 409 - 5 executor unit tests covering 404, WORM 409, draft-allow, happy path, and period-lock Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
15af0cd7ea |
feat(mcp): add per-line account overrides to create_supplier_invoice_from_inbox (#805)
Adds a line_overrides param (array of {line_number, account_number}) so agents
can override the BAS account on individual lines rather than every line
inheriting the supplier default. Priority chain: line_overrides → extracted
accountSuggestion → supplier default → 4000.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
4f96ce920a |
fix(arcim-migration): dedup customers/suppliers by name when org-number is absent (#788)
The register import skips already-imported customers and suppliers by org-number only. Records without an org-number (private-person customers, suppliers like PostNord/IKANO) can never match, so every re-sync re-creates them — and their invoices — producing 2x, 3x, 4x duplicate rows. The nameToCustomerId / nameToSupplierId maps were already built and populated but never consulted for dedup. Use them as a fallback: match on org-number when present, otherwise on name. This mirrors the sales-invoice customer-stub path, which already keys on `org ?? name`. When an org-number is present we still match on it alone, avoiding false-positive name collisions across distinct legal entities. Signed-off-by: Jonas Hagberg <jonas@lindan.se> |
||
|
|
96989f1cde |
fix(arcim-migration): treat 12-digit Swedish numbers as domestic + route personnummer to personal_number (#799)
Customer/supplier type inference flagged any identity number whose digit count was not exactly 10 as a foreign org number, so a 12-digit century-prefixed personnummer (e.g. 19700616-7113) was misclassified as non_eu_business instead of swedish_business — applying export VAT to a domestic party. Recognize both 10- and 12-digit (19xx/20xx) Swedish numbers via a new looksLikeSwedishIdNumber helper. Separately, mapCustomer always wrote the provider's single identity-number field into org_number, even for individuals. The customer form renders personal_number for individuals (org_number for businesses), so a Privatperson's personnummer was stored in a column the UI hides. Route the number to personal_number for individuals. Adds entity-mapper-customer-type unit tests. Signed-off-by: Jonas Hagberg <jonas@lindan.se> |
||
|
|
47f03f4238 |
feat(arcim): import Bokio underlag and link to verifikat (#786) (#813)
* feat(arcim): import Bokio underlag and link to verifikat Adds an optional, re-runnable step that pages the Bokio /uploads, resolves each receipt's target verifikat via the SIE-preserved voucher number, and archives it through the document service linked to the journal entry. Closes the gap where neither the SIE GL import nor the entity import carries the receipts/underlag attached to each verifikat. - lib/providers/bokio: getBytes() binary download + an attachments resource module (uploads list, GUID->voucher index, per-upload download); pageSize capped at 100, file type taken from the upload's contentType since the download is octet-stream - importProviderDocuments: bulk in-memory resolution keyed on (fiscal period, series, number) — scoped per fiscal year because Bokio restarts numbering at V1 each year; idempotent on (company_id, sha256) so re-runs don't duplicate the undeletable BFL-linked rows - POST /import-documents route, kept off the migration critical path because the Bokio document API is rate-limited (200 req/60s) - journal_entry_id link only for v1; reuses the document-service link path (same module as #804) rather than forking it Closes #786 Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(arcim): stable pagination order + account for unresolvable receipts Addresses two findings from a Codex review pass on the import step: - Add .order('id') to the paged journal_entries / document_attachments / fiscal_periods reads. fetchAllRows pages with .range(), and PostgREST paging without a deterministic order can skip/repeat rows once a table exceeds one page (journal_entries crosses 1000 across several migrated years), which would defeat both voucher resolution and the sha256 dedup. - Keep every upload carrying a journalEntryId in scope instead of pre-filtering on a resolvable voucher ref, so a receipt whose Bokio entry number didn't parse (or resolves to no verifikat) is counted as unmatched rather than silently dropped from the best-effort report. Tests: add unresolvable-ref and zero-uploads cases; mock now supports .order(). Signed-off-by: Jonas Hagberg <jonas@lindan.se> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> |