* fix(reconciliation): server-side confidence floor for unattended auto-apply
runReconciliation applied every greedy match, including auto_fuzzy at
confidence 0.75, with no server-side threshold. The UI is checkbox-gated,
but the unattended callers (enable-banking nightly sync cron, the
extension's post-sync sweep, and the v1 run endpoint) had no guardrail.
- Add ReconciliationOptions.confidenceThreshold (0..1, clamped): the
apply loop skips matches below it. Skipped matches stay in the result's
matches array and are counted in the new skippedBelowThreshold field,
so they are reported for review rather than silently dropped. Dry runs
are unaffected; omitting the threshold preserves current behavior.
- Both enable-banking sync callers now pass
DEFAULT_UNATTENDED_CONFIDENCE_THRESHOLD (0.9, mirroring the
gnubok_auto_match_period MCP default), so unattended runs never commit
fuzzy (0.75) or date-range (0.85) matches.
- v1 POST /reconciliation/bank/run accepts confidence_threshold
(optional, 0..1, mirroring the MCP tool naming) and returns
skipped_below_threshold; registry docs/pitfalls updated.
Closes#880
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reconciliation): surface skippedBelowThreshold in unattended sync logs
Review finding on the first pass: the floor's 'reported, not silently
dropped' guarantee never reached the two unattended callers, which
discarded or under-logged the result. Also logs the DECISIONS.md line
for the deliberate no-default choice on the v1 route.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(db): drop delete_user_account RPC that bypassed BFL retention
delete_user_account disabled the retention/immutability/audit triggers,
deleted audit_log rows, and cascaded auth.users, destroying 7 years of
legally retained rakenskapsinformation (BFL 7 kap 2 paragraf). It was
SECURITY DEFINER with only a self-only guard and no REVOKE, so any
authenticated user could call it via PostgREST.
The product path already uses anonymize_user_account, which so far
existed only on production (drift). This migration drops the dangerous
RPC, commits the prod definition of anonymize_user_account verbatim,
adds the profiles tombstone columns it writes (also drift), and locks
grants down to authenticated only.
Closes#342
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: log profiles tombstone-column drift-capture decision
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(salary): allow recalling approval on a salary run (approved → review)
An approved run was a dead end: the only forward path was paid → booked,
so a wrong salary snapshot (e.g. stale employee monthly pay) could not be
fixed without paying and then storno-correcting. Approval is an internal
control point — nothing legally binding happens until payment, booking,
or AGI filing — so recalling it is allowed until the AGI reaches
Skatteverket.
- POST /api/salary/runs/[id]/unapprove: approved → review; clears
approved_by/at and payment-file tracking; deletes generated-but-unfiled
AGI declarations (stale XML must not stay exportable); 409 once the
AGI is pending_signature/submitted/accepted — correction AGI (same
specifikationsnummer) is the lawful path then.
- New salary_run.approval_reverted event for the audit trail.
- "Ångra godkännande" secondary action on the run page with a
consequence-aware confirm (payment file possibly at the bank, sent
payslips, generated AGI), sv + en.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(salary): delete stale AGI after the unapprove transition, not before
Bot-review triage on #894: the declaration delete ran before the
optimistic status update, so a failed transition (concurrent flip,
transient error) would have destroyed the generated AGI while the run
stayed approved. Flip the run first; a delete failure afterwards is
harmless (agi_generated_at is already null, regeneration upserts over
the orphan). Also record the deleted declaration id in the
approval_reverted event payload, and warn in the confirm dialog that a
manually filed AGI requires a correction declaration instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(salary): close the unapprove TOCTOU on concurrent AGI filing
Superagent P2 + compliance-bot round 2 on #894: AGI submission is
allowed from approved (also out-of-band via MCP/public API), so a
filing could land between the route's read and its update, and the
route would flip the run and delete a submitted declaration.
- Re-assert agi_submitted_at IS NULL inside the optimistic update
filter, not just on the stale read.
- Guard the declaration delete with the same status filter so it
no-ops if the declaration advanced since the read; log a miss.
- Zero-row update (PGRST116) now returns 409 "status har ändrats"
instead of a generic 500.
- The approval_reverted event only reports deletedAgiDeclarationId
when a row was actually deleted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.
Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.
Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.
Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs
Every tab in the Skatt & bokslut nav group now tells an unconfigured user
what is missing and where to fix it, instead of dead-ending or rendering
zeros:
- Momsdeklaration: gates on vat_registered with a settings CTA; auto-fetches
the configured period on load and on every period change (no more "Hämta"
button); one period control (räkenskapsår picked inline for helårsmoms,
shell selector + back link dropped via new ReportDescriptor.standalone);
raw <select>s replaced with the Select primitive; banner when moms_period
is missing; BankID connect returns to the page instead of the report
library.
- Deadlines: callout (sv+en) when no system-generated tax deadlines exist —
they are derived from tax settings, so point at /settings/tax rather than
presenting an empty manual todo list.
- Årsbokslut: "no räkenskapsår yet" (CTA to bookkeeping settings) is now
distinguished from "nothing to close yet".
- Skattekonto: non-auth fetch failures render an error card with retry
instead of the misleading "inget saldo hämtat ännu" empty state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skatt): address bot review — res.ok guard, blocking moms_period gate, panel hidden while räkenskapsår unresolved
- Auto-fetch treats non-2xx or unparsable responses as errors (with retry)
instead of rendering undefined data.
- momsPeriodMissing now blocks the declaration (EmptyState + settings CTA)
rather than fetching a guessed quarterly period behind a banner — a
declaration submittable for the wrong period type is a hazard, not a
convenience.
- SkatteverketPanel is not rendered while yearly mode awaits a fiscal
period, so its actions can never target an unconfirmed period.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(ux): create salary runs, employees & recurring schedules in modals
The last three full-page create flows move to ?new=1 URL-driven dialogs,
matching the verifikat/invoice pattern (#861):
- Salary run: 4-field form on /salary — creation was pure interruption
before landing on the run-detail workspace.
- Employee: the last register entity still page-based after customers,
suppliers, and articles.
- Recurring schedule: consistency with the invoice modal it feeds.
Old /new routes survive as redirects so bookmarks and agent intents keep
working. Dialogs close explicitly (header X / Avbryt) so half-typed forms
survive stray Escape or backdrop clicks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: move DECISIONS.md to repo root
dev_docs/ is gitignored, so the decision log was invisible to other
developers. Root matches the existing convention (CONTRIBUTING.md,
SECURITY.md). CLAUDE.md pointer updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(lint): ignore Claude Code worktrees in eslint walk
.claude/worktrees/ holds full repo copies; without the ignore, local
npm run lint / check:lint walks them until the ratchet's 64 MB JSON
parse buffer overflows. CI is unaffected (no worktrees there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>