Commit Graph
5 Commits
Author SHA1 Message Date
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
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>
2026-07-04 15:58:06 +02:00
Jakob WennbergandClaude Opus 4.7 d0fbc2b616 refactor(ui): app-wide UI/UX consistency pass (#436)
* refactor(ui): app-wide UI/UX consistency pass

Net: +1,159 / −1,373 LOC across 77 files. No new features, no behavior
changes. Locks in a uniform design system across every dashboard surface.

What changed:

- **Foundation**: sidebar width 232→256px (md:w-64), spacing scale locked
  (Tailwind 1/2/3/4/6/8/10/12; 2.5/5 forbidden), card padding p-6 default
  (p-4 for compact metric cards), space-y-8 between page sections.

- **Tables unified**: all 33 thead blocks now share the Resultatrapport
  pattern via shadcn Table primitive (text-[11px] font-medium uppercase
  tracking-wider text-muted-foreground). Hand-rolled <table> instances
  converted where they were data tables; form/edit grids kept distinct.

- **Status badges unified**: every status indicator routes through
  shadcn <Badge variant>. Eliminated raw Tailwind colors
  (bg-amber-100, bg-emerald-500/10, bg-blue-100, bg-purple-100, etc.)
  in favor of the gnubok semantic palette (success=sage, warning=ochre,
  destructive=terracotta).

- **Empty states unified**: list pages migrated from hand-rolled
  "flex flex-col items-center py-12" divs to the EmptyState primitive.

- **Loading skeletons unified**: hand-rolled bg-muted rounded animate-pulse
  divs replaced with shadcn <Skeleton> across 15 files.

- **Touch targets**: 6 back-buttons + edit-pencil + inbox delete bumped
  from 24/32/36px to shadcn's 40px icon default. Added aria-labels on
  9 icon-only navigation buttons.

- **Date formatting**: formatDate() for accounting data (ISO yyyy-MM-dd,
  table-friendly) vs formatDateLong() for metadata (Swedish long form).
  Raw {x.invoice_date} renderings routed through formatDate() in 18 sites.

- **Toast titles**: eliminated 33 generic "Fel" titles. Each toast title
  now carries the action ("Kunde inte skapa lönekörning" etc.) with
  description carrying the error detail.

- **Page-level cleanups**:
  - Dashboard: dropped greeting hero + Snabbåtgärder/Att hantera nav
    duplicates + Visa detaljer collapsible.
  - Reports: 5-col mega-menu replaced with left-rail layout
    (new ReportsNav component).
  - Bookkeeping: fixed layout jump between Verifikationer/Ny verifikation
    tabs (moved FiscalYearSelector inside journal tab).
  - Bookkeeping: added voucher sort (A1 first / latest first) alongside
    existing date sort. Required matching API param sort_by.
  - KPI page: FiscalYearSelector instead of raw <select>; InfoTooltip
    instead of inline info-button toggle; bigger numbers.
  - Salary section: enum values translated to Swedish labels, mobile
    table collapses to Anställd+Netto on <md, KPI typography aligned
    with dashboard.
  - Invoice forms: styled RequiredMark + aria-required, tabular-nums
    on amount inputs.

- **CLAUDE.md**: new "Design System Tokens" subsection documents the
  locked spacing scale, primitives table, typography rules, date helpers,
  and forbidden patterns so future contributors don't drift.

Tests: 2,906 passing (unchanged). Lint: unchanged from main baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback (Greptile + compliance bot)

- **formatDate / formatDateLong timezone fix**: switch from new Date() to
  parseISO. Bare yyyy-MM-dd strings are now parsed as local midnight rather
  than UTC midnight, eliminating the off-by-one display in west-of-UTC
  timezones flagged by Greptile.

- **DashboardContentProps cleanup**: removed unused firstName and settings
  fields from the interface, and the corresponding fetch (profiles table)
  + computation in app/(dashboard)/page.tsx. The greeting was dropped in
  the dashboard cleanup; these props were dead weight.

- **Voucher sort behavior documented**: extended the comment in the journal
  entries API route to explain why voucher sort intentionally uses strict
  fiscal_period_id filtering (BFL 5 kap 6–7 §§ — voucher numbers are
  series-scoped within a fiscal year). The row-count delta between date
  sort and voucher sort is now a documented design choice.

- **delete_last_voucher migration + draft-delete test included**: the UI
  already shipped the "Radera utkast" path in the previous commit; this
  pulls in the backing RPC migration that allows draft deletes (with the
  full safety logic — drafts skip series/period checks since they have
  voucher_number=0, posted entries go through the existing unchanged
  path). This was originally meant for a separate PR but the UI shipped
  half the feature without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(migration): rename to match applied version

The function delete_last_voucher is already applied to the production DB
under version 20260509103736 (verified via pg_get_functiondef — exact
byte-for-byte match to file content). The previous file timestamp
20260509120000 would cause a fresh `supabase db push` to attempt re-applying
under a different version row. Renaming the file aligns local tracking
with what the database actually has.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address compliance bot findings (payroll label + VAT visibility)

- sick_karens label: drop "(första sjukdagen)" qualifier. Per sjuklönelagen
  6 §, karensavdrag is a single calculated amount (20% of one week's
  sjuklön) deducted from the first sick day's pay — not bounded to the
  first day. The qualifier could mislead users when the first sick day
  and return-to-work span a weekend. Swedish-payroll bot recommendation.

- Omvänd skattskyldighet badge: variant outline → warning. The reverse-
  charge indicator is compliance-critical (ML 16 kap) — missing it leads
  to incorrect input VAT deduction. Outline was too subtle; warning's
  ochre fill matches its semantic weight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:34:07 +02:00
Jakob WennbergandClaude Opus 4.7 4131db2894 chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes (#402)
* chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes

MCP server gains six intent-shaped tools that collapse multi-call
agent flows into one: vat_close_check, query_journal, auto_match_period,
create_supplier_invoice_from_inbox, audit_package, year_end_readiness.
Tools wired into TOOL_SCOPE_MAP and OPERATION_RISK_TIERS as appropriate
(create_supplier_invoice_from_inbox at medium tier — reversible until
approve, but stages a leverantörsskuld).

BankID enrichment now persists to a dedicated bankid_enrichment table
keyed by user_id. extension_data has been company-scoped (NOT NULL
company_id) since the multi-tenant refactor, so every BankID signup has
silently been failing the enrichment upsert. Select-company picker reads
from the new table.

delete_last_voucher (BFNAR 2013:2) needs to clear
document_attachments.journal_entry_id before deleting the entry, but the
new document immutability trigger blocks that UPDATE. Added the same
gnubok.allow_delete transaction-scoped bypass pattern used by the
journal-entry/line/retention triggers. pg-real tests cover the happy
path, the unauthorized direct UPDATE, and the swap-to-different-entry
attempt under the bypass flag.

fiscal_periods.no_overlapping_fiscal_periods exclusion was scoped to
user_id from before multi-tenant — rebound to company_id so the same
user can have overlapping fiscal years across companies they own/are
member of.

Also adds scripts/seed-demo-account.ts for end-to-end demo seeding
(two companies, full FY2025, active FY2026 with mixed state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-402): address review feedback

Migrations
- Drop 20260506140000_document_journal_entry_immutability_delete_bypass.sql:
  redundant with 20260506140000_document_journal_entry_immutability_bypass.sql
  that landed on main while this branch was open. Both share the same
  gnubok.allow_delete pattern; main's version is what the DB actually has.
- Rename 20260506150000_bankid_enrichment_table.sql →
  20260506160000_bankid_enrichment_table.sql to clear the timestamp clash
  with 20260506150000_protect_document_journal_link.sql on main (Supabase
  branch preview was failing on schema_migrations PK collision).

Tests
- Drop the swap-under-flag test from delete-last-voucher.pg.test.ts:
  main's bypass returns NEW unconditionally when gnubok.allow_delete='true',
  so the swap is permitted. Drop the duplicate happy-path test (already
  covered by 'clears journal_entry_id on attached documents and deletes
  the voucher'). Keep the unauthorized-direct-UPDATE test.
- Add bankid-enrichment.pg.test.ts covering the SELECT RLS policy:
  user reads own row, cannot read another user's row, INSERT denied for
  authenticated.

gnubok_query_journal
- amount_min/amount_max is applied post-fetch (PostgREST can't OR
  abs(debit) and abs(credit) cleanly), but PostgREST's count is computed
  pre-filter. Reporting that as total_lines mislead agents into
  paginating a tail that was already filtered out. When the amount
  filter is applied, anchor total_lines and truncated to the filtered
  set and surface db_matched_pre_amount_filter +
  amount_filter_applied_post_fetch separately.
- Escape `_` in the free-text LIKE filter so a search for "2_441"
  doesn't match "2X441".

VAT close check
- Reverse-charge blocker no longer fires on ruta 30 (seller-side
  domestic omvänd skattskyldighet) — the seller books no VAT, the buyer
  does, so missing ruta 48 is expected. Now scoped to ruta 31/32 (EU
  acquisition) where the buyer must book both calculated output (2615)
  and matching ingående moms (2645).
- High-value receipt threshold no longer reads journal_entries.total_amount
  (column doesn't exist; check silently never fired). Sums debits across
  the entry's lines, which equals the gross for ordinary purchase entries
  — comparing a gross figure against the BFL/ML 4 000 SEK threshold per
  ML 17 kap 26–28 §.

seed-demo-account.ts
- Require an explicit email argument; refuse to run with the previously
  hardcoded fallback that would silently target a real user. Ensure
  email is non-undefined for downstream typing.
- Type the supabase fiscal_periods insert result locally so tsc no longer
  reports 'fp implicitly any' from the loose untyped client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(test): adjust fiscal-period-start-day pg test for per-company overlap

The pg-real failure on PR #402 was a latent bug surfaced by this branch's
fiscal_periods exclusion constraint flip from user_id to company_id
(migration 20260506140100). The test was inserting periods that overlapped
seedCompany's default 2026-01-01..2026-12-31 period; the previous
constraint slipped past it because the test's INSERT didn't set user_id
(NULL escapes the WITH = match), so two same-company overlapping periods
silently coexisted.

Now that the constraint correctly fires per company, pick years that
don't overlap with the seeded 2026 period. The trigger's behavior under
test (allow mid-month start when no earlier period exists, allow
back-dated SIE imports, reject mid-month start when an earlier period
exists) is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vat-close-check): correct reverse-charge/import blocker rutor

Rutor 30/31/32 are the buyer's calculated utgående moms on reverse-
charge purchases (domestic byggtjänster/electronics → 2614 → ruta 30;
EU goods → 2624 → ruta 31; EU services → 2634 → ruta 32). The buyer
must also book matching ingående moms (2647 inhemskt / 2645 utlandet
→ ruta 48). The previous fix removed ruta 30 on the basis that it was
seller-side; that's incorrect — domestic-RC sellers book no VAT at
all (they report only beskattningsunderlag on ruta 41), so 2614 only
sees buyer-side entries. Restore ruta 30.

Also extend the check to import rutor 60/61/62 (non-EU import VAT
declared via momsdeklaration since 2015 — 2615/2625/2635). Same
mechanic: importer books output VAT on these rutor and deducts the
input side via ruta 48. SaaS-from-AWS / OpenAI / Vercel companies hit
this path; without including 60/61/62 the blocker would silently miss
their misbookings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): expose ruta 60/61/62 (import VAT) on the local VatReportResult

The vat-close-check fix referenced vatReport.rutor.ruta60/61/62 but the
MCP server's local VatReportResult type only carries ruta 05-49. Build
broke on tsc.

Extend the MCP server's slim VAT report to also project import VAT —
2615 → ruta 60 (25%), 2625 → ruta 61 (12%), 2635 → ruta 62 (6%) — and
fold those into ruta 49 (att betala/återfå). Mirrors the BAS-to-Ruta
mapping in lib/reports/vat-declaration.ts. Output schema and required
list updated accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:41:36 +02:00
Mattsson ce3af4d17e Fix/multiple domain issue (#401)
* feat: enhance invoice management and immutability checks

- Update InvoiceDetailPage to prevent deletion of drafts with assigned invoice numbers, providing user feedback.
- Modify the invoice conversion API to ensure invoice number allocation occurs only after successful item insertion and proforma cancellation.
- Implement structured error responses for invoice deletion, ensuring only drafts without assigned numbers can be deleted.
- Add comprehensive tests for invoice deletion and conversion scenarios, including edge cases for draft invoices.
- Introduce immutability checks in the document management system to prevent unauthorized changes to linked documents.
- Create SQL migration to enforce document metadata immutability, ensuring compliance with accounting regulations.

* fix(invoice): prevent invoice number consumption on PDF render failure

* feat: add document journal entry immutability enforcement for delete_last_voucher RPC

* fix(invoice): implement rollback for orphan invoices on proforma cancel failure

* fix(document): extend immutability trigger to protect journal entry links
2026-05-06 14:08:38 +02:00
Mattsson 2d36dedf34 fix(journal-entry): add error logging for delete operation in journal… (#379)
* fix(journal-entry): add error logging for delete operation in journal entries

* fix(journal-entry): enhance error logging and add tests for delete_last_voucher functionality

* fix(journal-entry): restore enforce_journal_entry_immutability function to handle DELETE and un-reversal updates

* fix(tests): refactor delete_last_voucher tests to use insertPostedEntryWithLines for consistency
2026-04-28 21:55:51 +02:00