281cb3989dd1e0fca29c2887df2fceccdd0e2d6c
897 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
281cb3989d |
fix(build): break Turbopack chunk-name hash collision from #1385's import edge (#1394)
Since the #1385 squash-merge every production build failed with 'Two or
more assets with different content were emitted to the same output path'
on [root-of-the-server]__1ge0sz5._.js: two distinct server chunk groups
(an AWS smithy helper chunk and the withRouteContext auth chunk) hash to
the same chunk name. The graph change that tipped the chunk layout into
the colliding state was entity-mapper.ts (arcim-migration extension
entry graph) importing lib/vat/supplier-invoice-line-checks, which
drags lib/money into the extension root.
Move normalizeVatRateToFraction into an import-free leaf module
(lib/vat/vat-rate-unit.ts), re-export it from
supplier-invoice-line-checks for all existing callers, and point
entity-mapper at the leaf. Behavior is unchanged (511 targeted tests
pass); the server chunk graph returns to the pre-#1385 shape that
builds cleanly. Verified: npm run build fails on
|
||
|
|
ff864ad3db |
feat(packs): sync system templates from packs/ instead of the frozen migration (#1390)
Phase 2b. The packs become the source of truth; the table stays the query surface, so the existing read path (one RLS query returning system + company + team templates) and every template id are untouched. pack_slug is the stable upsert key (migration 20260803230000, backfilled onto all 26 seeded rows). Matching on name instead would have meant that correcting a Swedish label looks like a new template, and because booking_template_usage.template_id is ON DELETE CASCADE, retiring the old row would silently wipe every company's "recently used" history for it. For the same reason a removed pack DEACTIVATES its row rather than deleting it: the read path already filters is_active, so it leaves the picker while usage history survives. The sync fails closed. A pack that will not parse or validate aborts the whole run with zero writes, because a database left in a state no commit of the repo describes is worse than a stale one. An empty catalogue is treated as a broken deploy (packs/ not bundled) rather than an instruction to retire every system template. Runs as a daily cron rather than at boot: boot-time work would have every serverless instance racing to write the same rows, and would re-apply a bad catalogue continuously instead of once a day where it is visible. Idempotent, so a database already matching the packs performs zero writes. Three database guards, each covered by tests/pg/booking-template-pack-slug.pg.test.ts: a partial unique index (one pack, one template), a format CHECK mirroring PACK_SLUG_RE so the database refuses what the loader would, and a CHECK keeping pack_slug off company templates, where it would shadow the pack it collides with. Verified the upgrade path locally the way the pg-upgrade job will run it: base schema, seeded fixture company with posted verifikat, an existing company template, then this migration alone. 26 slugs backfilled, company template intact, upgrade assertions pass. This is that job's first real migration. Payloads are spelled out rather than spread so the phantom-column guard can check them, and docker/crontab.* are regenerated for the new vercel.json entry. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0ef3c03904 |
fix(salary): book net deductions as settlement lines so salary entries balance (#1374)
* fix(salary): book net deductions as settlement lines so salary entries balance Net deduction line items were skipped entirely in createSalaryEntry, so the credit side (2710 tax + 1930 net) fell short of the gross debit by exactly the deducted amount and the balance trigger rejected the voucher. Net deductions now book on their mapped settlement account (1613 advance repayment, 2794 union fee, 7385 benefit co-payment, 2799 other; explicit account_number overrides), aggregated and undimensioned like the other settlement legs. The default mapping in account-mapping.ts moves off 7210 so payslip lines, the booking preview and the voucher all agree. Fixes #316 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): address compliance review on net-deduction accounts Label 7385 with its BAS 2026 name (Kostnader för fri bil) instead of the benefit-generic Bilförmån, document why the single benefit-payment item type defaults to 7385 with per-line override for other benefit kinds, and add a repayment-direction test (positive net deduction books as debit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8443062b1f |
fix(vat): enforce fraction unit for supplier invoice vat_rate writes (#1385)
Closes the remaining #310 write paths: credit-note item copies (web, v1, pending-operations) and arcim-migration supplier imports now normalize vat_rate to the decimal-fraction unit before storage, and a NOT VALID CHECK constraint guards every new supplier_invoice_items row. Customer invoice items deliberately stay percent; legacy supplier rows are left untouched so posted-entry reversals reuse the exact original values. Fixes #310 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b9605d8e9 |
fix(packs): repair the four broken system templates the validator found (#1388)
Phase 2a quarantined four defects rather than guessing at Swedish accounting. Each is now resolved against a domain source. KNOWN_BROKEN is empty. Löneutbetalning could never post. It debited 2710 @0.3 + 2920 @0.12 + 7010 @1.0 against a single 1.0 credit, totalling 1.42x the amount, so the balance trigger would reject every entry built from it. Rebuilt per the swedish-payroll skill: Debit 7010 gross, Credit 2710 tax, Credit 1930 net. The 2920 semesterlöneskuld line is gone because vacation accrual is its own verifikat (7290/2920), and a legal_note now says the 30% split is schablon and must be adjusted to the actual skatteavdrag. Periodiseringsfond avsättning/återföring referenced account 2113. Per swedish-year-end-closing the year-tagged block is 2120-2129 (2126 = tax year 2026), so 2113 was the fund for tax year 2013: long since reversed and absent from BAS 2026. Both now use 2110 Periodiseringsfonder, which does not rot annually, with a legal_note pointing at the year-tagged accounts for a company that tracks funds per year. Preliminär F-skatt (EF) turned out to be RIGHT, and the reference was wrong. Account 2012 "Avräkning för skatter och avgifter" was simply missing from lib/bookkeeping/bas-data (the file jumps 2011 -> 2013), while the swedish-year-end-closing skill uses it in two places as an enskild firma equity sub-account. That is not cosmetic: account-backfill.ts only seeds accounts present in BAS_REFERENCE, so any entry touching 2012 failed with AccountsNotInChartError. Added it with the equity SRU code its siblings share, and a description separating it from 1630, which carries a confusingly similar name on the asset side. The port test now distinguishes deliberate divergence from accidental drift: a pack not listed in INTENTIONAL_DIVERGENCES must still match the seeded JSONB exactly, and a listed pack must actually differ, so neither an unnoticed edit nor a stale entry can survive. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8299ee9fb4 |
fix(bookkeeping): resolve settlement account in all categorization flows and ship mis-booking audit (#1383)
Completes the #985/#986/#987 caller sweep: categorize-core, v1 batch-categorize, pending-operation edits and the MCP categorize path now resolve the settlement leg from the transaction's cash account instead of inheriting a hardcoded or stale account. Extends the correct_entry preview with currency, tax and dimension line metadata so staged corrections preserve full line fidelity. Adds a read-only audit query and a runbook for reviewing and correcting historical mis-bookings via staged storno with explicit approval; no automated bulk mutation. Fixes #1001 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e40dc64485 |
feat(invoices): invoice list column sorting and row-level status styling (#1375)
* feat(invoices): add column sorting and row-level status styling to invoice list Client-side sorting on number, customer, due date, amount and status with Swedish collation, null-last ordering and stable date/id tie-breaks. Status chips move to the shared RowStatus descriptor so normal states stay muted and exceptions carry semantic color. Invoice fetch now pages past the PostgREST 1000-row cap via fetchAllRows so sorting covers the full list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(invoices): make rounded-amount sort assertions non-degenerate Distinct rounded totals now prove the comparator orders by displayed value; the rounded tie case is kept as an explicit tie-breaker test since integer rounding is monotonic and can only create ties, never reorder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2f7132c94 |
fix(year-end): conservative historical repair for carried-forward 2099 (#1373)
* fix(year-end): conservative historical repair for carried-forward 2099 The steady-state year-end flow already reclassifies the opening 2099 (Arets resultat) to 2098 (Foregaende ars resultat) right after the opening balance is generated. Periods opened before that fix still carry the prior year's result on 2099. Add lib/core/bookkeeping/result-appropriation-repair.ts: a pure classifier plus assess/post helpers that auto-post the 2099 -> 2098 transfer only when it is unambiguous (open unlocked aktiebolag period, posted explicit opening_balance entry, active 2099/2098 accounts, no posted result_appropriation yet, current posted 2099 still equal to the explicit opening amount, and no other entry touching 2099). Everything else is skipped or listed for manual review; nothing is reconstructed from cumulative history. All writes go through the bookkeeping engine. Rework scripts/repair-result-appropriation.ts into a thin CLI over the library: global/company/period dry-runs, and commit mode that requires one exact --company-id, --period-id, and --user-id and re-assesses immediately before posting. Fixes #735 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(year-end): require company membership for repair attribution Compliance review (ASVS V8.2.1): commit mode accepted any --user-id and attributed the posted journal entry to it unvalidated. The service-role client bypasses RLS, so nothing downstream would catch an outsider uuid. postHistoricalResultRepair now verifies a company_members row for the target company before posting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(year-end): reference the opening-balance underlag on the repair verifikat Swedish compliance review (BFL 5 kap 6-7 §§): the historical repair entry validated against a specific opening-balance entry but never recorded it. Link it machine-readably via source_id and human-readably in the entry note ("Underlag: ingående balans, verifikat A1 (<id>)"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(year-end): harden repair CLI arg parsing, pagination and exit code CodeRabbit review on #1373: - arg() rejects flag-shaped or missing values instead of silently consuming the next flag as an id - global company and period scans paginate via fetchAllRows() so deployments past the PostgREST 1000-row cap are fully covered - exit code is non-zero when any period failed to list, assess or post 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> |
||
|
|
5d7952a01e |
feat(mcp): model-free document upload via signed URL (#1378)
* feat(mcp): model-free document upload via signed URL (#748) Adds gnubok_create_document_upload + gnubok_complete_document_upload so document bytes reach storage through a short-lived signed PUT URL and never pass through the model context. Fixes silent base64 corruption on real-size PDFs and the context blowup on batch uploads. - pending/ staage keys with TTL cleanup; completion validates magic bytes + SHA-256, moves bytes to the WORM key and adopts the reserved UUID as document id, making retries and concurrent completions idempotent - legacy gnubok_upload_document kept for clients without file access, description now points to the signed-URL pair; shared mime resolution and inbox-item creation extracted - both new tools mapped in TOOL_SCOPE_MAP (transactions:write) and MCP_TOOL_CAPABILITY_MAP (ai) so the paywall and scope gates hold - payload guard ceiling 58.5K to 59K after trimming the create tool's outputSchema to upload_id/upload_url/expires_at Fixes #748 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): satisfy capability-map lock and phantom-column scanner The exact-entries lock in capability-maps.test.ts now includes the signed-URL pair as dispatch-only AI tools, and the inbox insert uses a literal payload (explicit UUID instead of a conditional spread) so the no-phantom-columns scanner can resolve every column. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00d4c8a49e |
feat(invoices): ROT/RUT payout file dialog and file guards (#1380)
* feat(invoices): ROT/RUT payout file dialog and file guards Rebuild the UI for the existing headless HUS V6 payout-file flow (demanded via #789): a dialog on the invoices page to pick eligible paid ROT/RUT invoices, generate the XML, download it and track request status. Adds file-level guards from the Skatteverket spec: future payment dates blocked, one file per payment year, max 100 cases per file, with per-invoice blocker messages. Submission stays manual (upload + sign in the SKV e-service); no direct submission API exists. Fixes #789 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): compute rot-rut gating date in Europe/Stockholm The candidate and begäran date defaults used the UTC calendar day, which near midnight Swedish time could wrongly block or admit an invoice via FUTURE_PAYMENT_DATE and shift the 31 January deadline warning. Use getSwedishLocalDate() like the bookkeeping engine. Raised by the Swedish compliance review on PR #1380. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df34cae9bf |
feat(packs): konteringspaket as validated data files (phase 2a) (#1386)
* feat(packs): konteringspaket as validated data files, ported losslessly The 26 system booking templates lived inside migration 20260413160000. Under the never-modify-a-shipped-migration rule that froze them: correcting a wrong BAS account or a Swedish typo needed a whole new migration, and nothing checked that a seeded account existed in the chart or that a template balanced. #1321 was exactly that failure with seeded chart names. They are now one YAML file per pattern under packs/, with a Zod contract and a CI gate. A correction becomes a one-line edit plus a green run. The port is proven lossless, not asserted. The test fixture was read out of a Postgres with all 548 migrations applied, so it is the exact JSONB production holds; lib/packs/__tests__/port-is-lossless.test.ts asserts the YAML reproduces it by value. Phase 2b can swap the seeded rows for the loader as a no-op. The gate checks what makes a pack CORRECT, not just well-formed, because #1321 was structurally valid and still wrong: every account must exist in BAS 2026, and every pack must balance at five probe amounts through the real applyTemplate() rather than a reimplementation. Account numbers validate through lib/invariants, so a pack cannot disagree with the API or the SIE importer about what an account number is. Doing that immediately found four pre-existing breakages in the shipped templates: loneutbetalning debits total 1.42x the amount against a 1.0 credit: it can never post periodiseringsfond-avsattning-ab account 2113 is not in BAS 2026 and is not periodiseringsfond-aterforing-ab seeded into any company chart preliminar-f-skatt-ef account 2012, same problem These are quarantined in KNOWN_BROKEN, not fixed and not hidden: a quarantined pack's findings are warnings, any NEW finding fails the build, and the validator fails if a quarantined pack turns out to be clean, so the list may only shrink. Each is a Swedish accounting content change to a user-facing template, which deserves its own review rather than riding along inside a file-format change. Five shipped descriptions contain em dashes, preserved verbatim and pinned by a test: a lossless port must not silently rewrite user-visible strings. js-yaml is promoted from a transitive dependency to a declared one (MIT, already in node_modules), so the catalogue does not depend on it by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deps): regenerate package-lock.json with npm 10 to match CI `npm ci` failed on every job with "Missing: @swc/helpers@0.5.23 from lock file". The lockfile was written by local npm 11.6.0; CI runs npm 10.8.2 on node 20, and npm 11 emits a tree npm 10 reads as out of sync. Regenerated with `npx npm@10 install --package-lock-only`, which cuts the diff from a sprawling rewrite down to the three entries this branch actually adds (js-yaml, @types/js-yaml, and the @swc/helpers entry npm 11 had dropped). Verified with `npx npm@10 ci --dry-run`. This is the documented gotcha for this repo: regenerate lockfiles with npx npm@10, never with a local npm 11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c1888fcd5e |
feat(inbox): make custom-domain dialog ship-ready while gate stays off (#1377)
* feat(inbox): make custom-domain dialog ship-ready while gate stays off Polish the gated custom inbox-domain dialog from issue #814: full sv/en i18n via next-intl, admin/owner role gating on manage actions, explicit load-error state with retry, resilient clipboard copy with failure toast, and locale-aware verified-date formatting. The INBOX_CUSTOM_DOMAINS_ENABLED gate (product decision 2026-07-02) stays in place: routes still return 403 FEATURE_DISABLED when off and the workspace entry point remains removed. Enabling is a deliberate release decision. Refs #814 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(inbox): narrow useFormat locale to ErrorLocale for error messages The zero-extensions build failed type check: useFormat() returns a plain string locale while getErrorMessage expects ErrorLocale. Use the established repo idiom (cast once, reuse) as in DimensionsManager and AgentSkillsPanel, and point the useCallback deps at the derived value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cd7d7f52b9 |
feat(invoices): per-recipient email delivery outcomes (#1384)
* feat(invoices): per-recipient email delivery outcomes Resend delivery webhooks identify affected addresses in data.to, so one message with CC recipients can carry independent To/CC outcomes instead of masking the failing address into the aggregate reason text. - new apply_invoice_delivery_provider_event RPC merges each reported recipient onto its immutable To/CC position with the same rank and timestamp ordering as the aggregate status (retry and out-of-order safe) - recipient map is PII-free: keyed to:N / cc:N, BCC and unmatched recipients are never represented, and the map is cleared on PII redaction - delivery summaries, API route and MCP tool expose the sanitized map; the route re-sanitizes as defense in depth - UI shows a per-recipient status list under the aggregate outcome The prod ops check in issue #1350 (webhook registered in Resend and RESEND_DELIVERY_WEBHOOK_SECRET set in Vercel) cannot be verified from the repo and remains a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(invoices): commit provider event before cross-context read The BCC-leak test applied the event inside the rollback-scoped service role helper and then asserted through a separate member context, so the applied status was rolled back before the read. Use the committing runAsServiceRole helper for the apply, matching how the summary read is performed in its own context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bb1eddcccf |
fix(salary): exempt F-skatt compensation from arbetsgivaravgifter (#1372)
* fix(salary): exempt F-skatt compensation from arbetsgivaravgifter calculateAvgifterRate() never checked fSkattStatus, so F-skatt earners were charged the standard 31.42% employer contributions even though they pay their own egenavgifter. This overstated employer cost by ~31% and booked incorrect 7510/2731 entries. Add an early return in calculateAvgifterRate for f_skatt (rate 0, category exempt) before any age-based rules, and zero the avgifter basis in calculateSalary so salary reports and AGI totals do not carry a false contribution basis. The FK011/FK131 AGI rendering defect stays scoped to issue #315. Fixes #314 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(salary): assert full exempt return contract for F-skatt CodeRabbit review: calculateSalary does not read amount/basis from AvgifterCalculation, so the direct-return test must pin both to zero to catch a regression in the exempt early return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
24911abde0 |
feat(import): support Wise balance statements (#1368)
* feat(import): support Wise balance statements * fix(import): fail closed on ambiguous Wise rows * fix(import): guard Wise statement netted-fee assumption with running-balance continuity check Swedish accounting review asked whether balance-statement Total fees is netted into Amount. It is: Running Balance moves by exactly the signed Amount per row, so a separate fee row would double-count the cost. Codify the assumption with a pairwise continuity warning (order-agnostic, chain resets across skipped rows) and document the decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): cap bank-import validation payload and harden issue assertion CodeRabbit review: bound the VALIDATION_ERROR issues array to 20 entries with issue_count carrying the full total, so a large malformed file cannot balloon the response or log sink. Gate stays format-agnostic on purpose: error severity means do-not-ingest for every parser, and no non-Wise parser emits per-row errors alongside parsed transactions today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0510d4c13f |
fix(sync): stop expired trials from starving automatic bank and skattekonto sync (#1376)
The bank sync and skattekonto sync crons fetched the 50 oldest connection/token rows and only then checked entitlements per item, so expired-trial rows permanently occupied every batch slot and entitled companies were never synced automatically. Fetch all candidate rows, resolve capability grants in bulk via the new getCompanyIdsWithCapability() (company and firm grants cascade, expired grants excluded, explicit per-company disable wins), and apply the 50-item run cap after filtering. Entitlement query failures now fail the run instead of silently skipping every company. Fixes #563 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c9625fa45c |
fix(salary): book lonevaxling pension provision from frozen run snapshot (#1382)
The book flow never populated pension_contribution/pension_slp, so a gross_deduction_pension line item reduced the salary entry but the 7410/2740 pension provision and 7533/2514 SLP lines were never posted. Derive both at the createSalaryRunEntries boundary from the run's frozen calculation_params.slpRate snapshot so the dashboard, MCP and v1 booking paths all emit the pension verifikat, and reuse the exact 1.058 factor via calculateLoneVaxlingPensionProvision shared with the planning calculator. Fixes #317 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6782da3e9e |
feat(bokslut): calculate and book overavskrivningar (2150/8850) (#1379)
Add an automatic excess-depreciation calculator for machinery and equipment under IL 18 kap: 30-rule and 20-rule residuals (fiscal-period aware for short and long years), ledger vs asset-register reconciliation, fail-closed blocking states, and a signed proposal that books via the dispositions flow (8853/2153). Releases of an over-target reserve are mandatory and not overridable; increases are optional and capped server-side. Fixes #323 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
16fbcefbbc |
feat(invariants): shared format contracts + upgrade-path CI (#1364)
* feat(invariants): centralise shared format contracts, reconcile the org-number paths
The same format rules were written out independently across the codebase, and
where they disagreed the disagreement was invisible until a filing failed.
Worst case, now fixed: four Skatteverket- and Bolagsverket-bound export paths
each had their own idea of a valid organisationsnummer.
lib/skatteverket/format.ts strip '-' only threw on any input with a space
lib/salary/ku/ku10-generator.ts replace('-', '') first hyphen only, spaces survived
lib/salary/agi/xml-generator.ts strip non-digits stray letters passed the length check
lib/bokslut/ixbrl/validate /^\d{6}-?\d{4}$/ rejected the 12-digit form, no Luhn
A company stored with a space or in 12-digit form could file AGI all year and
then fail at the arsredovisning deadline with a message that did not say why.
lib/invariants/ now owns account number, ISO date, four-digit fiscal year and
org number, each with the rationale recorded next to the rule. normalizeOrgNumber
moves here from lib/company-lookup/ and isSaneDateString from lib/utils.ts; both
old paths re-export, so no caller changes. lib/api/schemas.ts builds its
primitives on the module, so ~100 schemas inherit any correction.
The arsredovisning check-digit verdict is a warn, not an error: a wrong Luhn
digit is almost certainly a typo worth surfacing, but whether every org number
Bolagsverket accepts satisfies Luhn is a Swedish domain question we have not
verified against a primary source, and an error there blocks Skicka in. We do
not block a statutory filing on an unverified assumption.
KU10 still passes a 12-digit stored org number through unfolded. That is
pre-existing, and whether the KU10 schema wants 10 or 12 digits is not covered
by the swedish-payroll skill, so it is pinned by a test rather than changed
silently.
Guard 8 (hand-rolled-invariant) tracks the remaining 114 inline copies as a
ratchet that may only go down, same mechanism as the roundOre guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(ci): add an upgrade-path job that applies new migrations against real data
The pg-real job applies all 548 migrations to an EMPTY database. Empty means
zero rows, so a migration that adds a NOT NULL, adds a CHECK, creates a unique
index or backfills passes trivially in CI and can still fail on production,
where the rows exist. CI proved that a fresh install works; nothing proved that
an existing install upgrades.
The new pg-upgrade job: apply the schema as it stands at the merge base, seed a
small real company (three posted verifikat, balanced lines, one ore-level
amount), then apply ONLY the migrations this PR adds, then assert the data
survived (entries still posted, lines intact, ledger still balances, ore
unchanged, voucher numbers sequential). A PR with no migration no-ops.
Verified locally against supabase/postgres:15.8.1.060 rather than assumed, with
three deliberately bad migrations:
rescale money on posted lines empty: would pass seeded: ERROR (immutability trigger)
CHECK violating the ore row empty: exit 0 seeded: exit 3
NOT NULL on a populated column empty: exit 0 seeded: exit 3
Base migrations are read out of the merge-base git tree, not the working tree,
so a PR that edits an already-shipped migration still gets the original applied
and the edit surfaces as a failure here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: record the invariants and upgrade-CI decisions
Two entries covering what this PR changes and, more importantly, the calls that
are not obvious from the diff: why the arsredovisning check-digit verdict is a
warning rather than an error, why KU10's 12-digit passthrough is pinned instead
of fixed, and why the ROT/RUT brf org-number schemas stay on their own rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(test): mark the upgrade fixture as CI-only, never a production template
The fixture writes posted journal_entries and their lines directly, bypassing
the engine and the atomic commit RPC. That is the only way to hand a migration
pre-existing posted rows to break, and it is safe against a throwaway CI
database, but it reads like a sanctioned pattern to anyone who finds it later.
Says so explicitly, with the reason it is confined here (no voucher sequence to
keep gapless, no retention obligation on a database destroyed with the job) and
a pointer back to Hard Rule 2 for anything touching a real database.
Raised by the Swedish compliance review bot on #1364.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ef237351b6 |
test(db): verify annual-report compliance stack (#1367)
Closes #1352 |
||
|
|
f10bac6023 |
fix(reports): block unsupported EUR annual reports (#1366)
Closes #1360 |
||
|
|
d9fb5da16d |
fix(reports): localize latest voucher label (#1365)
Closes #1267 |
||
|
|
9e54a8e400 |
fix: preserve invoice payment dates (#1332)
Signed-off-by: Emil <emilmattsson14@gmail.com> |
||
|
|
18cdba3574 |
fix: make out-of-order SIE opening balances atomic (#1334)
* fix: preserve SIE IB on out-of-order imports * fix: make SIE opening balance replacement atomic * test: seed accounts for atomic IB pg coverage * test: complete atomic IB pg fixtures * fix(import): avoid IB resync across fiscal-year gaps * test(import): mirror PostgREST date values in pg adapter * fix(import): address opening balance review feedback |
||
|
|
8a2498a987 |
fix(mcp): add true continuation to invoice list tools (#1327)
* fix(mcp): add true list continuation Signed-off-by: Emil <emilmattsson14@gmail.com> * test(mcp): cover pagination edge cases Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(mcp): validate pagination offsets Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(mcp): preserve continuation without counts Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> |
||
|
|
115147d304 |
docs(docker): improve NAS self-hosting compatibility (#1330)
* docs(docker): improve NAS self-hosting compatibility Signed-off-by: Emil <emilmattsson14@gmail.com> * docs(docker): retain optional resource controls Signed-off-by: Emil <emilmattsson14@gmail.com> * docs(docker): clarify compatibility prerequisites --------- Signed-off-by: Emil <emilmattsson14@gmail.com> |
||
|
|
bfbd926950 |
fix: paginate MCP inbox items (#1329)
* fix: paginate MCP inbox items * fix: validate MCP inbox cursors * test: reset MCP inbox pagination state |
||
|
|
bcf919d995 |
fix: expose inbox filenames via MCP (#1328)
* fix: expose inbox filenames via MCP * fix: handle joined inbox attachments |
||
|
|
fd376eff94 |
fix: audit Cloud Backup OAuth redirects (#1324)
* fix(cloud-backup): pin OAuth callback origin * fix: reject non-web cloud backup origins |
||
|
|
d684e3c440 |
feat: add theme palettes (#1326)
Add Neutral, Indigo, Forest, and Sand palettes independently of Light, Dark, and System. Persist and hydrate the selection, add the accessible settings picker, and include the validated review fixes for keyboard navigation and Swedish copy. |
||
|
|
f24e1076cf |
fix(templates): explain the Kostnad/Intäkt vs Betalning choice in the mall editor (#1325)
* fix(templates): explain the radtyp choice, and give the assistant product knowledge A user reported not knowing when to pick Kostnad vs Betalning on a template line, and that the in-app assistant could not answer either. UI, at the point of choice: - The editor dropdown said "Kostnad" while the template list said "Kostnad/Intäkt" for the same tag; both now use type_cost_revenue. - An InfoTooltip on the Rader label explains the three radtyper: what each one means, that the tag never changes which account is booked, and the exactly-one-of-each rule that makes a template pickable directly on a bank transaction. Assistant, structurally: a new always-active 'product' atom tier for knowledge about Accounted's own features, which no existing tier could hold (horizontal/vertical/modifier are accounting-domain and composer-owned). First atom: product/bokforingsmallar. The tier CHECK migration must precede the generated seed; product atoms are composer-invisible because every composer filter picks a specific tier. The general-help and settings-help intents now route feature questions to product atoms instead of guessing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): widen the remaining tier unions the product tier missed The zero-extensions build caught emitSkillLoaded pinning a literal copy of the old SkillTier union. The same sweep found two surfaces the type system could not flag: the mcp.skill_loaded event type, and the Vad din agent vet overview (agent-competence + CompetenceCard), which queries the registry with no tier filter and would have rendered product atoms as dormant. Product atoms are always active there too, labelled with a new tier_product string in both locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(agent): cover empty-slug 400 and curation-off 404; mention product knowledge in the panel help Review follow-ups: the 404 test only covered a missing row, not a row with mcp_exposed=false, and the empty-slug validation path was untested. The Kunskap panel's help popover still described the library as only Swedish accounting and tax knowledge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * revert(agent): pull assistant product knowledge back out; ship the UI clarification only Founder call 2026-07-31: the assistant gets no internal knowledge of Accounted's own features yet. This removes the product atom tier, the bokforingsmallar atom, both migrations, the discovery/MCP/panel wiring, and the intent routing added in the earlier commits; the working implementation stays in this branch's history (green at bb65224c) and is tracked in a follow-up issue. What ships: the unified Kostnad/Intäkt label and the radtyp InfoTooltip in the template editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4933fae7a9 |
fix(bookkeeping): correct seeded BAS account names that contradicted engine bookings (#1321)
- seed_chart_of_accounts named 7210 'Semesterlöner' while payroll books gross salaries there (BAS: 'Löner till tjänstemän'; vacation pay is 7285), so every seeded AB showed salary costs under a vacation-pay label in Nyckeltal and every other report - 3002 was named 'Försäljning varor 25%' although 3002 is the 12% revenue account everywhere else (invoice booking, category mapping, default_vat_rate seeding) - 7010 and 3001 get their BAS 2026 names; 2631 loses a double space - backfill renames existing rows only on exact seeded-literal match plus is_system_account, so accounts users renamed survive untouched - account-descriptions.ts had the 7010/7210 names swapped; client-account-names.ts labeled 2510 (Skatteskulder) as 'Personalskatt' Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f49dc3438d |
fix(sandbox): lock what the sandbox cannot actually do (#1318)
* fix(sandbox): lock what the sandbox cannot actually do Three surfaces in the sandbox advertised capability the sandbox blocks outright, or rendered a staged preview wrong. Skatteverket promo card: hidden for sandbox companies. The sandbox landing page tells users Skatteverket is off, and the authorize route 403s via guardSandbox, so the dashboard nudge was a dead end. Same precedent as TaxSettingsContent, which already hides its Skatteverket section on is_sandbox. Dokumentinkorg: locked with a state that says what the workspace does and sends the user to registration. Checked before the capability gate on purpose: the seed_trial trigger grants every new company (sandbox included) 30 days of every paid capability, so the existing paywall waved a demo company straight through. The CTA signs the anonymous session out first, mirroring SandboxBanner. Staged categorize_transaction preview: the seed wrote its kontering under the generic preview_lines key, but categorize_transaction is the one type with a dedicated preview component, and it reads `lines`. The card fell through to its legacy summary branch and rendered blank Debetkonto and Kreditkonto plus "NaN kr" from formatCurrency(undefined). The seeded blob now mirrors what gnubok_categorize_transaction stages, extracted into buildSandboxPendingOperations so both shapes are unit-testable. CategorizePreview also learns to read preview_lines and to show a missing amount as a gap, so a live 24h sandbox stops showing NaN before its data expires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sandbox): don't leave for /register when sign-out failed CodeRabbit review: the ExtensionSandboxLockState CTA ignored the signOut() result, so a failure routed to /register with the anonymous session still live, which registers INTO the sandbox instead of leaving it: exactly what the sign-out exists to prevent. Surface the failure and stay put so the user can retry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0f1c7c9365 |
fix(import): refuse a Bokio connection that opens a different company (#1315)
A Bokio integration token is scoped to one Bokio company and the company id is typed in by hand, so credentials for the user's other company imported that company's customers, suppliers and invoices with no error at all. Probe /companies/{id} before storing, mirroring the Bjorn Lunden /details probe, and refuse on a confident org-number mismatch.
Also surface the inbox mail body when nothing was attached: it was captured in email_body_text and never read back, which made Gmail's forwarding-confirmation mail unreadable and the forward impossible to complete.
|
||
|
|
27ae59040e |
fix(transactions): retire stale invoice match pointers when an invoice settles (#1313)
* fix(transactions): retire stale invoice match pointers when an invoice settles potential_invoice_id / potential_supplier_invoice_id are write-once import suggestions: nothing revisited them once written. With recurring same-amount invoices, an earlier suggestion pointed transaction A at invoice X, X was then paid off by transaction B, and A kept pointing at a fully paid invoice. The match dialog computed its amount diff against that invoice's 0 kr remaining_amount and reported a bogus partial payment, and the dead pointer also blocked a fresh suggestion: both re-suggestion scans require the column to be NULL. Add one shared helper, clearSettledInvoiceSuggestions(), that nulls a settled invoice's own suggestion column on every other transaction of the same company, scoped by company_id and by that invoice id only, never widening to the confirmed invoice_id / supplier_invoice_id links. It is best effort by construction: every caller has already booked a payment verifikat, so a failed cleanup logs and returns instead of failing the settle. Wired into every path where an invoice reaches paid through a payment: the dashboard and v1 match-invoice / match-supplier-invoice routes, the dashboard and v1 mark-paid routes, settleInvoicePayment, the batch allocation route (per fully settled allocation), linkInvoiceToVoucher and linkSupplierInvoiceToVoucher, linkTransactionToJournalEntry, and the MCP staged-operation executors for mark_invoice_paid and match_transaction_invoice. Partial payments are deliberately left alone: a partially paid invoice is still matchable. The v1 supplier match route also clears its own row's hint, which it was missing next to its dashboard twin. Read-time revalidation stays as the backstop for the paths not wired up here. countSuggestedMatches now delegates to listSuggestedMatches, which already revalidates candidates, so the worklist badge can no longer claim a number the list refuses to render. A data-only backfill migration retires the pointers already stranded in the database. It touches no journal entry, verifikat or period-locked data, is idempotent, and its status lists mirror lib/invoices/matchable-statuses.ts. Fixes #1259 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(transactions): wire the MCP batch allocation into the settled-pointer cleanup Review follow-up on the #1259 fix. commitMatchBatchAllocate calls the same match_batch_allocate RPC as the dashboard route, and gnubok_match_batch_allocate is a live staged MCP tool, so an agent settling a samlingsbetalning reproduced the issue exactly: the RPC nulls potential_invoice_id / potential_supplier_invoice_id only on the source transaction, leaving every other transaction of the company pointing at an invoice the batch just closed. The per-allocation loop moves into clearSettledBatchAllocationSuggestions() so the HTTP route and the MCP executor run the same code and cannot drift again, with a commit-path test pinning that only the fully settled allocation is retired. The enlarged badge scan is made safe. countSuggestedMatches now feeds up to 200 ids into listSuggestedMatches, past the 150 per .in() that countInboxDocuments already chunks for, so the candidate lookups are chunked at IN_CLAUSE_CHUNK too and their ids deduped. Both lookups now check .error: previously a 414, a 500 or an RLS change produced empty maps, an empty list and a zero badge with nothing logged. Every failure branch here logs companyId, matching the logAndZero convention. Also: restore the anchorSupplierInvoiceDocument doc comment above its own call in the dashboard supplier-invoice mark-paid route (the #1259 block had been inserted between them), and assert the transaction update payload in the v1 match-supplier-invoice test, which now covers the potential_supplier_invoice_id null that the route was missing next to its dashboard twin. Fixes #1259 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1a5d205bd6 |
fix(webhooks): derive the stuck-in_flight window from the cycle bound and charge the stall an attempt (#1311)
* fix(webhooks): derive the stuck-in_flight window from the cycle bound and charge the stall an attempt recoverStuckInFlight re-armed any in_flight row older than 2x REQUEST_TIMEOUT_MS (20 s), but a cron cycle claims 50 rows and attempts them serially, stamping updated_at once at claim time. From row 3 onward every row was past the threshold before its own attempt started, so each cycle recovered and re-claimed the rows the previous cycle was still working through: duplicate POSTs of the same X-Gnubok-Delivery, and a terminal status decided by a race whose loser was swallowed by enforce_webhook_delivery_immutability as a log.warn. Both halves of #1257 are fixed: 1. The window is derived, not guessed. The attempt loop is now bounded by an explicit CYCLE_BUDGET_MS (120 s) instead of relying on the platform to kill it, and the sweep window is that bound plus one receiver timeout plus slack (160 s), floored at the cron's own batch size so the 5-row emit kick cannot re-arm rows the 50-row cron still owns. Each row is also re-stamped immediately before its own attempt, so a row's in_flight age measures the attempt rather than the claim. The same write doubles as an ownership check: a zero-row result means another cycle took the row, and the POST is dropped instead of duplicated. 2. The sweep charges an attempt, so MAX_ATTEMPTS is a real cap again. The predicate moves into a SECURITY DEFINER RPC because PostgREST can express neither `attempts = attempts + 1` nor the conditional flip at the cap, and a read-then-write loop would reopen a TOCTOU against the immutability trigger. A row recovered past the cap lands on exactly the terminal state the normal retry path produces: status 'dead', attempts = MAX_ATTEMPTS, error prefixed 'attempts_exhausted'. The trigger is neither weakened nor bypassed: the outer UPDATE keeps status = 'in_flight' in its own WHERE, so a row that raced to a terminal status fails re-evaluation under READ COMMITTED and is skipped rather than aborting the statement. Rows the cycle claimed but will not reach are handed back as re-claimable instead of being stranded in in_flight, without charging an attempt they never made. Adds the partial index the sweep needs (idx_webhook_deliveries_due is partial on pending/failed and structurally excludes in_flight). No retention or pruning cron: webhook_deliveries still has no cleanup path, which is a separate decision and stays a follow-up. Fixes #1257 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(webhooks): back the cycle budget with maxDuration and give the stall the normal retry backoff Review follow-up on the #1257 fix. Two of the findings were blocking and compound each other: the fix made stranding likely and destructive at the same time. 1. The 160 s sweep window was derived from CYCLE_BUDGET_MS, but nothing granted a dispatch cycle 120 s: the cron route declared no maxDuration. If the platform killed the invocation before the budget check fired, releaseUnattempted never ran and the claimed-but- unattempted rows stayed in in_flight carrying their claim-time updated_at, which is exactly the invariant the window depends on. The route now declares maxDuration = 300, the way the stripe transactions and documents verify crons pair a budget with one, and a route test asserts both the literal and its relation to CYCLE_BUDGET_MS. The kick path can never be given a maxDuration (after() runs inside an arbitrary route), so dispatch-kick.ts now states why it does not need one: KICK_BATCH_SIZE x REQUEST_TIMEOUT_MS is 50 s, so the dispatcher's budget check never fires there. 2. The sweep charged an attempt but re-armed at p_now, i.e. no backoff, while the normal failure path waits RETRY_BACKOFF_SECONDS. A row that kept getting stranded (deploy, instance recycle, any cycle that outlives its invocation) was re-claimable on the next per-minute tick and could burn all 8 attempts in roughly 20 minutes, landing in the terminal, immutable 'dead' state without its receiver ever being contacted. Pre-fix that loop was infinite but harmless, so this was a net-new way to lose a delivery. recover_stuck_webhook_deliveries now takes p_backoff int[] (RETRY_BACKOFF_SECONDS, still single-sourced in TS) and sets next_attempt_at with the same clamped index lookup markFailedForRetry uses, so a stall costs an attempt AND the same wait a 500 costs. A non-positive or empty schedule is rejected rather than silently degrading to p_now. The migration has not been applied to any deployed environment, so it is amended in place rather than superseded; it drops the old 3-argument signature so no ambiguous overload can survive in a dev or CI database. Also from the review: - stuckInFlightAfterMs(batchSize) was dead code whose Math.min clamp made every input return 120_000, so the documented DEFAULT_BATCH_SIZE floor never fired and the test that pinned it (stuckInFlightAfterMs(5) === stuckInFlightAfterMs(50)) was a tautology. It is now the plain constant STUCK_IN_FLIGHT_AFTER_MS with a comment that credits the budget, and the test drives the window through dispatchDueDeliveries at batch sizes 5, 50 and 500, which fails if the window ever becomes batch-derived again. - The sweep's outcome reaches the operator: recovered / recoveredDead are on DispatchSummary and in the cron's structured log, so a tick that takes deliveries terminal is visible without grepping helper-level warn lines. - releaseUnattempted no longer writes 'failed' onto a never-attempted row. claim_due_webhook_deliveries does not return the pre-claim status, but it does return attempts, and every path that writes 'failed' also writes attempts >= 1, so attempts = 0 identifies a row that was 'pending' and it is restored as such. webhook_deliveries is customer-visible behandlingshistorik; a delivery that was claimed and handed back without a single POST must not read as a failure there. The two deferred hygiene items (no retention path for webhook_deliveries, and the sweep still being an unbounded tenant-global UPDATE) are reported as a comment on #1257 and noted in the migration. Fixes #1257 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f3bf50d862 |
fix(invoices): roll back the header row when a recurring-schedule item replace fails (#1312)
* fix(invoices): roll back the header row when a recurring-schedule item replace fails
PATCH /api/invoices/recurring/[id] and the update_recurring_schedule commit
executor wrote the schedule header first, then replaced the items. An item
insert failure restored the items snapshot but left the header update
committed, so a combined edit half-applied: a new day_of_month or
default_dimensions stayed while the line edit was undone.
Both write paths now go through one shared helper,
lib/invoices/apply-recurring-schedule-update.ts, which snapshots the header
before writing it (only for a combined edit, the only case with something to
undo) and compensates it on any items failure. The rollback update is filtered
on the updated_at stamp our own write produced, so a concurrent writer (the
hourly cron, a second edit) wins instead of being clobbered from a stale
snapshot: audit finding C2 in lib/invoices/voucher-matching.ts.
A compensation that itself fails is no longer swallowed. The helper reports
itemsRestored / headerRestored, logs the unrecoverable rows and the intended
restore payload, and both call sites then return the new
INVOICE_RECURRING_UPDATE_PARTIAL registry entry, which tells the user in
Swedish that the schedule may be half-saved and to check fields and items
before retrying. A clean rollback keeps the PG-mapped error so a CHECK
violation still surfaces its specific message.
Also in the rewritten block:
- the items DELETE error is checked, so a failed delete no longer proceeds to
an insert that would duplicate every line;
- the 404 existence check moved above every write, so a PATCH with items for a
missing or cross-tenant id writes nothing;
- the items snapshot uses select('*') with id/created_at stripped on restore
(same idiom as replaceInvoiceItems), so a column added later is carried
through instead of silently dropped;
- NewRecurringScheduleDialog unwraps the nested { error: { message } } envelope
the route returns, which otherwise reached the toast as "[object Object]".
The cron's no-empty-items invariant holds on every failure path: the items are
either untouched, restored, or the failure is reported explicitly.
Fixes #1275
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(invoices): never write when the compensating snapshot is unavailable
Follow-up on the recurring-schedule rollback: the helper still performed two
writes it already knew it could not compensate.
- The header snapshot read now checks its error and a missing row, and the
header UPDATE is skipped entirely when either holds, so no header change is
committed that we already know can never be rolled back.
- An unreadable item snapshot now aborts BEFORE the delete (rolling the header
back) instead of deleting first and reporting itemsRestored: false, so the
cron invariant "a schedule always has items" holds on every failure path.
- That header read now runs whenever items are replaced and is scoped by
company_id, so it doubles as the ownership proof the schedule_id-only item
delete/insert lacks (the commit executor runs with RLS off). Stated in the
JSDoc as well.
- The item snapshot is paginated via fetchAllRows: a schedule with more than
1000 lines could otherwise restore partially while reporting a clean
rollback.
- The executor now returns errorCode INVOICE_RECURRING_UPDATE_PARTIAL,
surfaced as CommitResult.code and persisted as result_data.error_code, so a
staged-op caller can detect the partial state without substring-matching the
Swedish sentence.
- Route: details keys are camelCase throughout, and an item failure is logged
once, with the repair context kept on the partial path only.
Tests: the unreadable-snapshot branches are exercised (including the
previously unused itemsSnapshotError harness hook), and the test that pinned
"header written with no possibility of rollback" now asserts that nothing is
written at all.
Fixes #1275
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4a38fa30ed |
fix(recon): share the cash-account scope and cover the no-1930 case (#1309)
PR #1295 (
|
||
|
|
9bb8b40420 |
fix(vat): veto the ruta 05 rate fallback on a contradicting account label (#1310)
* fix(vat): veto the ruta 05 rate fallback on a contradicting account label The read-path fallback for #1289 shipped in #1296: fetchDynamicRuta05Accounts drops the SQL sats filter and inferDomesticSalesRate resolves a NULL default_vat_rate from the 30x1/30x2/30x3 suffix plus a matching "25/12/6 % moms" label. That half is already on main and is not touched here. What it lacks is a veto. Both signals can agree while the rest of the label says the konto is not domestic taxable sales at all: "Forsaljning byggtjanster 25 % moms, omvand betalningsskyldighet" is ruta 41, a VMB konto is ruta 07, an export konto is ruta 36, a momsfri konto is ruta 42. Inferring 0.25 for any of them files the amount in ruta 05, which is a wrong box rather than a missing one. A contradicting term now stands the fallback down, so the konto keeps the behaviour it has today (omission) instead of being misfiled. An explicitly configured rate, including an explicit 0, never reaches this check and stays authoritative. Two existing rules are now pinned by tests as deliberate, since a looser resolver was proposed and rejected: the label must say the word "moms" after the percent ("Forsaljning konsult 25 %" is a rate of pay, not a sats), and a label naming two different sats resolves to nothing rather than to whichever it spells out first. Fixes #1289 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(vat): anchor the 0 % veto so it stops matching 10/20/30/100 % The CONTRADICTING_ACCOUNT_NAME alternative `0\s*%` had no left word boundary, so it also matched the trailing zero of any percentage ending in zero. An ordinary domestic sales konto named "Forsaljning varor 25 % moms, rabatt 30 %" was vetoed: agreeing 30x1 suffix, agreeing "25 % moms" label, none of the veto's documented cases (momsfri, omvand, VMB, export) applying. It dropped out of the ledger fetch, ruta 05 came out short, and runVatDeclarationChecks raised a blocking OUTPUT_VAT_WITHOUT_SALES_BASE: the exact #1261 symptom this module exists to remove, re-created by the veto. `\b0\s*%` keeps a genuine "0 % moms" label vetoing and stops matching inside a longer number. The comment now records why the boundaries differ per alternative, since the asymmetry is deliberate: "vmb" needs both, "export" and "utanfor" are bare substrings so Swedish compounds are caught too. Both directions are pinned by tests that fail without the anchor. Fixes #1289 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fa394e3759 |
fix(skattekonto): look-alike beslut rows, the list-to-voucher round trip, makulerad rendering, huvudbok discoverability (#1297)
Four fixes from the exit mail Anders Orback (Center Node AB) sent hours after churning. His five points were mostly one job: reconciling skattekontot against banken before årsredovisningen. Skattekonto look-alike rows. Skatteverket splits a retroactive omprövningsbeslut across every month it re-charges and sends one transaction per month, sharing date, text and amount; only ranteberakningsdatum separates them, and we stored it but rendered it nowhere. A real company posted 15 such vouchers (67 785 kr across Feb 2025-Apr 2026) unable to tell them from duplicates of the automatic hämtning. Surface the field when it carries information: its month differs from the Datum column, or another row in the same band is otherwise indistinguishable. The list-to-voucher round trip. The verifikat list collapsed to a skeleton on every refetch and sprang back, moving rows under the pointer; only the first load shows a skeleton now. Filter state is React-only, so leaving the list loses it: add a hover-revealed open-in-new-tab affordance on the voucher list and the skattekonto page, where the link had been behind a hand-rolled opacity-0 that coarse pointers never trigger. Makulerad rendering. A stornoed verifikat now reads as struck out, per data cell rather than on the row, because text-decoration propagates and a child cannot opt out. Vouchers-per-account discoverability. /reports/huvudbok?account=1930 already existed; the palette matcher requires every token and the entry never contained the word "verifikat". Add ReportDescriptor.searchTerms plus a report-library search box. Also fixes a false "Saknar underlag" compliance chip that flashed before attachment counts resolved, and a keyboard-access regression where HOVER_REVEAL_CLASS carried focus-visible only, hiding controls inside a non-focusable wrapper from keyboard users. No migration. No write paths, storno paths or posted entries touched. Follow-ups filed: #1300 #1301 #1302 #1303 #1304 #1305 #1306 #1307 #1308. Open decision: #1305 (Omförd vs Makulerad). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
144cc51458 |
fix(mcp): scope VAT close reconciliation account (#1295)
Resolve the VAT close reconciliation scope using the selected cash account, currency, and unassigned-transaction behavior. Add regression coverage for cross-account leakage and fail closed on lookup errors.\n\nCloses #1290 |
||
|
|
6318501b71 | fix(vat): recover ruta 05 for null-rate custom accounts (#1296) | ||
|
|
392e847c1e |
fix(transactions): block invalid invoice match targets (#1294)
Classify customer and supplier invoice targets as matchable, settled, or otherwise not open. Block invalid targets with localized guidance while retaining the valid partial-payment flow and add focused regression coverage. Fixes #1260 |
||
|
|
17a7a62ceb |
fix(reports): stop the resultatavslut zeroing declarations, and make the mistake uninventable (#1293)
* fix(settings): explain why account deletion is blocked The delete-account button was disabled while the user still owned companies, but the reason only lived behind the "?" on the blocker row, so the greyed-out button read as broken. Surface it as one visible attn sentence directly under the button, and point aria-describedby at it whenever the button is disabled, not only on a load error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(enable-banking): share one PSD2 consent across a user's companies Connecting the same bank for a second company required a second BankID, and at SEB that new authorization silently revoked the first one. A user with four companies at one bank therefore signed four times a quarter and ended up with three dead feeds, each still rendering as "Aktiv" with a stale last_synced_at until someone pressed Synka. Prod says this is not one customer: every SEB customer holding connections in more than one company has had an earlier company stop syncing at the moment the next was authorized, most of them while the consent was still formally valid for weeks. The same measurement over other banks is far quieter, so the one-active-session-per-PSU limit is real and ASPSP-side. Enable Banking already supports the shape we want. POST /auth carries no account restriction, so a session covers every account the user ticked at the bank, and GET /accounts/{uid}/transactions takes no session id, so a second company can sync its own accounts from an existing session. bank_connections has no unique constraint on session_id, so this needs no migration. Adds lib/session-sharing.ts plus GET /reusable-sessions and POST /attach. When a live session in another of the user's companies still exposes accounts no company syncs, the settings panel offers to reuse it: the new row shares session_id and consent_expires, carries only the unclaimed accounts, and lands in pending_selection so the existing IBAN-aware account picker does the ledger mapping. Only the consent is shared; accounts, cash_accounts and transactions stay strictly per-company. Sharing a session changes three lifecycle paths, all handled here: - Disconnect and reconnect now refcount before revoking. A blind revoke would take down a sibling company's feed, which is the exact failure this removes. The count runs on a service-role client because RLS hides a sibling in a company the user has since left, and it fails closed: an uncertain count is treated as shared, since a lingering consent lapses on its own in 90 days while a wrongly revoked one kills a working feed. - A renewed consent fans out to every company sharing the old session, and re-points their account uids by IBAN. Several ASPSPs reissue uids on re-authorization, so carrying the session id alone would have left siblings calling retired uids and re-broken them every quarter. This is also why the superseded session_id is no longer nulled at /connect: the callback needs it. - The nightly probe runs once per distinct session and applies the verdict to every row holding it, and expiry mails are keyed per (user, session), so one dead consent is one probe and one mail rather than four of each. Only enabled cash_accounts rows count as claiming an IBAN. The callback mirrors every account in a consent, deselected ones included, so counting any row as a claim would leave nothing offerable once the first company connects. An account handed to a company also stops being offered while that company's picker is still open, closing the window where two companies could book the same physical account. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ink2): read the resultaträkning from the pre-closing books INK2R summed journal entries raw, so it included the resultatavslut that zeroes every P&L account into 2099 at year-end. Nettoomsättning, kostnader, periodiseringsfond and skatt all came out as 0, which cascaded into INK2S 7650/7651 and the taxable result. INK2 is always filed after bokslut, so this was every real declaration, and nothing warned: with the P&L at zero the balance sheet still tied out. INK2R now reads two views of the same period. The balance sheet comes from the closed books so 7302 keeps arets resultat via 2099; the income statement comes from the pre-closing books via excludeFinalClosingEntry, which drops only fiscal_periods.closing_entry_id so skatt and bokslutsdispositioner stay on the form (7525, 7528). The equity adjustment is now conditional on a posted closing entry having moved the result into 2099. Second, independent bug: accounts were mapped by BAS number with no regard for the sign of the balance, so konto 1630 with a credit was reported as a negative fordran instead of a skatteskuld and konto 2641 with a debit was netted off the liabilities. The three sign-reclassification rules the K2 iXBRL mapper already had are extracted to lib/reports/sign-reclassification .ts and applied to INK2R too, so both statutory reports present the same balance sheet. Only the rule table is shared: k2-mapper keeps its sumOre arithmetic because the iXBRL path is ore-exact while INK2R truncates per SFL 22:1. NE-bilaga had the same empty-resultatrakning bug and gets the same fix. Adds the closed-period coverage that was missing: the old tests only exercised the mapping table against an open period, the one state in which the engine happened to work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reports): make the year-end closing decision explicit at every call site generateTrialBalance took two optional booleans, so a caller that never thought about the resultatavslut silently got 'include'. That is the wrong default for anything summing class 3-8: the closing verifikat posts the mirror image of every P&L account into 2099 inside the same period, so the report reads ZERO across the board while the balance sheet still ties out and nothing warns. The booleans are replaced by a required closingEntry: 'include' | 'exclude-final' | 'exclude-all-year-end' with no default, so the build fails until each call site decides. All 40 were audited individually; every one keeps its current behaviour except the two that were provably broken: - Resultatrapport read zero on every line for a closed year, in JSON, PDF and XLSX, and its prior-year comparison column read zero for anyone whose previous year was closed. - Resultat per projekt (dimension-pnl) had the same defect and must stay in lockstep with Resultatrapport to keep reconciling. Both now pass 'exclude-all-year-end', which keeps them agreeing with the formal Resultaträkning rather than pre-empting Stage 2 of #1051 (DECISIONS.md:632). Deliberately unchanged and recorded in DECISIONS.md: the KPI expense composition, which is blank for a closed year but cannot be fixed without a migration and a displayed-figure change, and getBookedBolagsskatt, whose contract is an open period and whose call chain already caused a too-high-tax customer bug once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(vat): keep the resultatavslut out of the momsdeklaration The closing verifikat posts the mirror image of every P&L account into 2099 inside the same fiscal period. Revenue accounts drive rutor 05, 39 and 40, so any VAT period containing the fiscal-year end reported NEGATED turnover once the year was closed. get_vat_declaration_totals already excluded vat_settlement and opening_balance entries, but not this one. Reproduced read-only against production: for December of a closed year the December declaration reported ruta 39 = -794 734 kr. After the fix that period reports 0 and the January period carrying the real sale is unchanged at 794 734 kr. Keyed on fiscal_periods.closing_entry_id, not source_type = 'year_end': avskrivningar, periodiseringsfond and skatt share that source_type and must keep whatever VAT effect they carry. A reversed closing entry is retained together with its storno so the pair still nets to zero, the same predicate trial-balance.ts uses for closingEntry: 'exclude-final'. Migration applied to the staging branch only; prod gets it via merge. The pg test is written but has NOT been executed locally (no DATABASE_URL configured and no local Postgres), so CI is its first real run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(kpi): keep the resultatavslut off the monthly chart The monthly income/expense chart summed every posted entry in the fiscal period. The closing verifikat posts the mirror image of every P&L account, so once a year was closed the fiscal-year-end month charted the whole year's revenue as negative income. Measured read-only on production: 28 companies across 34 month-rows. The worst case charted December income as -10 347 459,81 kr where the real figure is +12,88 kr. Other examples: -1 868 731 -> +128 730, -1 850 501 -> +431 709. Both paths are fixed together so they keep agreeing: the RPC's monthly section now joins the tb_ex_ye_entries CTE it already computes for tb_ex_year_end, and monthly-breakdown.ts (the dimension-filtered fallback and the MCP path) gains the matching source_type filter plus the storno/correction chain of REVERSED year-end entries, so an undone bokslut does not leave half a pair behind. Migration 20260723180000 had recorded the omission as deliberate, on the grounds that it mirrored the JS scan. It did, but the JS scan was wrong. Migration applied to the staging branch (function body identical; three comment lines differ from the committed file). Prod gets the file via merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(reports): pin every statement generator against a closed fiscal year The per-generator suites all exercised an OPEN fiscal period, which is the one state in which a generator that forgets the resultatavslut happens to work. Declarations are filed AFTER bokslut, so the untested state was the only state that occurs in production. That is why the same defect could ship three times. Two new suites over one shared fixture (closed-year-fixture.ts, a synthetic closed AB with a resultatavslut, a credit 1630 and a debit 2641): closed-year-statements.test.ts enumerates the generators and asserts each reports the year's revenue rather than zero, plus its own bottom line. The table IS the checklist: a new report either appears in it or nothing stops it shipping with this bug. Verified by regressing income-statement back to closingEntry 'include', which fails 2 of its assertions. cross-surface-agreement.test.ts asserts the surfaces agree with each other, which is what every customer complaint actually was. INK2R and the K2 årsredovisning must produce the same årets resultat, the same fritt eget kapital, the same sign reclassifications and the same balance total. The operational family (Resultaträkning, Resultatrapport) must agree internally, and the gap BETWEEN the families is asserted explicitly as bokslutsdispositioner + skatt, so when Stage 2 of #1051 lands the test names the expectation to change instead of failing vaguely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(guards): ratchet against new reports that scan the ledger directly A statement generator that aggregates journal_entry_lines itself has to remember, on its own, that the resultatavslut posts the mirror image of every P&L account into 2099 inside the same fiscal period. Three forgot, and each read ZERO revenue for a closed year while the balance sheet still tied out, so nothing warned. generateTrialBalance now requires an explicit closingEntry mode, which makes that decision a compile error. This guard is what keeps NEW reports on that path: any generator under lib/reports or lib/bokslut that reads journal_entry_lines and is not in the baseline set fails CI. Verified by adding a throwaway report, which the guard rejects by name. Voucher and line listings (general-ledger, journal-register, SIE export, reconciliation, diagnostics) are sanctioned: they show the ledger as posted and have no closingEntry decision to make. Four existing lib/bokslut files are grandfathered rather than migrated. One of them is a genuine open follow-up recorded in DECISIONS.md: sarskild-loneskatt-calculator sums 7410-7419 with no year-end exclusion, so its basis reads ~0 if it runs against an already-closed period. Left alone deliberately: it is a tax figure whose call chain has caused a customer bug before and deserves its own verified change. Also ratchets naive-ore-round down 646 -> 641. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(reports): pin where sign reclassification applies, in both directions No behaviour change. The sweep asked whether the 1630/2641 sign reclassification should be extended to the remaining balance-sheet surfaces; the answer is that there are none left. Both STATUTORY presentations already have it: the K2 iXBRL årsredovisning since 2026-07-23 and INK2R since 2026-07-29. The other two balance-sheet surfaces must NOT have it: /rapporter Balansräkning and Balansrapport are organised by account number under BAS-prefix headings, and balansrapport documents an invariant that depends on every row staying debit-positive where it was booked. Moving konto 1630 into a liability section would break the add-the-rows-to-verify-the-balance property and hide the account from anyone looking it up by number. Asserting both halves is the point. The first half stops the reclassification silently disappearing from one statutory surface again, which is how a customer ended up comparing two of our own reports against each other. The second half stops a future sweep "fixing" the operational reports into disagreeing with their own documented contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(reports): detect statement disagreement instead of waiting for a customer Every year-end problem reported so far was a DISAGREEMENT between two of our own screens, not a single wrong screen. The årsredovisning said one figure, INK2 said another, and the customer did the reconciliation for us. Nothing in the product noticed, because each screen tied out on its own. Two additions: INK2R self-checks. On a closed year it compares the årets resultat it is about to declare against the booked konto 2099, and warns in Swedish when they disagree. This is the alarm that was missing: when INK2R reported 0 kr against a booked 469 542 kr, the balance sheet still balanced, so no warning fired. Mirrors the equivalent check k2-mapper has had since 2026-07-23, so both statutory reports now catch the same fault. reconcileStatements + GET /api/reports/statement-reconciliation return årets resultat from every surface side by side, grouped into families. ledger + statutory must agree and a mismatch is named; operational legitimately differs by bokslutsdispositioner + skatt until Stage 2 of #1051 lands, so that gap is explained rather than flagged. The visual panel is deliberately not built here: it needs a /frontend-design pass against the locked concept conventions plus sv/en strings, and the warning above already puts the alarm where the user looks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reports): address review findings from PR #1293 pg-real (7 failures, one signature): the new fixture called insertFiscalPeriod({ isClosed: true }) and then inserted journal entries into it, so enforce_period_lock (migration 017, legally required) refused the write. Not worked around: the RPC's predicate keys on fiscal_periods.closing_entry_id and never reads is_closed, so the fixture now links the closing entry and leaves the period open, which exercises the path that actually matters. CodeRabbit, closed-year-fixture: EX_YEAR_END_ROWS dropped only the P&L legs of the year_end entries (8811, 8910) and left their balance-sheet legs (2125, 2512) at pre-closing values, so the 'exclude-all-year-end' view sat 160 000 kr out of balance and misrepresented what generateTrialBalance returns. Latent, because today's consumers read class 3-8 only, but a shared fixture that does not balance is a trap for the next consumer. Both legs now go, and a new test asserts all three views sum to zero. CodeRabbit, INK2 totals: renamed totals.resultAfterFinancial to aretsResultat. It holds the result after bokslutsdispositioner AND skatt, which is årets resultat, not resultat efter finansiella poster, and build-data.ts uses the old name correctly for the different subtotal. The UI already labelled the value "Årets resultat", so the name was simply wrong. CodeRabbit, statement-reconciliation: the statutory branch called a generator and caught any throw as "wrong entity type", mapping genuine failures to a null figure that the comparison then skipped, so a real bug in a declaration generator made the function report isReconciled: true. That is the opposite of its purpose. It now dispatches on entity_type and surfaces a generation failure as a named disagreement. CodeRabbit, enable-banking (Emil's call to include): fetchClaimedIbans returned an empty Set on a cash_accounts read failure, which is indistinguishable from "nothing is claimed" and made every IBAN in the session offerable, including accounts another company already books to. Its own comment said it failed closed and its log said "offering nothing"; it failed open. Returns null now, and findReusableSessions offers nothing when the claimed set is unavailable. The test that pinned the fail-open asserted toHaveLength(1) under the name "offers nothing"; it now asserts []. Also removed an em dash per CLAUDE.md. The remaining enable-banking finding (consent-expiry cooldown stamped only on the selected connection, so it leaks one duplicate mail per sibling company) is deliberately left to Emil: it changes email-sending behaviour in his feature rather than fixing a stated contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reports): resolve second-round review findings on PR #1293 pg-real, two NEW signatures (the closed-period one from cycle 1 is gone): kpi-report-aggregates-rpc.pg.test.ts asserted the exact contract migration 20260730090000 deliberately changes. Its comment read "year_end entries are NOT excluded from monthly" and expected December expenses 1250. That fixture's December holds only year-end-chain entries, so with the fix the month drops out of the chart entirely, which is the correct operational view: a month whose only activity is bokslut has no operating result. Assertion and file docstring updated to the new contract rather than the test being removed. vat-totals-closing-entry.pg.test.ts passed the wrong account arrays. p_net_ accounts is VAT_SETTLEMENT_NET_ACCOUNTS (2650/1650, the momsredovisning settlement pair), not the output-VAT accounts. Putting 2611 there made the extra year_end entry match the settlement-SHAPE detector, so an ordinary sale-with-VAT was classified a momsredovisning and dropped, and the test read 0 instead of 10 000. The RPC was right; the fixture was not. CodeRabbit, statement-reconciliation: resolveEntityType checked neither query's error, so a genuine DB failure (RLS, permissions, connectivity) returned null indistinguishably from "no entity type set", fell into the unsupported-form branch and reported isReconciled: true. That is the same silent-false-reconciled bug the cycle-1 refactor closed, one level down. The companies error now throws; a missing company_settings ROW stays tolerated, because .single() errors on zero rows and many companies have none. Mirrors the pattern the INK2 and NE engines already use. Still open by Emil's explicit choice: the consent-expiry cooldown is stamped only on the connection it was handed, so it leaks one duplicate mail per sibling company on the shared session. That changes email-sending behaviour in his feature rather than fixing a stated contract, so it stays his. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
954ce873a8 |
fix(transactions): confirm and allow undo on a counterparty booking (#1292)
Booking via "Tidigare motparter" ended on a bare setExitingIds().add(id): the row animated out of the inbox and that was the entire feedback. No "Bokförd" toast, no Ångra, no unbooked-count decrement, and the id was never removed from exitingIds again, so an undo would have restored the row's data while leaving it filtered out of the list. Extracted runCategorize's success tail into one finishBooking() rather than copying the toast into the counterparty branch: the counterparty path was already a second, thinner implementation of it, which is why it drifted. Both paths and the counterparty activate-and-retry go through it now, pinned by a parity test. Also fixed on the way: - handleTransactionBooked (manual booking dialog / voucher match) never decremented the unbooked count, so the header read one too high until the next refetch. It deliberately gets no Ångra: its `matched` branch links the transaction to a PRE-EXISTING verifikat, and /uncategorize storno-reverses whatever journal_entry_id the transaction points at. - The 350ms animation timer re-applied the booked shape unconditionally, so an Ångra resolving inside that window left the client claiming a journal_entry_id the server had already storno-reversed. A completed undo now wins. - finishBooking cleared processingId unconditionally; scoped to the finished id so it cannot wipe an unrelated row's spinner. No migrations. |
||
|
|
198d3092c7 |
fix: counterparty template pick crashes the page (#1291)
Picking a suggestion under "Tidigare motparter" in Bokför transaktion replaced
the page with "Något gick fel". handleOpenTemplateReview built the review state
from `{ id, name_sv } as BookingTemplate`, so `template.debit_account` was
undefined, reached QuickReviewDialog's required `defaultAccount: string`, and
threw on `accountOverride.startsWith('2')` during the first render.
Typed the dialog's template prop as a narrow ReviewTemplate whose optional
fields are actually optional, so the cast disappears and the compiler owns this
class of bug. Also carries the counterparty's learned accounts and VAT (the
preview showed the category fallback, not what the server books) and decides
"is this a counterparty booking" from the template id rather than the presence
of a line_pattern (single-line templates got an account/VAT editor the
categorize route discards).
Five more page-crashes of the same shape, adversarially verified:
- suppliers/[id] and supplier-invoices/[id] passed the error envelope OBJECT as
a toast description. The Toaster is a sibling of {children} in the ROOT
layout, so that throw escapes both segment error boundaries onto global-error.
- components/reports/views wrote the same object into a useState<string | null>
at 13 sites and rendered it bare.
- components/ui/toaster.tsx now coerces non-renderable values as a choke point.
- skattekonto read data.informationstext.length off Skatteverket's raw JSON,
where the field is not required.
- TicWorkspace read profile.statuses.length off a persisted jsonb blob. 17 of 17
prod rows predate the TIC v2 upgrade (#584) and lack the key, so that
workspace was in the error boundary for every company that had opened it.
Plus hardening: formatCurrency coerces a null currency to SEK (prod has 0 NULL
across 28 416 transactions, so defense not a live bug) and cleanSignatory
returns [] for a missing description.
Verified by rendering the real dialog against a throwaway /sandbox route: the
pre-fix prop shape reproduces the exact error boundary, the fixed one renders
D: 6570 Bankavgifter / K: 1930 Företagskonto and the matching verifikat.
No migrations.
|
||
|
|
16f34fb214 |
fix(arcim): resolve OAuth redirect_uri identically in authorize and exchange (#1287)
* fix(arcim): resolve OAuth redirect_uri identically in authorize and exchange The authorize leg honored the FORTNOX_REDIRECT_URI / VISMA_REDIRECT_URI override while the token-exchange leg hardcoded the NEXT_PUBLIC_APP_URL fallback. After the app-domain cutover (2026-07-21) the Fortnox env var still pointed at app.gnubok.se while NEXT_PUBLIC_APP_URL moved to app.accounted.se, so the two redirect_uri values differed and Fortnox rejected every code exchange (RFC 6749 4.1.3). The failure was invisible: the error popup posted its message from the old-domain origin, the wizard's event.origin check dropped it, and the popup closed itself. Both legs now resolve through one resolveArcimCallbackUrl() helper, and the error popup stays open with the reason on screen so a dropped postMessage can never again turn into "nothing happens". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log OAuth popup and redirect-uri rollout decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ef25a87d75 |
feat(mcp): Tasks extension (io.modelcontextprotocol/tasks) (#1283)
* feat(mcp): speak spec revision 2026-07-28 (stateless core) Adopt the 2026-07-28 MCP spec revision on the connector endpoint while keeping every handshake-era client (2025-06-18 and earlier) byte-identical: - Accept per-request _meta protocol negotiation (io.modelcontextprotocol/protocolVersion); unsupported versions return UnsupportedProtocolVersionError (-32022) with the supported list. - Implement server/discover (spec MUST): supported revisions, capabilities including the extensions field, identity, instructions, freshness hints. - Decorate results for stateless clients: required resultType, serverInfo in _meta, and CacheableResult ttlMs/cacheScope on tools/list, prompts/list, resources/list, resources/read. - Validate the standard Mcp-Method/Mcp-Name request headers when present (HeaderMismatchError -32020); absence stays accepted. - Declare the ratified MCP Apps extension (io.modelcontextprotocol/ui) in capabilities; the widgets already use the ratified mime type and _meta.ui.resourceUri shape, so no widget changes are needed. - OAuth: include the RFC 9207 iss parameter on every authorization response (success and error) and advertise authorization_response_iss_parameter_supported in RFC 8414 metadata. Resource-not-found already used -32602 and tools/list ordering was already deterministic; both are covered by the new test file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): Tasks extension (io.modelcontextprotocol/tasks) Durable handles for long-running MCP tool calls, per the official Tasks extension. A client that declares the extension in its per-request capabilities gets a CreateTaskResult (resultType: "task") immediately; the work completes after the response via after() and lands in the new mcp_tasks table for tasks/get polling. Clients that did not declare the extension are never handed a task (spec MUST). - New mcp_tasks table (migration 20260729094000): company-scoped SELECT RLS, service-role-only writes (mirrors pending_operations), 1-hour expiry, status lifecycle CHECK. pg-real coverage included; triaged as excluded in the full-archive backup contract (transient state). - tasks/get (creator-scoped), tasks/cancel (cooperative, working-only flip), tasks/update (ack no-op: no input_required flows yet). - Tool opt-in via shouldRunAsTask predicate; first producer is gnubok_audit_package, the one genuinely long-running blocking call (multi-minute ZIP generation). estimate_only stays synchronous. - Tool failures complete the task with the standard isError envelope, exactly what the synchronous call would have returned; the failed status stays reserved for infrastructure errors. - server/discover and initialize now advertise the tasks extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): creator-only task RLS, enforced expiry sweep, RoPA entry Compliance-swarm follow-ups on the mcp_tasks migration (editing the migration is safe: it has not shipped beyond the ephemeral PR preview): - SELECT RLS tightened from company-wide to auth.uid() = user_id so the DB grant matches the creator-scoped tasks/get contract; task results carry raw tool output (Art. 5(1)(c)). pg test now proves a same-company colleague cannot read the row. - The 1-hour retention is now enforced, not aspirational: createMcpTask opportunistically deletes expired rows on every creation (idx_mcp_tasks_expires), best-effort (Art. 5(1)(e)). - RoPA entry mcp.async_task_handles added to .compliance/ropa.yaml (Art. 30). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): literal terminal-update payload for the phantom-column guard The conditional spreads in resolveMcpTask made the payload unresolvable for the no-phantom-columns guard (362 > 360 ceiling). A literal payload writing null for absent terminal fields is equivalent here: the terminal transition sets the complete terminal state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4501f118c2 |
feat(mcp): approval-queue MCP Apps widget for staged operations (#1278)
* feat(mcp): approval-queue MCP Apps widget for staged operations gnubok_list_pending_operations(render_ui=true) now renders an interactive approval queue (claude.ai / Claude Desktop) where the user approves or rejects each staged operation with a click. High-risk operations arm the approve button and the second click sends confirmed=true, so the BFL 5 kap 5 acknowledgment is a first-party human action instead of the agent asserting confirmed=true on the user's behalf (the audit weakness flagged in dev_docs/erpclaw_analysis.md). - New widget ui://pending-operations/app.html following the established self-contained postMessage/JSON-RPC pattern (no fetch, theme-aware, Swedish labels, expandable preview_data per row). - Result-level _meta.ui hint gated on render_ui=true, mirroring the VAT report wiring; the tool stays data-only by default. - Widget tool references project per namespace (accounted_* clients see accounted_ names inside the HTML). - tools/list payload ceiling 58K -> 58.5K per the in-test convention: prose trimmed to the floor first, remainder is wire contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): time out the widget RPC bridge so a silent host cannot strand a row Review follow-up: sendRequest never settled if the host dropped a response, leaving op._working=true forever with the approve/reject buttons gone. A 30s timeout rejects the promise; the existing catch paths restore the row with an error message so the user can retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dc5aea4a35 |
feat(mcp): speak spec revision 2026-07-28 (stateless core) (#1277)
* feat(mcp): speak spec revision 2026-07-28 (stateless core) Adopt the 2026-07-28 MCP spec revision on the connector endpoint while keeping every handshake-era client (2025-06-18 and earlier) byte-identical: - Accept per-request _meta protocol negotiation (io.modelcontextprotocol/protocolVersion); unsupported versions return UnsupportedProtocolVersionError (-32022) with the supported list. - Implement server/discover (spec MUST): supported revisions, capabilities including the extensions field, identity, instructions, freshness hints. - Decorate results for stateless clients: required resultType, serverInfo in _meta, and CacheableResult ttlMs/cacheScope on tools/list, prompts/list, resources/list, resources/read. - Validate the standard Mcp-Method/Mcp-Name request headers when present (HeaderMismatchError -32020); absence stays accepted. - Declare the ratified MCP Apps extension (io.modelcontextprotocol/ui) in capabilities; the widgets already use the ratified mime type and _meta.ui.resourceUri shape, so no widget changes are needed. - OAuth: include the RFC 9207 iss parameter on every authorization response (success and error) and advertise authorization_response_iss_parameter_supported in RFC 8414 metadata. Resource-not-found already used -32602 and tools/list ordering was already deterministic; both are covered by the new test file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): Mcp-Name covers params.uri, base64 sentinel, version-header consistency Review follow-ups against the transport spec text: Mcp-Name mirrors params.name OR params.uri (resources/read), values arrive base64-wrapped in the =?base64?...?= sentinel and must be decoded before comparison, and an MCP-Protocol-Version header that disagrees with the _meta protocol version is a HeaderMismatch. Absence of any header stays accepted since this server supports handshake-era clients (spec-sanctioned leniency). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |