* perf: cross-system snappiness batch (middleware, loading states, bundle)
Middleware: resolve the active company at most once per request and run
the user_preferences + first-membership queries in parallel, cutting 1-2
sequential DB round trips from every authenticated page load.
Loading states: add loading.tsx skeletons for the six highest-traffic
dashboard routes, render the real salary page header during load instead
of a full-page skeleton, replace the blank fallback={null} Suspense
flashes on customers/articles, and reshape the settings skeleton to
match the actual form layout.
Bundle: defer recharts chart components via next/dynamic on the KPI page
and report views, replace the import page's framer-motion marching-ants
border with a CSS keyframe, and enable optimizePackageImports for
recharts/date-fns/framer-motion.
Transactions: extract the potential-match lookups into a shared parallel
helper; a single-query PostgREST embed is blocked until the
potential_supplier_invoice_id FK exists in prod (see DECISIONS.md).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transactions): log potential-match query failures instead of dropping them
A DB error in the invoice/supplier-invoice hint lookups previously
surfaced as "no potential match"; log it so failures are diagnosable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(api): ROT/RUT + articles + dimensions on the v1 invoice surface (#895)
- v1 invoice POST now routes through buildInvoiceWriteData, the same
builder as the dashboard: ROT/RUT deduction lines (server-side compute,
personnummer encryption), article_id + revenue_account linkage,
accruals, and line_type no longer get silently dropped on the wire.
- v1 invoice PATCH accepts default_dimensions so integrations can tag a
draft with a project/cost centre after creation.
- New PATCH/DELETE /dimensions/:id/values/:valueId: rename, archive,
set end_date on project codes; delete unreferenced values (409 with an
archive hint when the BFL retention trigger blocks).
- New GET /articles: read-only artikelregister list (incl. housework_type)
so callers can resolve article_id before composing invoice lines.
- Invoice GET/POST projections now expose deduction fields and full item
columns; dry-run previews never echo the encrypted personnummer.
Closes#895
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): address review on #904
- Extract shared v1 invoice projections to lib/api/v1/invoice-columns.ts
so create/detail/patch responses can't drift; PATCH now returns
deduction_total + deduction_personnummer_last4 like GET/POST.
- Narrow the v1 create customer fetch back to the three fields the
builder reads instead of select('*').
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
gnubok_create_supplier_invoice_from_inbox sourced the invoice's header
vat_amount from the OCR-extracted totals.vat field instead of summing
the per-line vat_amount values. That header field is never reconciled
with the line items, so per-line VAT customization (or a mis-extracted
document total) could leave it at a stale or zero value.
createSupplierInvoiceRegistrationEntry (and the cash/privately-paid
variants) then gated the entire 2641 ingående moms posting on
invoice.vat_amount > 0, so a stale header silently suppressed a
correct per-line VAT split with no error. Confirmed via the ledger:
this exact defect hit Glesys 623884, DNB 9664449205, and ComputerSalg
500265968 (all already manually corrected via storno+correction).
Fix: derive vat_amount from summed lineItems in the MCP tool, and
switch the three registration-entry gates from the header field to
itemsHaveVat(items), so the engine itself can no longer be fooled by
an unreconciled aggregate regardless of which caller populates it.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
* 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>