c84f951a5c2f51279277a506754b2bd94de28249
169 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
25a7261eda |
fix(pending-ops): recovery sweep for operations stuck in committing (#843) (#1108)
The commit dispatcher claims an op with an atomic pending -> committing CAS; if the process dies after side-effects post but before the terminal committed write (or that write fails, the PR #841 log line), the row sat in status='committing' forever: the expire cron only sweeps 'pending'. Add lib/pending-operations/recover-stuck-committing.ts, invoked from the existing daily expire cron (no new vercel.json entry): - Only rows whose updated_at (the claim timestamp: the CAS bumps it via the update_updated_at_column trigger) is older than 15 minutes, well past the 300s Vercel function ceiling, so in-flight executors are never raced. - Positive evidence that side-effects posted finalizes the row to committed with result_data.recovered=true. Evidence exists only where params identify a target with an unambiguous posted state: categorize_transaction (is_transaction_booked RPC, skipped for allow_duplicate), link_transaction_journal_entry (exact tx+entry link), match_transaction_invoice (invoice_payments pair row). - No evidence: terminal rejected with an explanatory result_data, never back to pending (re-execution could duplicate side-effects that posted without a trace). Reason 'stuck_committing' is distinct from 'expired' so the UI badge never claims these rows. - Every terminal write is CAS-guarded on status='committing'; probe errors skip the row for the next run. - One structured 'pending_op_recovery' warn per row (count by outcome); runbook comment added next to the #841 finalize-failure log line. Tests: unit coverage for the decision logic and cron wiring (401, sweep invoked, failure isolation), plus a pg-real test proving row selection, the trustworthy updated_at anchor, committing -> terminal transitions through the real immutability/input-frozen triggers, and the is_transaction_booked evidence substrate. Fixes #843 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8294899543 |
test(skatteverket): guard LEGACY_DISCOVERY_HOSTS against config drift (#1093) (#1107)
Add validateLegacyDiscoveryHosts() next to the allowlist in lib/api/v1/base-url.ts and a unit test that pins the registered production configuration (app.accounted.se canonical, app.gnubok.se SKV OAuth pin). The invariant is checked both ways: the NEXT_PUBLIC_SKV_OAUTH_BASE_URL host must be reflectable by discovery (canonical or allowlisted), and every allowlist member must be accounted for by the registered configuration. Drift is now a red CI test instead of a silent production re-auth failure near a filing deadline. No runtime behavior changes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
826513b4fd |
fix(tax): representation meal warning inverted the 2017 VAT rule (#313) (#1106)
The restaurant/lunch warning claimed 'Momsen är inte avdragsgill sedan 2017', which is legally backwards. The 2017 reform (Prop. 2016/17:1) abolished the income-tax deduction for representation meals (IL 16 kap 2 §) but retained the VAT deduction on a base of up to 300 SEK per person excl. VAT, now ML 2023:200 13 kap 24-25 §§. The old 'ML 8:9' citation pointed at the repealed ML 1994:200. Verified against the swedish-vat compliance skill before rewording. Adds regression tests pinning the corrected message and legalBasis. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a0b524fbb |
fix(categorization): connect card descriptors to counterparty history (#1095)
* fix(categorization): connect card descriptors to counterparty history
suggest_categories returned no signal for recurring card merchants
(reported: Anthropic booked to 5420 fourteen times, zero suggestions).
Three compounding causes, all fixed:
- normalizeCounterpartyName() now reduces card-network descriptors to
their merchant segment ("ANTHROPIC* CLAUDE SUB SAN FRANCISCO" ->
"anthropic"; "PAYPAL *SPOTIFY" -> "spotify"), so monthly per-charge
tails stop splintering one merchant into unmatchable variants. SQL
mirror normalize_counterparty_key() updated in lockstep (migration
20260721140000), keeping the ledger-context template join exact.
- New token_subset match tier bridges templates learned from manual
bookings ("Claude Dec" -> "claude") to bank descriptors containing
the token, and card-core descriptors to legacy splintered templates.
Guarded by a distinctive-token filter so generic/geo words never
match on their own.
- Merchant history falls back to description when merchant_name is
null: card purchases never carry merchant_name, so the history path
was structurally blind to exactly the transactions that need it.
History keys now share the counterparty-template normalization and
the 200-row window is ordered by recency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(categorization): guard single-token matches, anchor history on original_description
Review follow-ups (CodeRabbit on #1095):
- token_subset tier: a single shared distinctive token now also requires
occurrence_count >= 3 on the template, so a template named after a
common word or first name (one prior booking) cannot vacuum up
unrelated transfers ("SWISH ANDERS JOHANSSON"). Multi-token agreement
stays unrestricted; the Claude/Anthropic case (14 bookings) is
unaffected.
- merchant history keys on original_description ?? description: the raw
bank descriptor is immutable while description is a user-editable
working title, so renaming a transaction no longer severs its history
link for future recurring charges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(migrations): re-timestamp card-descriptor migration after prod moved past it
Prod applied 20260721144311 (#1101) through 20260721201747 (#1104) while
this PR was open; 20260721140000 would sort before them and risk being
skipped by out-of-order auto-apply at merge. Not yet applied to prod, so
renaming is safe; the preview branch re-applies idempotently
(CREATE OR REPLACE).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
3920c893f4 | fix(migrations): adjust retention expiry trigger and validation for fiscal periods (#1104) | ||
|
|
e11f70b347 |
Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name |
||
|
|
aa6d42a167 |
fix(import): raise statement_timeout on import_sie_journal_entries to 290s (#1101)
A production Fortnox migration failed with "SIE-verifikationer kunde inte importeras atomiskt: canceling statement due to statement timeout": the atomic import RPC (20260712150000) inherits the 8s authenticator statement_timeout, so any real-world multi-year SIE file exceeds it and the whole import is cancelled and rolled back. Same failure class as 20260629160100 (replace_sie_import / undo_sie_import); same fix, a function-scoped statement_timeout of 290s sitting under the calling routes' maxDuration = 300 ceiling. Migration 20260721144311 is already applied to prod (proconfig verified carrying statement_timeout=290s); the committed file is byte-identical under the same version. The new pg-real ratchet pins the config on all three SIE RPCs because CREATE OR REPLACE FUNCTION silently drops ALTER FUNCTION settings. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b420f3e1d9 |
feat(domains): dual-domain cutover to app.accounted.se (#1087)
* feat(domains): dual-domain cutover to app.accounted.se The user-facing app moves to app.accounted.se while app.gnubok.se stays alive for machine traffic (MCP connectors, API keys, third-party OAuth callbacks, webhooks, crons), so no third-party callback registration is on the critical path. - next.config: host redirect app.gnubok.se -> NEXT_PUBLIC_APP_URL for page traffic only (/api, /.well-known, /_next excluded). Arms itself only once NEXT_PUBLIC_APP_URL leaves the legacy host, so merging this is inert and the cutover is a pure env flip + redeploy. - skatteverket: redirect_uri pinned via NEXT_PUBLIC_SKV_OAUTH_BASE_URL (Utvecklarportalen registration is slow to change); the OAuth callback now resolves the flow from the state token + stored oauth_user_id via the service client instead of session cookies, which no longer exist on the OAuth host. Legacy same-domain flows fall back to the session. - popup listeners (SkatteverketConnectPanel, AGIPanel) accept postMessage from the pinned OAuth origin; event.source identity check unchanged. - /.well-known discovery docs reflect the allowlisted request host so existing MCP connectors on app.gnubok.se keep a self-consistent issuer/resource after the flip; spoofed hosts fall back to canonical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log dual-domain cutover decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): recency-bound SKV state lookup, exact localhost match in discovery allowlist - The oauth_state lookup now only considers rows updated in the last 10 minutes: bounds how long a leaked/phished authorize URL stays completable, keeps the row set far below PostgREST's 1000-row cap, and surfaces query errors instead of misreporting them as CSRF. - resolveDiscoveryBaseUrl matches localhost/127.0.0.1 exactly; the prefix check reflected spoofed hosts like localhost.evil.example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d860567976 |
feat(pending): bulk reject selected operations in granskning (#1085)
* feat(pending): bulk reject selected operations in granskning The granskning queue could approve selected operations in bulk but rejection was one row at a time. Adds: - POST /api/pending-operations/bulk-reject: one guarded UPDATE (status='pending' filter) so rows resolved in a parallel session are reported as skipped instead of being flipped; optional rejection_category/rejection_reason applied to every rejected row so agents still learn from bulk 'no'. No high-risk skip server-side: rejecting posts nothing to the ledger. - 'Avvisa valda' button next to 'Godkänn valda'; the existing reject dialog doubles as bulk confirmation (category + note apply to all). - Route tests: 401/403/400/500, not-found, already-handled skip, read-write race, happy path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pending): disable both bulk buttons while either bulk action is in flight Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2908a951ab |
chore: remove Dependabot (#1084)
Delete .github/dependabot.yml and update the two doc references that pointed at it. Weekly grouped bumps were noise, and the #884 grouped bump broke Bedrock streaming in prod; dependency updates are manual and deliberate from now on. The @anthropic-ai/bedrock-sdk 0.29.1 exact pin remains enforced by scripts/checks/no-new-antipatterns.mjs. Open dependabot PRs #1083, #1082, #1012 closed alongside this change. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4e47335308 |
feat(year-end): administrative undo of executed year-end closing + skatteverket scope fixes (#1081)
* fix(skatteverket): request the ska scope for skattekonto v2 The skattekonto v2 API rejects skahmst-only tokens with 403 "The required scopes are not authorized" (observed in prod 2026-07-20; no company has synced since 2026-05-10). The requested `skattekonto` scope is silently dropped from every grant, while `ska` appears in one real May grant, so request it too: SKV grants the intersection, so this is harmless if wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): correct the skattekonto scope model around ska Root cause of the May 10 skattekonto outage, confirmed via git history and prod token data: the `ska` scope (the interactive skattekonto API's actual scope, requested since the extension's first commit in March) was removed by the "remove unused scopes" cleanup in the #431 series. Every token issued after that hour lacks it and the API answers 403 "The required scopes are not authorized"; no company has synced since. The May 15 repair re-added skahmst, which per its tjanstebeskrivning is a different bulk E-transport service and does not substitute; `skattekonto` is not a real SKV scope name and is silently dropped from grants. Follow-up to the ska re-request (cd8f7a30): - document the confirmed scope model in oauth.ts so ska is never "cleaned up" again - panel missing-scope warning and reconnect-button now gate on ska, not skahmst/skattekonto - scope badge labels: ska takes the saldo & transaktioner label, skahmst relabeled as the E-transport file service - consent-page note covers both terse scope names and says ska is required Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(year-end): warn on untaxed profit at verkstall, Swedish readiness messages, always-visible period selector An aktiebolag could execute year-end with a profit and zero bolagsskatt booked without any warning (support case: closing moved 592k to 2099 untaxed). The preview now computes bolagsskattMissing (AB + profit + no 89xx account among closed accounts, 8999 excluded) and both the preview and execute steps render an advisory, bypassable warning. validateYearEndReadiness messages are now Swedish (the bokslut wizard is a stays-Swedish surface); the MCP year_end_readiness classifier matches both the new Swedish strings and the legacy English ones. The wizard period selector now always renders, keeps a selected-but- ineligible period selectable, and resets a stale ?period= id from another company instead of leaving the user stuck on the wrong year. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(year-end): administrative undo of an executed year-end closing Storno-only reset used when a bokslut was executed prematurely (e.g. without bolagsskatt) and no arsredovisning exists yet: reverses the next period's result_appropriation and opening_balance entries, reopens the period, reverses the closing entry, and detaches closing_entry_id. Resumable if interrupted midway; attribution per BFL 5 kap 6. Migration 20260720140000 adds the trigger escape hatch: closing_entry_id may only change once set when the old closing entry is reversed with a posted storno chain (status flag alone is forgeable via PostgREST), and a non-NULL replacement must be a posted year_end entry in the same period. Covered by a pg-real test. planResultAppropriation idempotency is now posted-only: a reversed omforing no longer blocks the re-run from posting a fresh 2099 -> 2098 reclassification (it previously returned null silently, leaving the new year's equity polluted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address CodeRabbit, PR-Agent and compliance findings - undo script: company_id filters on verify queries, period-scope the arsredovisning precondition checks, validate service-key format, escalate audit_log insert failure to a hard error (BFNAR 2013:2) - detach migration: company-scope the storno chain EXISTS, replace the em dash in the new error message Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address round-2 compliance swarm and Swedish review findings - undo script: require --confirm-url with --commit so an env swap fails loud; retry the audit_log insert 3x and direct the operator to insert the behandlingshistorik row manually on final failure (BFNAR 2013:2) - year-end preview: document why resultAccountSummary is a complete 89xx scan; warning text now also names periodiseringsfond and overavskrivningar as legitimate zero-tax reasons Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e2d6c92e3a |
feat(onboarding): illustrated halftone backdrop from marketing-site art (#1080)
* feat(onboarding): illustrated halftone backdrop from marketing-site art Ports the gnubok-website halftone illustration set into the onboarding flow so signup -> app feels like one product: - OnboardingBackdrop: petal field across the paper background (radially masked to stay calm behind the form), Stockholm skyline dissolving into the bottom edge, two clouds drifting at reading pace that bounce off the viewport and the onboarding panel (IllustrationFloaters, physics ported from the website's BouncingFloaters). - Per-step instrument ghosted in white ink on the dark card header: pencil -> notebook -> adding machine -> calculator, one per step. - Dark mode via invert/hue-rotate filters; prefers-reduced-motion parks the floaters; all art is aria-hidden and pointer-events-none. Applies to /onboarding, /onboarding/agent and /select-company via the shared (onboarding) layout. No new strings, no API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(onboarding): strip backdrop to skyline + step art per founder review Founder kept the Stadshuset skyline and the per-step header instruments; the petal field and drifting clouds are cut. Removes the now-unused floater physics component and the petals/cloud assets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
87f0d5af48 |
fix: GH issues batch: deadlines opt-ins, SKV reconnect, narrative edit, payment-link gating (#1076)
* fix(errors): close remaining raw-message leaks after #1048 (#337) Follow-up to PR #1048. No user-visible toast or response field can now carry a raw engine or DB message; everything maps through getErrorMessage or the structured-errors registry. - get-error-message: only normalize a code-carrying Error instance into the structured path when the registry knows the code; unknown codes (Node system errors, stray third-party codes, Error-wrapped Postgres SQLSTATEs) fall through to pattern match, Swedish check, Postgres map and the status/context/generic fallbacks instead of returning the raw message. New Swedish-detection pattern for "ar last" phrases and a known-pattern row for "already has a journal entry". - structured-errors: add CANNOT_EDIT_NON_DRAFT (409) and MANDATORY_DIMENSION_MISSING (400) rows, plus common Node network codes (ECONNREFUSED, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN, EPIPE) as retryable 503 transients with a Swedish message. - pending-operations commit + bulk-commit routes: map executor error strings through getErrorMessage before responding (raw stays in logs); Swedish passes through, English falls to status-appropriate Swedish. - pending page: toast via getErrorMessage, fixing raw English toasts and "[object Object]" for structured envelopes on commit/bulk/reject. - transactions book + journal-entries routes: untyped catch and DB list errors no longer return err.message; mapped or static Swedish instead. - invoice send + issue-credit-note: partial_failures reasons are now Swedish (raw provider/DB text logged, never returned). - Tests: new unknown-code/Error-instance suite, registry rows asserted, route tests updated off the pinned raw-English expectations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): target the räkenskapsår for yearly VAT redovisningsperiod A yearly filer with a broken fiscal year has a Skatteverket period ending in its FY-end month, not December, and the panel's year state is never maintained in yearly mode (the year picker is replaced by the räkenskapsår selector), so calls targeted the wrong period even for calendar-FY companies filing after year end. The selected fiscal period now rides through the whole chain: panel query strings, draft/validate/ submit bodies, buildMomsuppgift (which resolves the FY bounds so the period id and the figures describe the same räkenskapsår), and the staged-commit path. MCP callers without a fiscal period keep the calendar fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): group same-day skattekonto deadlines into one card Moms, AGI and preliminärskatt legally share the skattekonto date (den 12:e), so a small monthly-moms employer saw 2-3 near-identical rows per month. Two or more pending system rows of the skattekonto family on the same due date now render as one grouped card with the date block once and each obligation as a sub-row keeping its own confirm-to-complete flow. Presentation only: rows, statuses, ICS feed unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): KU + ROT/RUT + long-tail opt-in deadlines, rolling horizon Follow-ups from the #1028 audit left out of the #1057-#1060 fix stack, each with its own condition modeling: - kontrolluppgifter (KU10/KU20/KU31), due 31 Jan (SFL 24 kap. 1 §): opt-in flag suggested from ledger signals (2898 utdelning, 2393/2893 ägarlån; deliberately not 2091, see DECISIONS.md), AB only, mirroring the #1059 EU-sales suggest-and-confirm pattern. - rot_rut_begaran, due 31 Jan after the payment year (Lag 2009:194 8 §): rows generated only for years with actually PAID ROT/RUT invoices, resolved inside the generator; invoice-derived suggestion. - Long tail, explicit opt-in ('Fler deadlines'): OSS quarterly and IOSS monthly with a skipBankingDayAdjustment config flag (EU-law dates stand on weekends), Intrastat (10th banking day of the following month), punktskatt (ordinary skattedeklaration schedule), and fyllnadsinbetalning (12th of 2nd month over 30k / 3rd of 5th month, SFL 62:8 + 65 kap.). Kvarskatt deferred: needs a slutskattebesked date the app does not hold. - Rolling generation horizon: recurring types ~6 months ahead, annual 12 months, mirrored in the backfill expectation keys so the nightly cron never thrashes; regeneration now preserves manual in_progress status; one-time cleanup migration removes existing far-future rows. Migrations also applied to the staging branch, together with the previously missing 20260717xxxxxx deadline migrations (staging had drifted and lacked dismissed_at). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): keep narrative editable after year-end close The narrative save endpoint refused writes whenever the fiscal period was closed/locked, but Verkstall bokslut closes the period before the arsredovisning text is ever written, so every legitimate save failed with PERIOD_LOCKED and the PDF fell back to placeholder text. The narrative is arsredovisning document text (ARL 6 kap.), not journal rakenskapsinformation, so the bookkeeping period lock does not apply. Saves are now refused only once a Bolagsverket submission for the period is registrerad (ARSREDOVISNING_REGISTERED, 409); the filed artifact was already frozen separately by the submissions immutability trigger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): surface dead SKV connections and nudge reconnect Prod has ~70 companies that connected Skatteverket before the post-connect sync fix (#1010) and silently never synced skattekonto: the only reconnect prompt lived in the settings panel nobody revisits. - transactions-page banner when the connection is needs_reconsent or expired without refresh, linking to /settings/tax - pre-connect note in the connect panel: approve ALL behorigheter on Skatteverket's consent page (previously only shown after a failure) - wire the inert skattekonto.connection.expired event to an email nudge to the token owner; one send per consent episode via claim-first dedup in notification_log (type skv_connection_expired, partial unique index in migration 20260720090000, applied to staging) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(archive): per-year behandlingshistorik covers late-booked vouchers + Drive backup disclaimer The per-fiscal-year archive filtered audit rows by created_at within the period, dropping treatment history for bokslut entries, stornos and SIE imports booked after year end (BFNAR 2013:2 kap 8). The year archive now unions the date window with every audit row touching the period's journal entries and lines, deduped by audit id; line rows (company_id NULL by trigger design) are admitted via a scoped OR and reachable on the service-role backup path. ARCHIVE_FORMAT_VERSION 2->3 forces a one-time Drive re-upload so existing archives pick up the complete history. The Drive card on /import Exportera and the LASMIG texts now state the Drive copy is a convenience backup, not the BFL 7 kap legal archive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(decisions): clarify Arsredovisning narrative save behavior on submission status * feat(invoices): gate payment links behind invoice settings opt-in The payment-link section (manual URL field + Stripe auto-create toggle) was visible on every invoice and auto-created Stripe links on send for any connected company. It is now opt-in per company: - new company_settings.invoice_payment_links_enabled, default false for everyone (no grandfathering of Stripe-connected companies) - invoice editor hides the whole section unless enabled; a draft that already carries a link still shows it so old links stay clearable - enforced server-side in maybeCreatePaymentLinkForInvoice (after the provider lookup, so the extension-free core build never queries), so dashboard, v1, MCP and recurring sends all obey it - new toggle on Settings -> Invoicing, saves instantly; sv/en strings Migration applied to the staging branch; prod gets it on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): add invoice_payment_links_enabled to company settings fixture The makeCompanySettings fixture missed the new required boolean, failing the core-only build's type check of tests/helpers.ts. Default false, matching the migration default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(review): address CodeRabbit, compliance and Swedish review findings Round 2 of PR #1076 review feedback, one change per accepted finding: - pending page: res.json() safe fallback in both commit paths so a non-JSON proxy response cannot surface a raw parser error - bulk-commit: map operation status enums to Swedish display labels in the 'Redan hanterad' skip message - payment-link settings: disable the toggle while a save is in flight to prevent out-of-order PUT responses - deadlines group card: route all UI strings through next-intl (deadlines namespace, sv + en) - archive export: scope the period audit entry lookup to posted/reversed, matching the rest of the export - error tests: assert the exact registry English message for ECONNREFUSED to lock the no-leakage contract - signal routes: log.warn when best-effort lookups swallow a Supabase error (forensics), keep fail-closed behavior - narrative route: document that 'avslutad' submissions deliberately stay editable (never registered at Bolagsverket) - VAT: yearly declarations without an explicit fiscalPeriodId now resolve the räkenskapsår ending in the target year from fiscal_periods instead of assuming a calendar FY (SFL 26 kap 10-11 §§); calendar fallback only when no fiscal period exists - deadlines: IOSS deadline no longer requires vat_registered (Art. 369s has no Swedish VAT registration prerequisite) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
30771b1619 |
feat(mcp): payroll e2e parity: staged salary-run booking + absence deletion (#1075)
* feat(mcp): payroll e2e parity: staged salary-run booking + absence deletion Close the last MCP-surface gaps for running payroll end-to-end via the connector (the v1 REST API already had the full chain): - gnubok_book_salary_run: stages a high-risk book operation; on approval the executor walks review -> approved -> paid -> booked via the new lib/salary/book-run.ts (extracted from the dashboard book route, which now calls the same core) and posts the immutable salary vouchers. - gnubok_delete_absence: staged inverse of gnubok_register_absence, reusing deleteAbsenceRange with a dry-run day-count preview. - Wire the missing payroll operation types into the Granskning label map (register_absence, update_payslip_line, employee ops, vacation_year_close had translations but fell back to humanized snake_case). - Update stale 'booking happens in the web UI' prose in tool descriptions, the payroll-monthly skill, and the workflow hint; payload-size ceiling 56K -> 57K per the documented bump protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): widen pending_operations op-type CHECK + roster typing for book_salary_run The op-type audit (pg-real) caught the exact bug class it exists for: book_salary_run and delete_absence were staged in code without the constraint-expansion migration, so every real staging INSERT would have failed with check_violation while dry_run previewed clean. Ships the documented widen (NOT VALID) + validate migration pair. Also fixes the strict-mode cast in book-run.ts that failed the production typecheck. Verified locally against supabase/postgres 15.8.1.060 with all migrations applied: op-type audit green, pg-real 692/693 (the one failure is the pre-existing TZ-sensitive get_unlinked_1930_lines assertion, green under TZ=UTC as in CI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
90e7c7f47f |
feat(ux): company context in settings, Kundfakturor rename, compact verifikat view (#1071)
* feat(ux): company context in settings, Kundfakturor rename, compact verifikat view Support feedback (2026-07-19): active company invisible in settings, menu said Fakturor next to Leverantorsfakturor, no compact verifikat view. - ActiveCompanyBadge chip in the settings modal header and the full-page settings header; the modal covers the sidebar CompanySwitcher - nav + page title Fakturor -> Kundfakturor (sv), Invoices -> Customer invoices (en); command palette gets a Kundfakturor page entry - verifikat list density toggle (comfortable/compact), persisted per company like the existing sort/page-size choices Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: decision log for scoped Kundfakturor rename Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): badge hover reveals full company name; pure density state updater Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ebf69d2933 |
fix: credit-note overdue countdown + voucher sequence resync after SIE import (#1069)
* fix(invoices): hide overdue countdown for credit notes in invoice list Credit notes stay in status 'sent' forever (invoices_credit_note_not_paid blocks paid states), so the relative due-date label rendered an ever-growing 'X dagar forsenad' on every issued credit note. Skip the label for rows with credited_invoice_id set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): resync voucher_sequences counters left behind by pre-RPC SIE imports The batch SIE import path that predated import_sie_journal_entries (20260712150000) inserted vouchers with explicit numbers but never updated voucher_sequences, leaving counters behind max (year-end integrity error, duplicate-key crash on the next voucher) or missing entirely (next_voucher_number restarts at 1 and collides). Idempotent data repair: raise lagging counters to the observed max and insert missing rows attributed to the company owner. Already applied to prod and staging; replay is a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): harden voucher-sequence resync per PR review Address PR #1069 review findings: close the ON CONFLICT race by upgrading DO NOTHING to DO UPDATE with GREATEST (a row created by next_voucher_number between snapshot and insert is raised instead of left at 1), unify the voucher_number > 0 filter across both statements, and record the manual prod/staging execution timestamps as the change record (ISO 27001 A.8.32, BFNAR 2013:2 behandlingshistorik). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46b8e2bfea |
Fix/fable design (#1063)
* fix(bokslut): make dispositions storno-safe and derive fond math from opening balances A reversed year_end voucher kept its storno in the income statement while the original was excluded (source_type asymmetry), inflating resultat fore dispositioner by exactly the reversed amount, and the posted-only fond balance produced a phantom negative 212X that leaked a bogus aterforing proposal. Support case: a user double-booked periodiseringsfond, reversed both correctly, and the dispositions page still showed wrong numbers. - trial-balance excludeYearEndClosing now also excludes entries chained to reversed year_end entries via reverses_id/correction_of_id (grammar verified against staging PostgREST) - listExistingPeriodiseringsfonder counts posted+reversed so storno pairs cancel, and returns opening balances per fond - schablonintakt per IL 30 kap 6a: opening balance base, rate = SLR per closing year (1.96% FY2025, 2.55% FY2026), replacing the wrong SLR+1pp 0.0355 constant - avsattning 25% cap is year-total: already-provisioned current-cohort growth consumes headroom in both preview and commit, so re-running the flow can no longer double-book the fond - SLP posts before avsattning (deductible, shrinks the cap base) and is posted-aware: no double proposal or double count on resumed runs - sumPostedYearEndDispositions counts correction replacements of reversed year_end entries and exposes the SLP portion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(bokslut): use roundOre for new fond/disposition rounding Satisfies the naive-ore-round ratchet that tightened on main; identical arithmetic, pinned by the existing exact-value tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): address PR #1063 review findings - computeProposal receives the already-validated period row: a transient DB failure can no longer silently skip a requested disposition (and two redundant per-item period fetches are gone) - getSchablonintaktRate fails closed for unmapped years instead of falling back to the latest known rate: statutory rates are never guessed; POST rate override remains the escape hatch - listExistingPeriodiseringsfonder is opening-balance-entry aware: a fond carried via the OB entry booked by year-end closing was counted twice (once from history, once from the OB entry); balances now derive from OB + current-period activity when an OB entry exists - periodStart is validated as a real calendar date, not just a shape - reversed year_end correction targets resolve company-wide in sumPostedYearEndDispositions, matching the trial balance exclusion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
05b954ac1d |
feat(deadlines): årsstämma replaces bokslut + moms_yearly auto-complete + EU-sales suggestion (#1059)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal The f_skatt deadline was gated on the F-skatt approval flag (DB default true), giving nearly every company 12 monthly payment reminders for a tax Skatteverket may not have debited at all (64% of all system deadline rows, one lifetime completion). Approval carries no recurring obligation; the monthly duty is payment of debiterad preliminarskatt and exists only while the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.). - Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already collected at onboarding, previously unread) and retitle it as a payment. - Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.). - Declare the prod-only preliminary_tax_monthly column in a migration so installs built purely from migrations stop failing tax-settings saves. - Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses it durably (hard deletes were resurrected by the nightly backfill within 24h); generator, backfill, and every read surface respect it. - Prune upcoming f_skatt rows for companies with no debited amount. Closes part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation The arbetsgivardeklaration deadline was gated on pays_salaries, which is wrong in both directions: a registered employer must file AGI every month including nil months (SFL 26 kap. 3 par.), and companies actively running payroll with the flag off got no AGI reminders at all (each missed monthly filing risks a forseningsavgift). - New company_settings.employer_registered (nullable, no default) gates AGI and the storforetag skatteinbetalning row; pays_salaries remains a fallback for rows saved before the flag existed and keeps its UI meaning. - Migration backfills employer_registered=true from pays_salaries=true and from actual payroll activity (salary_runs). - New employer_seasonal flag: sasongsregistrerade file only for payment months plus a December nil declaration, so only the December-period row is generated. - Settings UI: registration + seasonal checkboxes (sv/en strings). - AGI XML generation no longer auto-completes the deadline as submitted: SFL 26 kap. deems the obligation satisfied only when the declaration has come in to Skatteverket. The Skatteverket extension's kvittens reconcile remains the confirming path; manual filers tick the deadline themselves. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): statutory arsstamma replaces bokslut, moms_yearly auto-complete, EU-sales suggestion - Replace the non-statutory 'bokslut' deadline (3 months after FY end, no legal basis, off-by-one month math for broken FYs) with the statutory arsstamma deadline: within 6 months of FY end per ABL 7 kap. 10 par., the corporate act that gates the arsredovisning filing chain. Migration deletes pending bokslut rows; the backfill cron generates arsstamma rows. - Complete moms_yearly on Skatteverket submission/kvittens: the yearly branch previously returned null with a stale comment claiming annual VAT has no deadline type, leaving yearly filers with an eternally open row. The fiscal-year tax_period label is derived from company settings. - Add /api/settings/eu-trade-signal + a tax-settings callout: companies with booked EU sales (3108/3308/3107, last 15 months) but EU-trade/PS flags off are prompted to confirm the periodisk sammanstallning obligation (SFL 35 kap., 1 250 kr late fee per report). Suggestion only, never auto-enables. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): include dismissed_at in DeadlineForm payload The Deadline type gained the required dismissed_at field; the form's submit payload literal must carry it for the Omit<Deadline, ...> shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): make system-deadline dismissal atomic Constrain the dismiss update to source='system' and verify a row was actually updated: a concurrent regeneration can delete the row between lookup and update, and the route must not report a phantom success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da4d5a39ae |
feat(deadlines): gate AGI on employer registration + stop completing AGI at XML generation (#1062)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal The f_skatt deadline was gated on the F-skatt approval flag (DB default true), giving nearly every company 12 monthly payment reminders for a tax Skatteverket may not have debited at all (64% of all system deadline rows, one lifetime completion). Approval carries no recurring obligation; the monthly duty is payment of debiterad preliminarskatt and exists only while the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.). - Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already collected at onboarding, previously unread) and retitle it as a payment. - Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.). - Declare the prod-only preliminary_tax_monthly column in a migration so installs built purely from migrations stop failing tax-settings saves. - Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses it durably (hard deletes were resurrected by the nightly backfill within 24h); generator, backfill, and every read surface respect it. - Prune upcoming f_skatt rows for companies with no debited amount. Closes part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation The arbetsgivardeklaration deadline was gated on pays_salaries, which is wrong in both directions: a registered employer must file AGI every month including nil months (SFL 26 kap. 3 par.), and companies actively running payroll with the flag off got no AGI reminders at all (each missed monthly filing risks a forseningsavgift). - New company_settings.employer_registered (nullable, no default) gates AGI and the storforetag skatteinbetalning row; pays_salaries remains a fallback for rows saved before the flag existed and keeps its UI meaning. - Migration backfills employer_registered=true from pays_salaries=true and from actual payroll activity (salary_runs). - New employer_seasonal flag: sasongsregistrerade file only for payment months plus a December nil declaration, so only the December-period row is generated. - Settings UI: registration + seasonal checkboxes (sv/en strings). - AGI XML generation no longer auto-completes the deadline as submitted: SFL 26 kap. deems the obligation satisfied only when the declaration has come in to Skatteverket. The Skatteverket extension's kvittens reconcile remains the confirming path; manual filers tick the deadline themselves. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): include dismissed_at in DeadlineForm payload The Deadline type gained the required dismissed_at field; the form's submit payload literal must carry it for the Omit<Deadline, ...> shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): make system-deadline dismissal atomic Constrain the dismiss update to source='system' and verify a row was actually updated: a concurrent regeneration can delete the row between lookup and update, and the route must not report a phantom success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3c0bf3f584 |
feat(deadlines): gate F-skatt reminders on debited preliminary tax + durable dismissal (#1057)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal The f_skatt deadline was gated on the F-skatt approval flag (DB default true), giving nearly every company 12 monthly payment reminders for a tax Skatteverket may not have debited at all (64% of all system deadline rows, one lifetime completion). Approval carries no recurring obligation; the monthly duty is payment of debiterad preliminarskatt and exists only while the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.). - Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already collected at onboarding, previously unread) and retitle it as a payment. - Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.). - Declare the prod-only preliminary_tax_monthly column in a migration so installs built purely from migrations stop failing tax-settings saves. - Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses it durably (hard deletes were resurrected by the nightly backfill within 24h); generator, backfill, and every read surface respect it. - Prune upcoming f_skatt rows for companies with no debited amount. Closes part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): include dismissed_at in DeadlineForm payload The Deadline type gained the required dismissed_at field; the form's submit payload literal must carry it for the Omit<Deadline, ...> shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): make system-deadline dismissal atomic Constrain the dismiss update to source='system' and verify a row was actually updated: a concurrent regeneration can delete the row between lookup and update, and the route must not report a phantom success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
97907a5a5c |
fix: article ordering, free-text rows, invoice back-nav, onboarding resilience (#1053) (#1056)
* fix: article number ordering, free-text rows, invoice back-nav, onboarding resilience (#1053) Four fixes from Discord feedback in issue #1053: - Articles now order by article number with numeric-aware comparison ('2' before '10', unnumbered last, name tiebreak) in the invoice editor's article picker and as the register's default sort, via a shared lib/articles/sort.ts. Name order put article "1" last. - Invoice rows with no amounts (quantity 0, unit price 0) render as pure text rows on the PDF, the invoice detail page, and the review step via shared isTextLikeLine(), instead of printing "0 / 0,00 SEK / 0,00 SEK". Display-only; booking untouched. - The invoice editor navigates with router.replace after saving, so the detail page's back arrow returns to the list instead of reopening a fresh editor from history. - A transient query failure no longer reads as "no companies" / "onboarding not done": getActiveCompanyId throws CompanyContextError('resolution_failed') instead of returning null, the Edge middleware fails open on a degraded resolution (no onboarding redirect, no cookie clearing, no locale overwrite), and the dashboard page only redirects to /onboarding on a positively read incomplete/missing settings row. This is the likely cause of the completed onboarding wizard reappearing. Fixes #1053 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CLAUDE.md tenancy line matches actual resolution order (prefs-first, cookie not read) The middleware stopped reading the gnubok-company-id cookie when user_preferences became authoritative (RLS parity); the stale doc line still described cookie-first order and misled review tooling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0e9cca2750 |
Add/customer mcp (#1055)
* feat(mcp): kontoplan account tools + verifikat notes exposure Two gaps reported by an MCP-driven user: no account management in the API, and verifikat notes invisible to agents (they exist in the product but MCP could neither read nor write them). - add staged gnubok_create_account / gnubok_update_account (BAS 2026 prefill for catalog numbers; rename/VAT-default/SRU/activate via update; both LOW risk reference data) - add staged gnubok_set_voucher_note (notes-only annotation, legal on posted entries per the 20260608120000 trigger carve-out) and return entry_notes from gnubok_query_journal - new pending_operations types create_account / update_account / set_voucher_note (CHECK migration + validate companion, applied to staging) - tools/list payload ceiling 54K -> 56K (documented; wire contract, descriptions trimmed first) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): unstick BankID connect flow and stale connection views - respond to the OAuth callback immediately and run the post-connect refresh after the response (next/server after()): users no longer stare at Skatteverket's consumed consent page for up to 40s - open the consent flow in a full tab instead of a 600x750 popup that hid the approve button below the fold - disable connect buttons while the OAuth tab is open (parallel flows overwrote oauth_state + the PKCE verifier) and recover via a closed-tab watcher plus a delayed status refetch - persist MISSING_SCOPE token health from the post-connect sync and show an actionable "approve all permissions" notice - refetch connection state on tab visibility (settings connect panel, enable-banking panel, /skattekonto) so a connect completed in another tab or after a mobile app-switch shows up without a manual reload; fix /skattekonto never clearing its not-connected state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(article-form): add article number field with validation to ArticleForm * feat(account): enforce account type consistency with BAS class and add validation --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bfd5b42eb1 |
feat(settings): open Assistenten on Kunskap with the konteringskarta first (#1044)
The Assistenten settings hub used to open on Minne, with the konteringskarta buried two clicks away (Kunskap tab, below a second nested tab row). Now /settings/assistant opens on Kunskap and the LedgerGraph hero is the first thing on screen. - Kunskap is the default view and first tab; Minne moves to ?view=memory (old ?view=knowledge links still resolve to the default) - Drop the nested Kompetens/Minne/Regler & profil tab row inside the Kunskap view: Kompetens and Minne duplicated the top-level tabs one row above; Regler & profil now renders inline under the graph with a section header (KnowledgeTabs.tsx deleted) - Restore vertical rhythm (space-y-8) between the hero, detail section and footer, lost when the view moved into the settings tabs - Update redirects and memory deep links (/settings/agent-memory, AgentChat memory chips, FactsCard manage link) to ?view=memory - Match the loading skeleton to the new layout Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd1c266cb0 |
chore: salvage unmerged work from the 2026-07-16 worktree audit (#1043)
* chore: salvage unmerged work from the 2026-07-16 worktree audit Four items survived the 43-file dirty-tree audit as genuinely unmerged: - CLAUDE.md: Definition of Done rule 9, the last mile is verified in-session, not assumed (project-level counterpart of the switch-on check; cloud agents only see the repo file). - DECISIONS.md: eight decision lines from 2026-07-09 to 2026-07-15, condensed and scrubbed of production identifiers for the public repo. - .claude/skills/loop-ignite: skill that audits the agentic loops and ignites dead ones; must live on main for cloud routines to load it. - lib/bokslut/ixbrl testbank manual E2E: encodes the working testbank endpoints and the Luhn-valid test pnr (the documented one fails); skipped unless BOLAGSVERKET_TESTBANK_E2E=1, so zero CI cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: address review findings on the salvage batch - testbank e2e: kontrollera returns HTTP 200 even for invalid documents (outcome is in utfall), so assert zero typ='error' entries; also assert grunduppgifter returns a company name, not just the echoed orgnr. - loop-ignite: make ignition explicitly idempotent (enable/repair an existing trigger before creating, never duplicate). Skipped the fourth finding (require an observed firing as switch-on proof): a just-created cron cannot have fired yet; the audit table already reports last observed run per loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b8e3fa130 |
fix(vat): enforce decimal vat_rate on supplier invoice items and normalize MCP percent extraction (#1049)
Supplier invoice items store vat_rate as a decimal fraction (0.25) while customer invoices use integer percent (25). The shared Zod schema accepted 0-100, so a percent-shaped vat_rate silently booked 2500 % VAT via line_total * vat_rate, and the MCP inbox-conversion path staged the AI extraction's percent-integer vatRate straight into the decimal column with per-line vat_amount 0. Part of #310. - CreateSupplierInvoiceItemSchema.vat_rate is now a literal union of the statutory decimal set (0, 0.06, 0.12, 0.25) with a unit-hint error, covering the cookie route, the invoice-inbox convert route, and /api/v1 (whose runtime ALLOWED_SV_VAT_RATES guard stays as defense in depth). - New shared normalizeVatRateToDecimal() in lib/vat: percent-shaped values (25, 12, 6) divide by 100, results snap to the legal Swedish set, and anything else (foreign 19/20, non-finite) maps to 0. - gnubok_create_supplier_invoice_from_inbox normalizes vatRate at the extraction boundary and derives per-line vat_amount when the extraction carries none, so the staged header vat_amount is honest. - The pending-operation executor normalizes staged vat_rate on insert, so rows staged before this fix cannot book percent-scaled VAT. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
03fd1b60b7 |
fix(bokslut): derive preview netResult from the 2099/2010 closing amount (#1045)
The Arets resultat summary card on the bokslut preview step read its figure from generateIncomeStatement, which excludes entries tagged source_type='year_end'. Bokslut-flow entries (annual depreciation, bokslutsdispositioner) carry that tag, so the card showed the pre-depreciation result while the bokslutsverifikation table below it (built from the unfiltered trial balance) included depreciation in the 2099 balancing line. previewYearEndClosing now derives netResult from the closing-lines totals before the balancing line is appended: it equals, by construction, the signed amount transferred to 2099 (AB) or 2010 (EF); positive = credit = vinst, negative = debit = forlust. The posted verifikat is unchanged: executeYearEndClosing only consumes preview.closingLines, never netResult. Fixes #766 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a5e37d3510 |
Fix/build (#1041)
* fix(bookkeeping): harden correction account changes * feat(tax): enhance tax deadline generation with new settings and filing methods - Added new company settings: tax_turnover_over_40m, vat_has_eu_trade, vat_filing_method, periodisk_sammanstallning_enabled, and periodisk_sammanstallning_filing_method. - Updated deadline generation logic to accommodate new settings affecting VAT and employer declaration deadlines. - Implemented tests for new functionality, ensuring that completed obligations are preserved and not replaced by new pending rows. - Introduced a cron job to backfill missing tax deadlines for companies with settings but no upcoming deadlines. - Updated API routes for generating tax deadlines and handling cron jobs. - Modified database schema to include new columns for tax filing profiles and constraints for filing methods. * fix(invoices): record credit note reconciliation guard * fix(tax): correct automatic deadline settings * fix(tax): key AGI deadline to VAT taxable base and add storforetag payment deadline The 26th filing day for the skattedeklaration (AGI and VAT together) hinges on one statutory measure, a VAT taxable base above SEK 40 million (SFL 26 kap.), not a separate employer turnover. Drop employer_turnover_over_40m and derive the AGI schedule from vat_registered plus vat_taxable_base_over_40m, so a non-VAT-reporting employer is never shown the 26th when its binding date is the 12th. Also: - add a skatteinbetalning deadline row (12th, 17 January) for storforetag, whose deducted tax and employer contributions are due before the 26th filing date - normalize legally incoherent over-40m flag combinations to the earlier small-company schedule in a follow-up migration - replace hardcoded 27 December dates with the banking-day adjustment - extend the 40m help text to cover the SKV-decided early filing election and the payment-still-on-the-12th rule - document the regeneration race repaired by the daily backfill cron Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(migrations): add AGI and VAT filing logic with employer column removal * feat(settings): implement VAT registration logic and update related flags; enhance deadline handling --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f0907da7e9 |
fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches (#1033)
* fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches The v1 (MCP-facing) match-supplier-invoice route resolved paymentAccount via resolveSettlementAccount but only passed it to createSupplierInvoicePaymentEntry for pure-SEK matches (gated on isPureSek) and never to createSupplierInvoiceCashEntry at all. A foreign-currency match, or a kontantmetoden match, settling from a bank/cash account other than the primary 1930 (e.g. a EUR account on 1940) was still misbooked to 1930: the same class of bug PR #985/#986 fixed for the pure-SEK accrual path. Pass the resolved account through both branches unconditionally (the generators' internal 1930 default remains the documented no-link fallback, reached via resolveSettlementAccount's own fallback for transactions without a cash_account_id), and widen the findUnresolvableAccounts chart pre-validation from the pure-SEK accrual path to every non-customLines branch, since all of them now consume the resolved account. The dashboard route needed no code change: its FX/cash-method branches were already threaded inside PR #985 itself. Added branch-level regression tests on both routes (linked non-1930 account books to that account; no cash_account_id falls back to 1930; deactivated resolved account rejects with ACCOUNTS_NOT_IN_CHART before booking). Fixes #1000 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(v1): validate settlement account against the chart before the conflicting-JE storno The widened findUnresolvableAccounts pre-validation ran after the conflicting-categorization storno, so a request rejected with ACCOUNTS_NOT_IN_CHART could first reverse the transaction's posted categorization entry: an irreversible side effect on a failed request. Move the paymentAccount resolution and the chart validation ahead of the storno block (same !customLines guard, same error shape) and add a regression test asserting reverseEntry is never called when the chart validation fails. The dashboard route has no storno block and no chart pre-validation on this path, so it is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1dc85736d8 |
feat(import): add Wise (TransferWise) CSV import format (#1018)
* feat(import): add Wise (TransferWise) CSV import format Wise exports a single multi-currency transaction history (one row per balance movement). Add it as a bank-file format plugin so it flows through the existing upload -> preview -> confirm -> execute wizard. - lib/import/bank-file/formats/wise.ts: quote-aware parse (dates contain a space), Direction IN/OUT drives the sign, booked on the moved side (target for IN, source for OUT). Native currency preserved; SEK conversion is left to the downstream FX/booking pipeline (Riksbanken). - Non-zero Wise fees become their own negative "Wise avgift" row (source and target), so the fee books separately and the balance ties out. - Only COMPLETED rows import. external_id keys on the stable Wise ID (TRANSFER-/PLAN_ORDER-, -fee suffix for fee rows) via a new 'wise' branch in generateExternalId, so re-imports dedup exactly. - Register the format (types, parser list), add it to the manual-format picker and the v1 /imports/bank format enum. Tests cover detection, IN/OUT signing + currency, fee splitting, stable external_id, and COMPLETED-only filtering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Reinthal <email@reinthal.me> * fix(import): harden Wise parser against malformed rows (CodeRabbit #1018) - Strict amount parsing: reject "12abc"/"1,234" instead of parseFloat coercing them to 12/1 and silently corrupting the imported amount. - Require Status to be exactly COMPLETED: a blank/missing status no longer slips through the completed-only filter. - Fail hard on an unsupported Direction: a blank or non-IN/OUT value (e.g. NEUTRAL for a balance conversion) throws instead of being guessed as income; the parse route surfaces it as BANK_FILE_PARSE_FAILED. Proper conversion support is tracked in #1019. - Never invent currencies: a missing movement currency skips the row with a warning (no SEK default), and a fee with no currency of its own is dropped with a warning rather than inheriting the movement currency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Reinthal <email@reinthal.me> --------- Signed-off-by: Alexander Reinthal <email@reinthal.me> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
f8033cb32d |
fix(transactions): bind manually-fed transactions to a cash account (#1016) (#1017)
* fix(transactions): bind manually-fed transactions to a cash account (#1016) create_transactions inserted rows with cash_account_id = null, so ledger accounts fed via MCP/CSV without a PSD2 feed (e.g. 1935 Wise SEK) had no kassakonto: get_reconciliation_status 404'd with "Okänt kassakonto" and the "Matcha mot befintlig verifikation" dialog fell back to 1930. No schema change: cash_accounts.bank_connection_id is already nullable and source='manual' already exists (every company is seeded a manual 1930). This is the creation-side leg of the #985-#987 root cause: the resolution chain was fixed, but manually-fed accounts never got the cash_account_id link. - Add ensureManualCashAccount (lib/cash-accounts/service.ts): find-or-create a manual (source='manual', bank_connection_id=null) cash_accounts row for a ledger slot, tolerating the (company_id, ledger_account) UNIQUE race. - Add an optional ledger_account hint (^19xx) to gnubok_create_transactions; commitCreateTransaction resolves/creates the manual account and sets cash_account_id on the inserted row. Reconciliation and voucher matching then resolve the real account unchanged. Forward-looking; historical cash_account_id=null remediation stays in #1001. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Reinthal <email@reinthal.me> * fix(cash-accounts): guard ensureManualCashAccount against currency mismatch (CodeRabbit #1017) The existing-row lookup matched only on (company_id, ledger_account) and returned the row id ignoring currency, so a SEK transaction hinting at a ledger already claimed for USD would bind to the wrong-currency cash account. Since that pair is UNIQUE (one currency per ledger), a mismatch is a real conflict: throw instead of silently mis-binding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alexander Reinthal <email@reinthal.me> --------- Signed-off-by: Alexander Reinthal <email@reinthal.me> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
aa5edd3aa7 |
fix(database): unblock credit note constraint validation (#1024)
* docs: record legacy credit note migration repair * fix(database): assert repaired credit note state --------- Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
5ac560ce41 |
fix: generate tax deadlines for the installed base + correct 2893 label carryover (#1029)
* fix(bookkeeping): refresh correction line description on account change When editing an ändringsverifikation, CorrectionEntryDialog pre-filled each line's description from the original entry but never re-derived it when the user changed the account, so a description carried over from the old account (e.g. 2393 "Lån från närstående personer, långfristig del") stayed stale on the newly chosen account (e.g. 2893, the kortfristig account). The regular JournalEntryForm already auto-fills on account change; this mirrors it. The refresh is guarded: it only overwrites the description when it is empty or still equals the previously selected account's name, so a memo the user typed themselves is preserved. Logic is extracted into a pure, unit-tested helper. Note: the wrong text on an already-posted correction cannot be repaired (line descriptions of posted verifikat are immutable per BFL / migration 017); this prevents recurrence on future corrections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): generate tax deadlines for the installed base Automatic tax deadlines only regenerated when a tax-relevant settings field changed value (didTaxFieldsChange). Companies fill those fields once at onboarding, so a later save changed nothing and generated nothing; the annual cron was the only unconditional trigger. As a result only ~5 of ~776 real companies had any system deadlines, and the /deadlines empty state told users to "check the tax settings" that were already complete. - Settings save now also regenerates when the company has zero system deadlines yet (safe first-time backfill; cannot reset is_completed/status). Decision extracted into shouldRegenerateTaxDeadlines() with tests. - The empty-state banner gets a "Generera nu" action wired to the existing /api/tax-deadlines/generate route (previously it had no caller). New sv/en strings. - generateNewYearDeadlines (annual cron) paginates company_settings via fetchAllRows: a plain .select() silently caps at 1000 rows, leaving companies beyond the cap without next-year deadlines. - scripts/backfill-tax-deadlines.ts: one-off that reruns the real generator for non-sandbox companies with zero system deadlines. Known gap (follow-up): moms_period='yearly' has no deadline config, so annual VAT filers get no momsdeklaration deadline yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): address review feedback + fix settings-route test - settings/route.ts: fail safe when the system-deadline count query errors. A null count on error was treated as 0, which would trigger a delete+regenerate and reset is_completed/status on a transient failure; now a count error keeps the self-heal off (CodeRabbit, Major). - Update app/api/settings/__tests__/route.test.ts (added on main via the withRouteContext refactor) for the extra deadline-count query and the new shouldRegenerateTaxDeadlines export; add self-heal / no-regen cases. - Soften the "no deadlines created" copy: zero generated rows can also mean no applicable obligations (or the moms_yearly gap), not just incomplete settings (CodeRabbit, Minor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
072aedeaf9 |
Fix/supp ag fb (#1023)
* fix: prevent credit notes from entering payment flow * fix: persist and display customer personal numbers * feat: configure automatic invoice reminder days * fix: issue credit notes through send flow * chore: add repository agent guidance * feat(mcp): route tools across user companies * fix(articles): delete unused register entries * feat(invoices): improve issued invoice actions * feat(supplier-invoices): retain uploaded source documents * docs: record implementation decisions * feat: enhance customer personal number handling and validation - Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers. - Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema. - Implemented masking and encryption for personal numbers to enhance data protection. - Introduced new utility functions for masking and encrypting personal numbers. - Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries. - Enhanced error handling and logging for credit note issuance and invoice processing. - Updated tests to cover new credit note creation guards and personal number handling. * test: enhance list companies test with supabase query mocks |
||
|
|
a558c75678 |
fix(bookkeeping): clear the period IB link when stornoing an opening balance (#1022)
* fix(bookkeeping): clear the period IB link when stornoing an opening balance Reversing a period's opening-balance verifikat left fiscal_periods.opening_balance_entry_id pointing at the reversed entry, and nothing reads that pointer's status. The storno was a no-op where it mattered: - getOpeningBalances() reads the linked entry's lines with no status filter, so the Balansrapport kept showing the cancelled IB. - Year-end blocks while the pointer is non-null and tells the user to "reverse it before re-running year-end": advice the storno could never satisfy. delete_last_voucher and the opening-balance/correct route both refuse an already-reversed entry, so there was no in-app way out. reverseEntry now drops the link, mirroring the bank-transaction unlink directly above it. getOpeningBalances falls through to the duplicate-safe compute_prior_opening_balances RPC, and year-end can re-book the IB. This also closes the documented residual edge in opening-balance/correct (storno succeeded, relink failed) and makes runYearEnd's rollback comment true. Two statements, not one: enforce_opening_balance_immutability rejects a pointer change while opening_balances_set is still true. Covered by a pg-real test, since a mocked client happily accepts the single-statement version that the real trigger rejects. Found via support: a user could not close 2025 because bogus 2026 opening balances from a SIE import would not go away. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: record the storno/IB-link decision in DECISIONS.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b6332e9ff4 |
Fix/skv connection flow (#1015)
* feat(salary): one-click AGI submission with filing state machine and success feedback The AGI panel required users to know that "Ladda ner AGI-fil" was the generate step, then click submit, signing link, and kvittens manually. A nollkorning filing stalled on "AGI-XML saknas" pointing at a UI path that does not exist. - New primary button "Lamna in till Skatteverket" chains the existing endpoints client-side: generate XML if missing, POST underlag, poll kontrollresultat, create signing link, open Mina Sidor in a tab opened synchronously at click (popup-blocker safe). Inline stepper shows each step; the four old buttons become collapsed advanced/recovery actions, auto-expanded in stale-draft and rejected states. XML download stays visible and free for manual filing. - deriveAgiFilingState() + useAgiSubmission() lift the per-period submission record to the run page: the progress rail and salary hero now render the real state machine (generated, underlag inskickat, vantar pa BankID-signatur, inlamnad med kvittensnummer) instead of telling users to "lamna in" an already-submitted declaration. - Success card with kvittensnummer and signature metadata once signed, plus a toast when a poll flips the state while the page is open. - AGI kvittens cron every 15 min instead of every 2 h so filings signed on another device get stamped and emailed promptly. - Advanced submit also auto-generates, and the stale "Lon -> AGI -> Generera" error text now points at the real buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): instant OAuth callback feedback and dead-attempt cleanup The bank redirect landed on a blank page for the several seconds the callback spent exchanging the PSD2 session and mirroring accounts, and every failed connect attempt left a status='error' row that rendered forever as an "Atgard kravs" card next to a successful retry, showing duplicate connections to the same bank. - Stream a branded "Slutfor bankanslutningen" progress page from the callback: the shell flushes before the session exchange starts and a script/meta redirect follows when the work completes, with a 30s slow-work escape hatch. Fast outcomes (denial, bad params, unknown state) keep their plain redirects. - Delete never-activated connection rows (no session_id, no accounts_data) on denial or exchange failure, and sweep leftovers for the same bank on the next connect. Established connections keep their "Atgard krävs" card via the accounts_data guard; FKs are ON DELETE SET NULL so deletion has no dependents. - Show "Banken ar ansluten: hamtar dina konton" while the settings panel loads after the callback instead of an anonymous spinner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): reject re-send of issued invoices and gate bookkeeping on the sent flip A direct POST to /api/invoices/[id]/send against an already-issued invoice re-emailed the customer and posted a second revenue verifikat (createInvoiceJournalEntry has no dedup), overwriting journal_entry_id and orphaning the first entry. Only the UI hid the button; the v1 route and the MCP commit executor already rejected non-drafts. - Non-draft invoices now return 409 INVOICE_ALREADY_SENT. - The draft to sent status flip is an optimistic lock (status guard plus row-count check); journal entry, accrual schedules, PDF archival and the invoice.sent event only run for the request that won the flip. - On a flip failure the journal entry is deferred: the row stays draft and a retry re-runs the pipeline, ending with exactly one verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): payment links, failure visibility and sandbox guard for recurring auto-send - sendInvoiceFromSchedule now auto-creates an online payment link via applyPaymentLinkToInvoice before rendering and passes the payment link QR to the PDF: parity with the dashboard and v1 send routes, which recurring invoices silently lacked. - The recurring cron persists last_run_warning both when a claimed run throws (hourly retries stay visible on the schedule) and when a stale schedule is rolled forward, so a deterministic failure can no longer skip a month silently. - Auto-send is blocked for sandbox companies at the email chokepoint (freeze-and-retain: the invoice is still generated as a draft), covering both the cron and the run-now route with one guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): close the Fortnox payroll API gaps (phases 1-4) Payroll now runs end-to-end through the open API, including onboarding a client from another payroll system, with every write staged for approval. - v1: per-employee payslips (list/detail/PDF), payslip line writes, run roster attach/remove, absence ranges (per-day storage), jamkning fields, cutover opening balances (single + atomic bulk PUT), vacation balance + vacation-year-close. PUT added to the wrapper's idempotency/ test-key set (test keys could otherwise write through PUT). - MCP: 10 new tools (get_employee/get_payslip/list_absence/ get_vacation_balance reads + staged update_payslip_line, register_absence, create_employee, update_employee, set_employee_opening_balances, close_vacation_year), executors, risk tiers, op-type CHECK expansions. create_employee encrypts personnummer at staging: pending_operations never holds plaintext. - Scope-map audit retrofit: 11 formerly unmapped tools now scoped; BREAKING for keys that relied on the 4 default-allow writes. - Cutover: employee_opening_balances (derived lock trigger, self-unlocks on run correction), engine YTD/karens/liability integration, Ingaende saldon section in the employee editor. - Arbetsschema-lite: employees.hours_per_week/workdays_per_week drive the hourly/daily divisors; legacy 173/21 preserved exactly at defaults so existing pay math is byte-identical. - Vacation ledger + semesterberedning/arsavslut: recomputed per-year day balances (synced on book/correct, non-fatal), year-close with the min-20 floor, 5-year sparade-dagar expiry to forced payout, and a 2920/2940 drift adjustment via the bookkeeping engine; Semester dashboard card with preview-then-confirm dialog. - Fix: Zod 4 defaults leak through .partial(), which made every sparse employee PATCH fail validation and reset defaulted columns. Migrations 20260713100000/101000/110000/121000/122000 (applied to staging with version rows; prod via merge). vacation_ledger renamed from 20260713120000 to avoid colliding with vat_declaration_totals_rpc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: cut dashboard page-load latency (region, round trips, caching, VAT RPC) The dominant cost was infrastructure: Vercel functions ran in iad1 (Washington D.C.) while Supabase (DB + auth) lives in eu-north-1 (Stockholm), so every request paid 4-5 transatlantic round trips of auth + company resolution before doing any real work (measured 530-1900ms for single-query GETs in prod logs). Pin functions to arn1 and cut the redundant work on top: - vercel.json: functions to arn1, same city as the database - getActiveCompanyId: preference + first-membership queries run in parallel; the fallback result doubles as validation in the common single-company case (one round trip instead of two sequential) - withRouteContext: Server-Timing header and authMs/companyMs/handlerMs in the op-completed log, so latency is attributable per phase - dashboard layout: nav badge counts off the critical path; DashboardNav loads them client-side via the new use-worklist-badges SWR hook with debounced realtime revalidation - swr (new dependency, approved): global provider; useCompanySettings shares one cache entry across consumers and renders from cache on back-navigation instead of re-showing skeletons - /pending: realtime refetch debounced; bulk operations previously fired 4 requests per row-change event - VAT declaration: new get_vat_declaration_totals RPC returns per-account totals, settlement-shape detection (#984) and source_type counts in ONE round trip instead of paging every entry+line through PostgREST. Account lists stay TS-side parameters so ACCOUNT_RUTA remains the single source of truth. Shape-exclusion coverage moved to tests/pg/vat-declaration-totals-rpc.pg.test.ts; DDL already applied to staging. - bundle: CommandPalette lazy-mounts on first Ctrl/Cmd+K, AgentChat dynamic-imports the markdown parser, @vercel/speed-insights (new dependency, approved) added for real-user timings The /salary fetch-waterfall fix from the same effort already landed inside 2084a756. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): settle öre-rounded payments from the mark-paid flow An invoice with öresavrundning shows a rounded "Att betala" on the PDF; the customer pays that amount (up to 50 öre off the stored öre total) and the invoice-page mark-paid flow rejected it with MATCH_AMOUNT_EXCEEDS_REMAINING: a dead end, while the bank-transaction match flow already absorbed the residual to 3740. - PaymentBookingDialog now proposes the rounded bank leg plus the 3740 residual line (credit when rounded up, debit when rounded down), resolved via getDisplayTotal from the per-invoice override and company_settings.ore_rounding. - settleInvoicePayment and the v1 mark-paid route absorb the sub-krona residual, gated by planInvoicePaymentForLines: absorption applies ONLY when the caller lines carry the exact residual on 3740; otherwise the strict plan applies (sub-krona partials stay partial, no-3740 overshoots keep the 400), so the GL can never diverge from the AR sub-ledger. - planInvoicePayment absorb-band boundary tightened to >= 1 kr: an exactly-1-kr overshoot used to slip past both the guard and the absorb branch and silently over-record paid_amount (pre-existing on the bank-match path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): resolve all 7 PR compliance findings - ASVS V3.3: per-request CSP nonce on the enable-banking finalize page (mirrors the mcp-oauth consent page); inline scripts are nonce-bound - ASVS V16: decouple callback finalize work from the response stream (eager promise + next/server after()) so a client disconnect cannot drop session persistence or the consent_granted audit emit - ISO 27001 A.8.15: failed audit-event emits log through the structured logger with a stable message for log-based alerting - ASVS V2.3: recurring-invoice cron and run-now routes resolve isSandboxCompany themselves and pass an explicit suppressAutoSend flag (defence in depth around the email chokepoint, freeze-and-retain kept) - ISO 27001 A.8.11: stagePendingOperation rejects plaintext personnummer-bearing keys in params/preview_data (key-based guard; EF org numbers make value-matching unsafe) - ASVS V4.5: employee PATCH body is truly sparse; cleared number fields are omitted instead of resetting DB values to hardcoded fallbacks - ASVS V8.2.1: route-level tests pin the v1 cross-company deny (404 by convention, not 403) on the payslip PDF endpoint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: implement vacation-year basis change validation and error handling - Added tests to block vacation-year basis changes when open balances exist. - Implemented error handling for open-balances guard query failures in the settings route. - Enhanced absence route to reject reversed date ranges with a validation error. - Updated absence handling to use atomic upserts instead of delete+insert for better performance and reliability. - Refactored salary calculation logic to correctly handle age-based avgifter rates according to Skatteverket's rules. - Improved error messaging for vacation year closure adjustments. - Adjusted employee opening balances handling to preserve audit information during upserts. * feat(settings): add validation to block vacation-year basis change with open balances feat(absence): reject reversed date ranges in absence queries fix(absence): update absence handling to use atomic upserts instead of delete+insert fix(employee): improve validation for jamkning dates in employee updates fix(opening-balances): ensure created_by field is preserved during upserts test(absence): enhance tests for absence range and date validations test(calculation): add tests for age-based avgifter rates and edge cases test(semesterberedning): validate vacation year closure adjustments and error handling test(employee-opening-balances): update tests to reflect changes in salary_run_employees schema * fix(migrations): implement NOT VALID constraints for pending_operations and add validation migration --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aab7e47c35 |
Bug/skv auth (#1010)
* fix(settings): open Skatteverket connect in a popup and fix the skahmst scope check The full-page OAuth round-trip left Skatteverket's pages and the consumed callback URL in browser history directly beneath /settings/tax, so closing settings walked Back into a dead OAuth chain and re-prompted BankID auth. The connect buttons now use the AGIPanel popup + postMessage pattern (the callback already supports window.opener); the page never navigates and the panel refetches status on success. Full-page navigation remains only as the popup-blocked fallback. Also fixes the reconnect-button condition: it checked for a scope literally named 'skattekonto', but SKV grants 'skahmst', which kept "Anslut igen" permanently visible on healthy connections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(skatteverket): extract per-declaration AGI kvittens reconciliation Behavior-identical extraction of the kvittens cron's core (fetch kvittens, promote declaration to submitted, stamp salary_runs, clear cached submission, complete deadline, notify) into lib/agi-kvittens-reconcile.ts so the upcoming post-connect refresh can reuse it. Auth-error mapping (needs_reconsent, grant revocation, APIGW config gaps) and run-level logging stay in the cron, which is why SkatteverketAuthError propagates out of the helper on purpose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): auto-settle production-format AGI skattekonto draws The settlement matcher only understood the SKV test environment's single combined row ("Arbetsgivardeklaration YYYYMM"). Production books the draw as two rows with Swedish month names: "Avdragen skatt maj 2026" (= total_tax) and "Arbetsgivaravgift maj 2026" (= total_avgifter), so auto-settlement has never fired against production data and the salary page kept showing paid periods as unpaid. parseAgiPeriod now also reads the month-name form (which additionally lets match suggestions resolve the period on prod rows, including beslut rows). Settlement classifies draws with start-anchored regexes so correction rows like "Beslut 260703 arbetsgivaravgift mars 2026" can never qualify, and settles a period on either the combined row matching the whole declared amount or exactly one tax row + exactly one avgift row matching their respective totals to the ore, dated to the later of the pair. Anything non-exact (partial draws, duplicates) still falls back to the manual mark-paid button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skatteverket): refresh skattekonto + kvittenser right after OAuth consent Nothing fetched Skatteverket data after a (re)connect: tokens were stored and the user redirected, with the next fetch left to the nightly cron. SKV's per-flow tokens (and refresh tokens) live ~65 minutes, so the crons usually find them dead; right after consent is the one reliable window for a personal-token fetch, which is why reconnecting never made stale numbers recover. The callback now awaits runPostConnectRefresh after storing tokens: a skattekonto sync (upsert + auto-settlement + balance snapshot) plus a kvittens re-check for this company's pending_signature AGI declarations. Awaited on purpose so popup-close means the data is already fresh and UI refetch listeners never race a background job; every step is best-effort and a refresh failure can never fail the connect that just succeeded. The callback's non-popup fallback also switches to window.location.replace so the consumed callback URL (one-shot code + state) drops out of history instead of re-running into a guaranteed CSRF error on Back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): reload the salary dashboard after a Skatteverket reconnect Listens for the BankID popup's skatteverket-oauth-success message and re-runs load(). Because the OAuth callback awaits the skattekonto sync and AGI auto-settlement before responding, the refetch already sees settled tax-payment state: the "Skatt att betala" card flips to paid the moment the popup closes, without leaving the page. Also logs the three decisions behind this series in DECISIONS.md (awaited post-connect refresh over after(), exact-pair settlement over per-period summing, popup flow over in-place history repair). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): verify OAuth popup source identity before trusting postMessage The three 'skatteverket-oauth-success' listeners accepted any same-origin message, letting any same-origin script trigger a data reload or a fake success toast (OWASP ASVS V3.2, SOC 2 CC6.1). The two components that open the popup now keep its handle in a ref and require event.source to be that exact window; a window reference cannot be forged by same-origin scripts, which is strictly stronger than a nonce threaded through the OAuth flow. The salary dashboard never opens the popup, so its raw message listener is replaced by a 'skatteverket-connection-updated' CustomEvent dispatched only after a component has source-verified the popup (and after disconnect, so connection-state consumers stay in sync). Also extends the connect consent copy (sv + en) to disclose that connecting immediately fetches skattekonto data and checks pending AGI receipts (GDPR Art.5(1)(b) transparency). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): CSP nonce and no-store on OAuth callback, deadline on post-connect refresh Both callback HTML responses now carry a per-response nonce CSP (default-src 'none'; script-src 'nonce-...'; base-uri 'none'; form-action 'none') so injected markup could never execute, plus Cache-Control: no-store because the callback URL carries a one-shot authorization code. The jsLiteral/appUrl injection invariants are documented at the definition site. The awaited post-connect refresh is bounded by a 30-second Promise.race deadline so a hung SKV call cannot hold the OAuth callback open; on timeout the refresh continues best-effort and the user still gets the success response. Refresh failures and timeouts now log through the structured logger with companyId + userId so they are attributable in log aggregation (ASVS V16, ISO 27001 A.8.15, SOC 2 CC8.1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): tenant guard and redacting logs in kvittens reconcile, slim cron response The agi_declarations update now also filters on company_id (ASVS V8.2.1), matching the salary_runs pattern. The reconciler and the cron's error paths log through the structured logger so third-party error strings pass personnummer redaction; uuidKvittens is dropped from log context (GDPR Art.5(1)(f) minimization, declarationId suffices). response_data gains submittedAtEstimated so the signeradTid fallback can never be mistaken for the legal filing time, and the submitted_by comment now states explicitly that it records the technical submitter while response_data.signeradAv is the authoritative legal signatory (BFL 5 kap 6 par, BFNAR 2013:2 kap 8). The cron HTTP response omits companyId per result row and sends Cache-Control: no-store; the extension_data delete documents why the period-scoped cache key needs no declaration-id guard. Cron tests observe the logger via a mock without weakening any assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): diagnosable settlement refusals and signeradAv ROPA documentation Settlement refusals (candidates present but amounts mismatch) now emit a structured info log with declared-vs-drawn ore amounts per kind, so a rounding divergence between stored declaration totals and SKV's actual draw is diagnosable instead of silently falling back to the manual button. No transaction texts are logged (they can carry personal data). parseAgiPeriod documents the beslut-row audit: correction rows parse to their period on purpose for match-suggestion boosting; settlement never uses parseAgiPeriod (anchored classifiers + parseNumericAgiPeriod only) and the only callers require an exact amount+side 1630 match first. .compliance/ropa.yaml documents signeradAv (signer personnummer in the SKV kvittens stored in agi_declarations.response_data): lawful basis Art.6(1)(c) via BFL 5 kap 6 par / BFNAR 2013:2 kap 8, 7-year retention per BFL 7 kap 2 par, access via company-membership RLS. DECISIONS.md records the accepted-with-documentation calls from the compliance review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): add 'already_claimed' status to reconcile outcomes and enhance logging for pending lookups --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b51af3d80 |
feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) (#935)
* feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) The human-facing surface for the openwiki ledger-context: a read-only page that renders the exact payload the AI agent reads (Accounted://ledger/context + the briefing digest) as a legible profile of how this company books. - Route app/(dashboard)/agent-knowledge (server component) calls the shared buildLedgerContext(supabase, companyId) directly: one payload, two renderers, no new API or data path. - Sections mirror the payload 1:1: coverage/freshness strip, counterparty patterns (monochrome confidence bars + seen/agree evidence), supplier patterns, explicit rules shown as authoritative instructions distinct from observed patterns, account usage, VAT profile, conventions. - Nav entry in the Analys group (icon Brain), ungated so it doubles as an upsell; flip requiredCapability to paywall. - Design per .claude/rules/design.md (PageHeader, Card, Table, Badge, AccountNumber BAS tooltips); sv + en strings (agentKnowledge namespace). VAT/BAS labels stay Swedish in both locales per i18n rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): deep entity-resolved analysis + radial graph for the knowledge page Reworks the 'Vad din agent vet' page from tables into a radial-hub graph driven by a new full-history deep analysis, per founder feedback. - fix(rpc): median_booking_lag_days now measures real posting promptness via committed_at, not entry_date (which the bank flow sets to the transaction date, giving a ~0 tautology: 151/152 on prod). migration 20260708120000. - feat(rpc): get_ledger_deep_context (migration 20260708130000): full-history, deterministic, read-side. Merges counterparties by normalize_counterparty_key (e.g. Claude = 14 bookings across 12 name variants, weekly, 9 710 kr, always 5420), mines booked verifikat for SEK spend (coalesce amount_sek), detects recurrence cadence, dominant account + share, plus supplier entities. Storno excluded, corrections kept; 19xx/26xx excluded from the dominant contra. - LedgerGraph: radial SVG (company center, accounts inner ring, payees outer ring), hover/focus reveals variants + spend + cadence + account. Keyboard focusable nodes with per-node accessible names + a screen-reader data table. - Page fetches the deep context alongside the light context; coverage strip gains tracked-payee / recurring / tracked-spend stats. sv + en strings. - 14 pg tests (light + deep) green; both RPCs applied to prod + version-matched. Reviewed by an adversarial multi-lens pass (accounting/SQL, frontend/a11y, prod-fact verification); all four verified findings fixed (SEK currency, storno-lag guard, keyboard a11y, spacing tokens). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): gentle mount fade-in for the radial map (reduced-motion safe) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): render the page for a rules-only company (empty-state edge case) isEmpty ignored explicit_rules, so a company with configured mapping rules but no posted transactions hit the 'hasn't learned anything' empty state and lost its rules section. Rules are independent of bookings. (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): show Kompetens (skills) + Fakta (memory) on the knowledge page The 'Vad din agent vet' page now shows the full picture of what the agent knows: alongside the booking map, a compact read-only view of its Kompetens (the Swedish accounting/tax knowledge atoms it ships with, grouped by tier as chips with active/dormant state) and the Fakta it remembers (top learned facts with kind + source), each linking to /settings/assistant for full management. Server-rendered via a new buildAgentCompetence() that mirrors GET /api/agent/skills + /api/agent/memory. Also renders in the no-bookings case so a new company still sees its agent's competence. sv + en strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(agent): restructure knowledge page - hero graph + tabbed detail Declutters the page per feedback: the booking map is the always-visible hero, and the supporting detail (Kompetens · Minne · Regler & profil) moves into tabs so only one view shows at a time instead of a long card stack. Split AgentCompetenceSections into standalone CompetenceCard + FactsCard for the tabs; removed the top stat row on request. sv + en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): Reconciliation Aurora rewrite of the ledger knowledge graph Full rewrite of LedgerGraph: node area = sqrt(spend), colour = cadence, shape = supplier/counterparty, confidence = depth-of-field; on-mount descriptor-collapse animation with xN badge; cadence pulse veins; deterministic seeded layout; framer-motion only (no new deps); keyboard navigation, reduced-motion and sr-only support. Build-verified; 3-lens adversarial review findings fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): sample-size-honest confidence in the ledger knowledge graph dominant_account_share was raw cnt/total, so a counterparty with a single booking rendered as '100% säkerhet': fake certainty by construction (the data_quality_master Item-C / P3 finding). New migration replaces get_ledger_deep_context with a Laplace-smoothed share (cnt+1)/(total+2) (1/1 -> 0.67, 3/3 -> 0.80) and exposes the raw evidence as dominant_account_count / dominant_account_total. The detail card now shows 'Bokförd hit i k av n fall' under the confidence bar; the existing focus buckets, stroke widths and percent labels inherit the honest value unchanged. pg-real test updated to guard the n=1 case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
64ea0fef02 |
fix(transactions): resolve customer-invoice payment account from cash_account_id (#987)
* refactor(transactions): add shared settlement-account resolution helper Cherry-picked from fork/worktree-starry-waddling-wirth (PR #985) commit 34d5d35 — pulling in just the new lib/bookkeeping/settlement-account.ts helper and its test, without the match-supplier-invoice route changes from that PR (those depend on 8bfc31d, not yet on main, and are out of scope here). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): resolve customer-invoice payment account from cash_account_id Customer-invoice payment matching never resolved the bank leg from the matched transaction's own cash_account_id: it was unconditionally hardcoded to 1930 in buildInvoicePaymentClearingLines, createInvoicePaymentJournalEntry, and createInvoiceCashEntry, with no override parameter at all. Any bank receipt landing in a non-primary cash/bank account (a secondary SEK account, or a foreign-currency account like 1940 for EUR) was silently misbooked to 1930 -- the same class of bug PR #985 fixed on the supplier-invoice side, except unconditional there (no stale-setting trigger needed). Adds an optional paymentAccount parameter (default '1930', preserving behavior for every caller that doesn't pass one) to the three lib functions, and threads resolveSettlementAccount(cash_account_id) through every real bank-transaction-matching call site: the dashboard match-invoice route (POST + preview), its v1/MCP-facing counterpart, and the agent/MCP match_transaction_invoice commit path. Deliberately left on default 1930: mark-paid (dashboard + v1, no bank transaction in scope), fix-cash-mismatch (narrow historical repair tool for a different bug), and the agent mark_invoice_paid commit path. Brings in lib/bookkeeping/settlement-account.ts (cherry-picked from fork/worktree-starry-waddling-wirth commit 34d5d35) so this PR is mergeable independently of #985's merge order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(invoice-entries): cover ROT/RUT 1513 line stays fixed under a non-default paymentAccount Compliance-bot finding on PR #987: createInvoiceCashEntry's paymentAccount override was only tested against a plain standard_25 invoice, never combined with a ROT/RUT deduction_type item. The 1513 receivable line was already correctly untouched by paymentAccount (it's never the bank leg), this just closes the test-coverage gap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors Same shared-helper fix as PR #985/#986: resolveSettlementAccount now throws BookkeepingDatabaseError on a genuine cash_accounts query error instead of warning and falling back to 1930. An explicit cash_account_id almost certainly resolves to a non-1930 account, so a transient failure masking it risked the same class of misbooking this whole PR series exists to fix, just via infra flakiness instead of a stale setting. No route/commit.ts changes needed: match-invoice (POST + preview) run under withRouteContext's existing catch-all, and commitPendingOperation already has identical generic bookkeeping-error handling for every other engine failure. Added regression tests for all three call sites (dashboard POST, preview, and the agent/MCP commit path) confirming the abort rather than assuming the shared infrastructure handles it silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(v1): guard resolved settlement account against chart of accounts Closes the two remaining gaps from jakobwennberg's triage on #987 (after rebasing onto main and picking up the already-pushed resolveSettlementAccount abort-on-error fix): - Added the v1 match-invoice route-level test coverage that was missing (cash-account threading, BOOKKEEPING_DATABASE_ERROR abort, ACCOUNTS_NOT_IN_CHART), mirroring the dashboard route's existing settlement-account-resolution tests. - Added the same findUnresolvableAccounts pre-validation guard against chart_of_accounts that 32c07c4 added to #986's match-supplier-invoice route, gated on !customLines since that is the only branch here that consumes the resolved paymentAccount. Signed-off-by: Jonas Flodén Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(bookkeeping): align settlement-account error assertion with #985 Use .rejects.toBeInstanceOf(BookkeepingDatabaseError) instead of toMatchObject({ constructor: ... }), matching #985's edef79d follow-up (the assertion was correct either way, but this is the more idiomatic check and now makes the shared helper's test file byte-identical across #985/#986/#987, removing the add/add merge conflict between them noted in the merge-order validation. Signed-off-by: Jonas Flodén Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(invoice-payment-lines): add missing 3740 coverage for non-1930 paymentAccount CodeRabbit nitpick on #987: the test named "...does not affect the FX-diff or öresavrundning lines" only exercised the 3960 FX-diff branch, never the pure-SEK 3740 öresavrundning branch it also claimed to cover. Split into two tests: the existing one renamed to describe only its FX-diff coverage, plus a new pure-SEK sub-krona-short case with a resolved non-1930 paymentAccount asserting the 3740 line books correctly and the bank leg lands on the resolved account, not 1930. Signed-off-by: Jonas Flodén Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(ci): quote compliance-pr.yml name to fix invalid YAML The unquoted colon in `name: compliance: review (advisory)` (introduced by #890's em-dash removal, which swapped an em dash for a colon in-place) makes YAML read it as a nested mapping key, so GitHub can't parse the workflow at all - every run fails with 0 jobs scheduled. Signed-off-by: Jonas Flodén Signed-off-by: Jonas Flodén <jonas@floden.nu> * Revert "fix(ci): quote compliance-pr.yml name to fix invalid YAML" This reverts commit e7c890245d1834cd8f3c9b13a2bc3247fea7eacb. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
8a41b5dbf2 |
fix(v1): resolve supplier-payment/categorize settlement account from cash_account_id (#986)
* refactor(transactions): extract shared settlement-account resolution helper Dedupe the identical cash_account_id -> ledger_account lookup across match-supplier-invoice (POST + preview) and categorize into resolveSettlementAccount, per CodeRabbit's nitpick on PR #985. Pure extraction, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(v1): resolve supplier-payment/categorize settlement account from cash_account_id Closes the v1/MCP-facing half of the settlement-account gap left open by PR #985 (which only fixed the dashboard routes): - match-supplier-invoice: the pure-SEK accrual path always called createSupplierInvoicePaymentEntry with no paymentAccount at all (hardcoded internal default 1930), never reading the transaction's cash_account_id. Now resolves it via resolveSettlementAccount, same as the dashboard route post-#985. - categorize: never called applySettlementAccount after building the mapping result, so every categorization booked the bank leg to 1930 regardless of which cash account the transaction was linked to. Left the FX/foreign-currency branch (createSupplierInvoicePaymentEntry) and the cash-method branch (createSupplierInvoiceCashEntry) on their pre-existing internal 1930 default, matching #985's own scope decision on the equivalent dashboard route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors Same shared-helper fix as PR #985: resolveSettlementAccount now throws BookkeepingDatabaseError on a genuine cash_accounts query error instead of warning and falling back to 1930. An explicit cash_account_id almost certainly resolves to a non-1930 account, so a transient failure masking it risked the same class of misbooking this whole PR series exists to fix, just via infra flakiness instead of a stale setting. No route changes needed: both v1 call sites (match-supplier-invoice, categorize) already run under withApiV1, whose existing catch-all converts any isBookkeepingError() throw into the correct structured 500. Added regression tests confirming the abort for both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(v1): guard resolved settlement account against chart of accounts CodeRabbit and jakobwennberg's triage on #986 both flagged that resolveSettlementAccount() returns cash_accounts.ledger_account unvalidated, so an inactive/removed account surfaced as the generic MATCH_SI_RECORD_PAYMENT_FAILED instead of an actionable error. Add the same findUnresolvableAccounts pre-check and AccountsNotInChartError race-guard the categorize routes already use. Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(bookkeeping): align settlement-account error assertion with #985 Use .rejects.toBeInstanceOf(BookkeepingDatabaseError) instead of toMatchObject({ constructor: ... }), matching #985's edef79d follow-up (the assertion was correct either way, but this is the more idiomatic check and now makes the shared helper's test file byte-identical across #985/#986/#987, removing the add/add merge conflict between them noted in the merge-order validation. Signed-off-by: Jonas Flodén Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
528c53ffe7 |
fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting (#985)
* fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting match-supplier-invoice (POST + preview) defaulted the credited cash account from company_settings.last_supplier_payment_account, a sticky setting written by the manual mark-paid "betald med privata medel" flow. Once that setting held 2893 (skuld till aktieägare) from an unrelated private payment, every later match against a real bank transaction reused it instead of the transaction's actual bank account, silently booking genuine bank payments as shareholder-loan repayments. Resolve the credit account from the matched transaction's own cash_account_id -> cash_accounts.ledger_account instead (falling back to 1930 when unlinked), mirroring the existing settlement-account lookup in transactions/[id]/categorize/route.ts. last_supplier_payment_account is no longer read by either route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * refactor(transactions): extract shared settlement-account resolution helper Dedupe the identical cash_account_id -> ledger_account lookup across match-supplier-invoice (POST + preview) and categorize into resolveSettlementAccount, per CodeRabbit's nitpick on PR #985. Pure extraction, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(transactions): cover settlement-account lookup-error and preview parity gaps Adds the two test cases CodeRabbit flagged as missing on PR #985: - POST match-supplier-invoice: cash_accounts lookup errors, falls back to 1930 and warns (previously unexercised). - preview match-supplier-invoice: linked cash account other than 1930 (parity with the equivalent POST-route test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): thread resolved settlement account into FX/cash-method supplier-payment branches Closes the remaining items from the Swedish-accounting-compliance bot review on PR #985: - match-supplier-invoice/route.ts computed paymentAccount via resolveSettlementAccount but only passed it into the pure-SEK clearing branch; the FX branch (createSupplierInvoicePaymentEntry) and cash-method branch (createSupplierInvoiceCashEntry) still defaulted to 1930 internally even though both already accepted the parameter. - resolveSettlementAccount now also warns (and falls back to 1930) when cash_account_id resolves to a row with no ledger_account, not just on a hard query error. - Documents company_settings.last_supplier_payment_account's scope via a column comment: it must never be read to resolve a matched transaction's settlement account. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors Compliance-bot finding on PR #987 (applies equally to #985/#986, shared helper): resolveSettlementAccount treated "no cash_account_id" and "lookup threw a real DB error" the same way -- warn and fall back to 1930. An explicit cash_account_id almost certainly resolves to a non-1930 account, so a transient failure masking it risked the exact class of misbooking this whole PR series exists to fix, just triggered by infra flakiness instead of a stale setting. Now throws BookkeepingDatabaseError on a genuine query error; every caller already runs under withRouteContext/withApiV1 (or the pending- operations dispatcher), whose existing catch-all already converts any isBookkeepingError() throw into the correct structured 500 -- no caller changes needed. The "row found but ledger_account empty" case stays warn+fallback (data-integrity gap, not a query failure). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(bookkeeping): use rejects.toBeInstanceOf for settlement-account error assertion Addresses CodeRabbit nitpick from the 2026-07-12 review round: matching BookkeepingDatabaseError via a `constructor` key in toMatchObject is non-idiomatic; toBeInstanceOf is the standard vitest assertion for this. Signed-off-by: Jonas Flodén <jonas@floden.nu> * docs: scope FX/cash-method paymentAccount gap note to /api/v1 and MCP routes CodeRabbit flagged the #1000 reference on PR #985 as ambiguous — the main match-supplier-invoice route's FX/cash-method branches already thread paymentAccount (per the prior entry), so the still-open gap only applies to the /api/v1 and MCP-facing route. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
715c671b67 |
fix(app): stop transient login errors flashing the full-screen fallback (#1002)
* fix(app): stop transient login errors flashing the full-screen fallback The BankID login landing (/auth/callback then the /select-company picker) fires several Supabase auth/DB queries right as the session cookies are set, so a transient failure there (most often a refresh-token rotation race, seen in prod as "Invalid Refresh Token: Already Used/Not Found" on /middleware, or a stale JS chunk after a deploy) threw during render. Only (dashboard) had an error.tsx, so these escaped every boundary and hit app/global-error.tsx, blanking the whole document with a bare "Nagot gick fel" screen for ~1s before the next request repainted and logged the user in as usual. Add an app-level error.tsx (AppErrorBoundary) that catches those segments and their layouts, and harden global-error.tsx. Both recover via a single guarded hard reload instead of React reset(): a reload re-runs middleware (fresh rotated auth cookie) and fetches a fresh bundle (ChunkLoadError after a deploy), matching the browser-navigation self-heal these transients already relied on, whereas reset() re-renders against the same stale payload/bundle. A per-path sessionStorage time-guard bounds it to one reload so a persistent error shows the manual fallback instead of looping. Reported via support: transient "Nagot gick fel" flash on BankID login. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): make the error-boundary reload guard loop-proof (review) CodeRabbit and the PR Agent both flagged that the 12s time-window guard could still loop if a failing render takes longer than the window (e.g. a slow SSR that eventually throws). Replace the time window with a per-path, per-tab-session one-shot flag, so the auto-reload fires at most once per path regardless of timing and a genuinely persistent error settles on the manual fallback. sessionStorage is per-tab, so a fresh visit (or a different path) still gets a fresh auto-recovery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): claim the reload flag atomically + add support escape hatch (review) CodeRabbit round 2: - Major: the one-shot flag was written in the effect but the reload fired even if the write threw (sessionStorage quota full), so it could reload forever without ever recording the attempt. Claim the flag inside decideInitialPhase instead, so entering the 'reloading' phase guarantees the flag persisted; any write failure falls through to the manual 'fallback' (no reload). - Minor: give global-error.tsx a support escape hatch. It can't use SupportLink (no providers when the root layout fails), so use a dependency-free mailto to the hardcoded support address. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
98d0c7f2d0 |
Add/stripe skv (#1004)
* fix(salary): align pain.001 salary file with the Swedish domestic bank dialect Verified against the Swedish Common Interpretation of ISO 20022 (Bankforeningen, Common Payment Types in Sweden, Appendix 1 Example 4: Salaries) and Nordea Corporate Access pain.001 examples v2.6 (2026-06-22), and XSD-validated against the official pain.001.001.03 schema: - drop SvcLvl SEPA (SEPA credit transfers are EUR-only; omitting SvcLvl gets the domestic NURG default) - drop RmtInf (not allowed for SALA salary payments; the beneficiary statement text comes from the Dataclearing LON code) - address employees domestically: clearing as CdtrAgt ClrSysMmbId SESBA, account WITHOUT clearing as CdtrAcct Othr with SchmeNm BBAN - share the clearing/account split (Swedbank 5-digit shift, Nordea personkonto prefix dedup) between the LB and pain.001 generators via splitDomesticBankAccount, fixing pain.001 duplicating the personkonto clearing - clamp MsgId/PmtInfId/InstrId/EndToEndId to Max35Text with the per-tx counter surviving truncation; carry the org number on Dbtr - return 400 from the pain001 route on an invalid clearing instead of emitting a broken file Also includes two unrelated decision-log lines from the parallel revisor-review session (DECISIONS.md is a shared append-only log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nav): surface the year-end chain in the sidebar Add Periodiseringar, Arsredovisning (aktiebolag only) and Inkomstdeklaration (INK2 for AB, NE-bilaga for EF) to the Skatt & bokslut group, in workflow order. Entity gating via a new entityOnly flag on NavItem; isActive carve-outs extended so exactly one row lights up for the new routes. Driven by an external revisor review that concluded these features did not exist because none of them were reachable from the nav. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): Stripe Connect integration behind config gate Connect OAuth per company (only the acct_ id is stored), automatic single-use Payment Links on invoice send, deterministic payment settlement against 1686 (BAS moved acquirer receivables 1580 -> 1686), payout booking with reverse-charge fees (6570 + 4535/4598 + 2645/2614), and a 15-minute sync cron. Non-deterministic events land as needs_review, never guessed at. Fully dark without STRIPE_CONNECT_CLIENT_ID: connect returns 503, the send hook and cron no-op, and the settings page shows 'Kommer snart' (hosted) until the Connect platform is verified. Self-hosted keeps the honest not-configured message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): add shared completeTaxDeadline and fix dead AGI deadline auto-complete generate-declaration.ts has updated non-existent columns (type/period/ status) since inception, so the arbetsgivardeklaration deadline was never auto-completed. Replace with a shared helper targeting the real schema (tax_deadline_type/tax_period/is_completed), also used by the kvittens crons and moms handlers in the follow-up commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rot-rut): import Skatteverket beslutsfil and record decisions on payout requests Parse the beslutsfil JSON from Skatteverkets rot/rut e-tjanst and record godkant belopp on the matching begaran: matched by stored skv_referensnummer first, then exact name among active undecided requests; arenden by fakturanummer then personnummer, exactly-one or the beslut errors (all-or-nothing). Never auto-settles: recording the beslut and booking the payout are separate acts. Exposed as an API route and the gnubok_import_rot_rut_beslut MCP tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skatteverket): system auth for background reads, one-click VAT submit, kvittens notifications Hybrid auth program: system CCG (org certificate) for background reads while personal BankID stays for interactive submissions, since SKV per-flow refresh tokens live 65 min and crons structurally cannot run on them. All system-auth code sits behind SKATTEVERKET_SYSTEM_AUTH_MODE (default off) with a stub transport until the Expisoft cert and CCG avtal land; auth resolution is centralized in resolve-auth.ts. Also in this change: - One-click VAT submit chaining kontrollera -> utkast -> las server-side with a stage discriminator; step-by-step buttons demoted to the overflow menu. - Kvittens crons (AGI + new VAT schedule) with email-only notifications, deduped in notification_log under the new skv_kvittens type. - Ombud grant probe + verification UI in the connect panel, and a dashboard promo card for unconnected companies. - skatteverket_company_connections table with pg-real coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): auto-settle AGI tax payment from skattekonto and surface SKV reconnect on the tax card The "Skatt att betala" card only cleared via the manual mark-paid button on the run detail page; the promised automatic flip from the Skattekonto sync was never implemented, so paid periods stayed red. - settleAgiTaxPayments: during every skattekonto sync, a booked "Arbetsgivardeklaration YYYYMM" debit row settles the matching agi_declarations.tax_paid_at, but only when the amount equals the declared total to the ore and the account is not in deficit (deterministic; drift or deficit falls back to manual). - Salary overview card: reconnect hint when the SKV token needs re-consent (link to /settings/tax, silent when the extension is off), plus an inline "Markera som betald" button reusing the existing endpoint and salary_payments strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add cloud backup scheduling and alerting features - Implement unit tests for scheduling logic in `schedule.test.ts`, covering various scenarios for determining if a backup schedule is due. - Create a new module `backup-alert.ts` to handle failure alerts for cloud backup auto-sync, including email notifications for reauthentication and repeated failures. - Introduce `schedule.ts` to manage scheduling logic, including handling local time zones and converting between local and UTC hours. - Add CSV report generation functions in `archive-csv.ts` for trial balance, income statement, balance sheet, and general ledger, ensuring compatibility with Swedish Excel formats. - Create a README generator for the archive structure in `archive-readme.ts`, providing clear documentation for users accessing backup files. - Implement tests for CSV report generation in `archive-csv.test.ts`, ensuring correct formatting and content. - Establish a full-archive coverage contract test in `full-archive-coverage.pg.test.ts` to ensure all company-scoped tables are properly classified for backup. * fix(stripe): correct invoice clearing reference and improve type safety in sync logic * fix(invoices): narrow accountingMethod before resolveInvoicePaymentSourceType settleInvoicePayment takes accountingMethod as a raw settings string, but resolveInvoicePaymentSourceType requires the 'accrual' | 'cash' union. Normalize at the call site (anything but 'cash' books as accrual), matching the existing useCashEntry semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address CodeRabbit review findings and nitpicks on PR #1004 Review findings: - backup settings redirect: always force view=export over incoming params - AGI/VAT kvittens crons: isolate best-effort post-submit calls, check the signed-state persist error, guard recovery calls in catch blocks so one company cannot abort the rest; surface grant_revoked in the run summary - kvittens notifications: atomic claim-first dedup with a partial unique index; map non-uuid reference keys to deterministic uuids - grant probe: record the actual 2xx status; mTLS transport: handle response-stream errors - stripe: amount-aware idempotency keys for payment links; emit stripe.disconnected on upstream revocations - ROT/RUT beslut import: mutate in-memory request state after apply, move item + header writes into an atomic apply_rot_rut_beslut RPC, add rot_rut_payout to JournalEntrySourceTypeSchema - migrations: use NOT VALID + VALIDATE CONSTRAINT for CHECK constraints on journal_entries, notification_log and rot_rut_payout_requests - cloud backup: hour_utc-only schedule updates clear stale hour_local Nitpicks: - stripe sync: enforce the cron time budget inside per-connection event processing with idempotent cursor progress; maybeSingle for settings; honest partial-customer DTO shared with the settlement boundary - shared applyPaymentLinkToInvoice helper for both invoice send routes, v1 docblock documents step 6b and PAYMENT_LINK_FAILED - settings panel: drop redundant decodeURIComponent - cloud backup: document worst-case archive memory headroom Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7d7f604e00 |
Add/stripe invoice link (#998)
* feat(supplier-invoices): show registered invoices under "Att betala" with inline approve Registered supplier invoices are already booked as debt (2440) but were hidden from the "Att betala" tab until approved, which confused users. The tab now shows registered invoices too, marked "Ej godkand" with a compact inline approve button. Approval remains the gate for payment, not visibility; status model and approve API untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reports): add date range filter to huvudbok (kontoanalys) Mounts the existing ReportDateRange control on /reports/huvudbok so the ledger can be narrowed to any date range within the fiscal year, matching Fortnox kontoanalys. Lines before the range roll into each account's opening balance so running balances stay correct at the range start; lines after the range are dropped. Applies to the XLSX export too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): add optional payment link on invoices (paste-link MVP) The user pastes a payment link created in their PSP dashboard (e.g. a Stripe Payment Link) onto an invoice. The recipient gets a "Betala online" button in the invoice email and a QR code + clickable link in the PDF payment box. No PSP integration server-side: this is the demand probe; a future Stripe Connect integration would auto-fill the same column. - invoices.payment_link_url (migration 20260709090000), https-only + 2048-char cap enforced in CreateInvoiceSchema; empty string normalises to undefined and build-invoice-write always writes a concrete value so clearing the field on a draft edit NULLs the column - editor field (real invoices only) with one-link-per-invoice hint; strings in sv+en (messages landed via e0e11066) - email button (customer.language, hidden for credit notes/proforma/ delivery notes, URL escaped for the href attribute) + URL in the plain-text part - PDF QR + link row following the Swish QR pattern; wired into send, download and preview routes - derived documents (credit note, proforma convert, recurring) do NOT copy the link: it encodes one amount for one specific invoice - MCP gnubok_create_invoice accepts payment_link_url (validated at staging and re-checked in the commit executor); v1 API exposes the column; tools/list token ceiling bumped 45K -> 45.5K (ledger entry in payload-size.bench.test.ts, headroom was <10 tokens) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): show oresavrundning on editor/form totals, supplier list and invoice email The rounding logic (getDisplayTotal) was correct but only applied on the PDF, invoice list/detail and review dialog. The invoice editor summary, the supplier invoice form totals and the supplier invoice list showed the raw ore total right next to the toggle, and the invoice email said "Att betala" with the unrounded invoice.total while the attached PDF showed the rounded amount (and the email also ignored the ROT/RUT deduction). Extract the PDF's Att betala block into getAmountToPay (lib/invoices/rounding.ts) and point PDF + email at it so they cannot drift; behavior-identical refactor for the PDF. Booked amounts stay ore-exact; display-only as designed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): adapt huvudbok date-range tests to the two-step entry-lines fetch The date-range tests (0969168f) mocked the old single-query shape with the parent entry embedded on each line; main's refactor (fetchEntryLines) queries journal_entries first and reattaches. Queue entry rows like the other tests so the merge of the two features is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): fetch full invoice projection in v1 send so ROT/RUT deduction and payment link reach the PDF and email The v1 send route's hand-rolled column list omitted deduction_total, deduction_personnummer_last4, payment_link_url and the item-level ROT/RUT fields, so invoices sent via the public API overstated 'Att betala' and dropped the deduction box. Reuse the shared INVOICE_FULL_COLUMNS/INVOICE_ITEM_FULL_COLUMNS so the send row can never drift from the GET shape again. Also harden the supplier-invoice inline approve: a thrown fetch left the button stuck spinning; failures now refetch the true server state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0ef5593388 |
refactor(reports): restructure declaration pages around the filing pipeline (#992)
* refactor(reports): restructure declaration pages around the filing pipeline Momsdeklaration (/reports/vat-declaration) becomes the four-step flow the user actually runs: kontrollera, granska, bokfoer, laemna in. - NEW VatChecksCard, mounted first and ungated: the local pre-flight checks and the RC-basis-gap worklist used to render inside SkatteverketPanel BELOW the filing CTAs, and vanished entirely for free-tier or not-connected users, exactly the manual filers who must not file a declaration the checks would have blocked. - The gap worklist scales: compact DataList rows (first 8 + visa alla), visible shared classification selects, per-row overrides, bulk "Korrigera alla" behind a confirm dialog with serial progress, and an in-page declaration refetch replacing "Ladda om sidan". The list outlives the aggregate RC_BASIS_MISSING check so remaining rows never vanish after the first fix. - Summary card: status Badge + font-display headline amount instead of a Badge carrying the number; sanctioned h3 section heads; Table primitive; NEW import block (rutor 50/60-62) and the utgaende sum now includes 60-62 so it matches ruta 49 arithmetic; drill-downs preserved. - SkatteverketPanel stops being an API console: three forward buttons (Validera, Spara utkast, Laas och signera), six lookup/recovery actions in an overflow menu with two-line descriptions, destructive confirms on radera/koppla bort, one truthful notice slot (not-found is info, never green), visible disabled-reasons instead of title attrs, signing link as a real anchor plus auto re-check of inlaemning on tab refocus, and per-period state reset so a Q1 signing link can never show Q4's kvittens. - VatCompositionChart deleted (decorative donut mixing in/out VAT); export menu keeps xlsx only, XML/PDF live in the Laemna in card. - Sibling declaration pages adopt the same grammar: PS gets shadcn selects, auto-fetch with stale-discard, refresh button, envelope-parse fix (message_sv never existed); NE/INK2 auto-fetch on fiscal-year change (kills stale-year data), shared keyboard-accessible DeclarationRutaRow (fixes the expense sign bug), whole-krona amounts matching filed SRU values, neutral info notes instead of bg-primary/10, Skeleton loading, accessible download errors. UI-only: no API, schema, or dependency changes. All strings hardcoded Swedish (statutory surface). Adversarially reviewed (19-agent pass); all 10 confirmed findings fixed in this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reports): address CodeRabbit review on #992 - formatWholeKronor truncates instead of rounds: NE/INK2 SRU generators drop oere with Math.trunc, and the UI must show the filed figures. - SkatteverketPanel disconnect surfaces non-ok responses instead of silently stopping the spinner. - VatChecksCard distinguishes a failed rc-basis-gaps fetch from a real zero-gap result: destructive note + retry instead of the benign 'Inga verifikationer hittades'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
650c7be5e1 |
fix(bookkeeping): revive counterparty template learning (dead since the multi-tenant refactor) (#989)
* fix(bookkeeping): revive counterparty template learning, dead since the multi-tenant refactor (#865) The learning half of counterparty templates has written nothing since 2026-03-30 (prod: 750 SIE imports, zero new templates). Two stacked bugs: - The multi-tenant refactor re-scoped categorization_templates to company_id and the lib stopped writing user_id, but user_id kept its NOT NULL: every insert failed with a null violation that supabase-js returns rather than throws, so nothing was ever logged. Migration 20260711100000 drops the NOT NULL and the dead user_id indexes. - Four of six learning call sites (both categorize routes, categorize-core, the MCP server) passed the auth user id as companyId, so even with the column fixed the writes would fail FK/RLS and corrections could never find the template they were correcting. Hardening while in here: - insertOrUpdateTemplate now checks every write result, logs failures, and returns whether a row was written; populateTemplatesFromSieVouchers reports only templates actually persisted. - Sign-mismatched matches (an incoming refund matching an expense-learned template) previously booked backwards: debit expense / credit bank for money coming IN. They are now mirrored into the correct refund shape (VAT leg reversed for deductible input VAT), flagged requires_review, and excluded from template/rule learning so a refund can never flip a learned template. - Template amounts are computed from the SEK-resolved amount, so foreign-currency transactions no longer produce unbalanced multi-line entries (or VAT computed on foreign units). - SIE extraction no longer hardcodes 25% for 2641 (rate-agnostic in BAS): the rate is inferred from voucher amounts and snapped to 25/12/6%, and reverse-charge counterparties learn vat_treatment='reverse_charge' instead of losing the RC legs (which also no longer poison the ratio base). - New pg-real test locks the exact insert column set against the real schema, so a schema/code drift like this can't ship green again. Closes #865 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): mirror fiktiv-moms legs on RC credit notes, exclude import VAT accounts from ratio base Compliance-review follow-ups on #989: - REVERSE_CHARGE_VAT_ACCOUNTS gains the import output-VAT accounts (2615/2625/2635), which pair with 2645 in import vouchers exactly like the RC pairs and must not shrink the business ratio base. - A sign-mismatched match against a reverse_charge template (an RC supplier's credit note) now mirrors both fiktiv legs (credit 2645 / debit 2614) instead of booking gross, so Ruta 30/48 net back to zero. The income line-builder nets VAT credits against debit legs to keep the mirrored pair balance-neutral (identical result for all existing credit-only output-VAT paths). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(types): CategorizationTemplate.user_id is nullable since 20260711100000 (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): use roundOre for the VAT netting, keep the ore-round ratchet at baseline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): review-gate stale 12% templates across the livsmedel transition, pattern-aware direction guard Compliance-review round 2 on #989: - Livsmedel VAT dropped 12% -> 6% on 2026-04-01 (Prop. 2025/26:55) while restaurang/hotell stay at 12%. A reduced_12 template whose last_seen_date predates the transition can no longer be trusted unreviewed: its match is flagged requires_review until a post-transition approval refreshes it (re-approval keeps 12%, a correction relearns 6%). Actively-confirmed 12% counterparties flow without friction. - The opposite-direction correction guard now falls back to the line pattern's business sides when the legacy fields are both settlement-ish and cannot classify a multi-line template. - Documented the accepted import-RC mirroring limitation (2614 vs 2615 ruta attribution) and the netted-vatCredit precondition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3a2c57a167 |
feat(billing): paywall conversion pass (deferred first charge, trial touchpoint, sell-view upgrade) (#991)
* feat(billing): paywall conversion pass: deferred first charge, trial touchpoint, sell-view upgrade - checkout passes subscription_data.trial_end (trial grant expiry, 49h floor) so a mid-trial upgrade costs 0 kr today instead of double-billing days the company already has free; billing/status counts 'trialing' as paying - trial countdown pill in the sidebar (CompanyContext.trialEndsAt via getCompanyEntitlements); hidden for sandbox, dev bypass, and once any non-trial grant is active - sell view: what-happens-when timeline, free-vs-paid comparison table, risk-reversal copy + chevron CTA, post-checkout confirmation state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(billing): review triage: fail-closed trial lookup, hourly countdown refresh, BFL retention note - checkout returns 500 (no Stripe session) when the trial-grant lookup errors, instead of silently charging immediately after the UI promised 0 kr idag - sidebar trial countdown recomputes hourly so a long-lived tab stays honest - sell-view retention copy states BFL 7-year retention explicitly (compliance-bot suggestion) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: retrigger CI (pull_request event delivery stuck) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d91ee0168 |
fix(vat): keep the momsrapport intact after a manual nollställning (#990)
* fix(vat): keep the momsrapport intact after a manual nollställning (#984) The momsrapport already excludes settlement verifikat tagged with source_type 'vat_settlement' (#983), but settlements booked any other way still zeroed every ruta the moment they were posted: manual momsomföringar booked before the tagged flow existed (the report in issue #984), SIE-imported settlements, and storno reversals of a settlement, which inverted the sign instead and silently doubled the rutor after an annullera. Exclude settlement entries by SHAPE as well: an entry with at least one line on a declaration account (ACCOUNT_RUTA) and at least one on a settlement net account (2650/1650) is bookkeeping about the declaration, not VAT-bearing activity, in both the web projection (fetchVatAccountTotals) and the MCP twin (computeVatReport). Opening-balance entries are exempt: carried-in 26xx balances are unsettled VAT that belongs in the next declaration. Shape-detected POSTED settlements now also gate the "Skapa verifikat" button through existing_entries, since the proposal re-clears the full period and booking it on top of a manual settlement would corrupt the 26xx balances. Stornos never gate, so annullera still re-enables booking. Fixes #984 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): stable id order for the paginated VAT-report line fetch (CodeRabbit) fetchAllRows pages with .range(); without a unique .order() rows can shift across page boundaries once a period exceeds 1000 lines, skipping or double-counting journal lines in the rutor. Same discipline as the web projection (fetch-all.ts). Pre-existing, but the query was already being touched for #984. Also documents the shape-rule triage from the compliance-bot review in DECISIONS.md: compound business-VAT-plus-2650 verifikat stay a known accepted residual (a direction guard would break the storno exclusion), and the opening-balance concern is false for app flows (SIE import and set_opening_balances both tag source_type 'opening_balance'). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2774e01258 |
feat(vat): book the momsrapport as an editable settlement verifikat (#980) (#983)
* feat(vat): book the momsrapport as an editable settlement verifikat (#980) Adds a "Bokfor momsrapporten" card under the VAT declaration that builds an editable verifikat proposal from the report and books it through the ordinary journal entry form: - lib/reports/vat-settlement.ts: proposal builder. Clears each 26xx account at exact ore, books the net on 2650 (att betala) or 1650 (att aterfa) at the filed whole-krona amount (buildFiledAmounts, oretal faller bort per SFL 22 kap 1 par), balances the gap on 3740. Surfaces existing vat_settlement entries in the period so the UI can warn before a double booking. - GET /api/reports/vat-declaration/settlement-proposal: same period params as the sibling report routes. - VatBookingCard (reports view): fetches the proposal, warns when the period already has a posted settlement or draft, and opens the JournalEntryForm (bare, prefilled, source_type vat_settlement) in a dialog so every line is editable before committing. Booking uses the existing engine path: balance validation, period locks, voucher series per source type. - vat_settlement entries are excluded from the declaration projection (calculateVatDeclaration via new shared fetchVatAccountTotals, and the MCP computeVatReport for parity): a pure-projection report would otherwise read zero, and a later Skatteverket submission would file zeros, the moment the settlement is booked. No migration needed: the vat_settlement source type shipped in 20260708100000. Closes #980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vat): block re-booking a settled period, fail loud on lookup errors (CodeRabbit) The proposal is not delta-aware (it re-clears the FULL period), so a second booking while a posted settlement exists would corrupt the 26xx balances: disable "Skapa verifikat" until that verifikat is annulled (storno restores the balances). And since the existing-settlement lookup now gates that button, a swallowed query error would silently re-enable it: throw instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da859d7236 |
refactor(ui): design-system consistency pass over dense pages + i18n de-bloat (#961)
* refactor(ui): normalize dense pages to the locked design system Sweep of the info-dense surfaces against .claude/rules/design.md; no behavior changes, classNames and primitive adoption only. - Replace hand-rolled h1s with PageHeader (import, suppliers, kpi, salary/employees, skattekonto, settings layout) and drop the one double title (SalarySettingsContent under the settings h1) - Replace hand-rolled empty states with EmptyState (skattekonto, banking/api-keys/oauth/counterparty settings) and hand-rolled pulse divs with Skeleton (deadlines, report view loaders) - Remove semantic colors used as chrome: amber/emerald banners in AGIPanel and SkatteverketPanel, success/warning tints in kassaflodesanalys, arsredovisning and import become neutral surfaces with the tint kept on the icon only - Full-opacity borders everywhere (border-border/30-60, border-destructive/20-40, border-foreground/30, text-destructive/80) - Snap off-scale spacing (p-5 to p-6, p-2.5 to p-3, gap/mt-x.5 to scale values); KPI metric tiles p-6 to p-4 per the tile rule - Remove the mobile Select that duplicated the invoices status Tabs (TabsList already scrolls horizontally); single Tabs now serves both breakpoints - supplier-invoices: shared formatCurrency instead of a local formatAmount helper; skattekonto: formatDate/formatDateLong/ formatDateTime instead of raw dates and toLocaleString - arsredovisning flerarsoversikt converted to the Table primitive with right-aligned tabular-nums cells - Settings: CardTitle text-base on section cards, one heading idiom in AccountSettingsContent, h3 to h2 in CompanyProfileView Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(i18n): trim text bloat and fix an untranslated sv string - Fix invoice_credit.create_failed_fallback: sv catalog carried the English "Failed to create credit note"; now "Kunde inte skapa kreditfaktura". Translate new_user_checklist.step3_title in en - Drop descriptions that paraphrase their own title (design.md forbidden pattern): invoice_detail.credited_description, invoice_credit.original_card_description, invoice_editor customer/notes card descriptions (keys deleted from both catalogs, zero remaining usages); the transaction booking DialogDescription becomes sr-only so screen readers keep it - Trim redundant sentences from settings_salary.info_payroll_scope, settings_backup.intro, ext_cloud_backup_long_description, settings.name_description, salary_payments.open_payments_note and shorten invoice_credit.reason_card_description; statutory BFL/tax prose untouched - Normalize toast punctuation (dimensions/self_billing created_description lose the trailing period like their siblings) - common.delete "Radera" to "Ta bort" (zero live call sites; Radera stays reserved for irreversible account/company deletion) Catalogs verified key-identical (4795 keys each) and JSON-parseable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c9d9c5fe99 |
fix(billing): block demo/sandbox accounts from Stripe checkout (#948)
* fix(billing): block demo/sandbox accounts from Stripe checkout An anonymous demo user on a sandbox company reached POST /api/billing/checkout and created a live Stripe customer. Neither the checkout nor the portal route checked is_anonymous or is_sandbox, and withRouteContext lets anonymous users through (they are authenticated, just anonymously). Guard both routes on both conditions before any Stripe call: refuse anonymous users (identity truth, cheap in-memory check) and sandbox companies (matches the existing lib/sandbox/guard.ts "never charge a token" doctrine). Surface isDemo on GET /api/billing/status so the client hides the upgrade CTA instead of showing a button that 403s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(billing): redact tenant/customer IDs from incident note (CodeRabbit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |