* 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>