Commit Graph

53 Commits

Author SHA1 Message Date
Jakob Wennberg 36a1df4f6b feat(import): detect and import the article register's Valuta column (#1183)
Fixes #1167. The register export gained a Valuta column in #1166 but
the importer ignored it, so re-imported non-SEK articles silently
became SEK, breaking the export -> edit -> re-import round-trip.

- Column detector recognizes valuta/valutakod/currency (claimed before
  generic columns; no keyword collision with Momskod).
- Parser normalizes to upper-case ISO shape, drops malformed codes
  with a file-level warning, and carries currency per row.
- Execute route validates codes lazily against the currencies table
  (FK stays the backstop when the reference read fails), imports valid
  codes, defaults absent to SEK, and in merge mode only overwrites
  when the file explicitly carries a valid currency.
- Edit step shows a muted currency marker next to non-SEK prices;
  manual column mapping offers Valuta.
- Export docblock caveat removed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:59:23 +02:00
Jakob Wennberg aead2bc1d1 fix(ui): stop mislabeling unconverted FX amounts as kr in aggregates and toasts (#1182)
Fixes #1173. invoices.total_sek stays NULL when the Riksbanken rate
fetch fails at creation, and every `total_sek || total` fallback then
treated a raw foreign amount as kronor:

- lib/calendar/utils: new invoiceSekAmount() returns null for
  unconverted non-SEK invoices; period summaries and day totals skip
  them and PeriodSummary exposes unconvertedCount. PaymentSummaryCard
  shows a one-line note when invoices were excluded; CalendarDayView
  renders each invoice in its own currency instead.
- Deadlines page: the overdue attn sum now skips unconverted FX
  invoices and appends "(+N i utlandsk valuta)" instead of adding EUR
  into a kr total.
- Supplier-invoice payment toast formats the amount with the invoice's
  currency (key drops its hardcoded " kr" in both locales).
- AR aging drill-down row labels Betalt with the invoice currency,
  mirroring the outstanding cell.
- BankFileColumnMappingStep: comment pinning why SEK is safe there
  (generic-csv hardcodes it).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:59:14 +02:00
Jakob Wennberg bb78f8fce8 fix(import): per-currency totals and row currency in bank-file preview (#1178)
* fix(import): per-currency totals and row currency in bank-file preview

Fixes #1170. ParsedBankTransaction carries a per-row currency (Wise
emits genuinely mixed rows; camt.053 reads Ccy per entry), but the
preview and confirm steps formatted every amount as kr and rendered
parser-level income/expense totals that sum across currencies.

Adds summarizeByCurrency() (income positive / expenses negative, ore
rounding, SEK default) and renders one total line per currency on both
steps; preview table rows format amount and balance with the row's own
currency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(import): use roundOre from lib/money (antipattern ratchet)

The naive Math.round(x * 100) / 100 form is blocked by check:guards
(subtly wrong on exact-half values); lib/money.roundOre is canonical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:58:47 +02:00
Alexander Reinthal 1dc85736d8 feat(import): add Wise (TransferWise) CSV import format (#1018)
* feat(import): add Wise (TransferWise) CSV import format

Wise exports a single multi-currency transaction history (one row per balance
movement). Add it as a bank-file format plugin so it flows through the existing
upload -> preview -> confirm -> execute wizard.

- lib/import/bank-file/formats/wise.ts: quote-aware parse (dates contain a
  space), Direction IN/OUT drives the sign, booked on the moved side (target
  for IN, source for OUT). Native currency preserved; SEK conversion is left to
  the downstream FX/booking pipeline (Riksbanken).
- Non-zero Wise fees become their own negative "Wise avgift" row (source and
  target), so the fee books separately and the balance ties out.
- Only COMPLETED rows import. external_id keys on the stable Wise ID
  (TRANSFER-/PLAN_ORDER-, -fee suffix for fee rows) via a new 'wise' branch in
  generateExternalId, so re-imports dedup exactly.
- Register the format (types, parser list), add it to the manual-format picker
  and the v1 /imports/bank format enum.

Tests cover detection, IN/OUT signing + currency, fee splitting, stable
external_id, and COMPLETED-only filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

* fix(import): harden Wise parser against malformed rows (CodeRabbit #1018)

- Strict amount parsing: reject "12abc"/"1,234" instead of parseFloat coercing
  them to 12/1 and silently corrupting the imported amount.
- Require Status to be exactly COMPLETED: a blank/missing status no longer
  slips through the completed-only filter.
- Fail hard on an unsupported Direction: a blank or non-IN/OUT value (e.g.
  NEUTRAL for a balance conversion) throws instead of being guessed as income;
  the parse route surfaces it as BANK_FILE_PARSE_FAILED. Proper conversion
  support is tracked in #1019.
- Never invent currencies: a missing movement currency skips the row with a
  warning (no SEK default), and a fee with no currency of its own is dropped
  with a warning rather than inheriting the movement currency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

---------

Signed-off-by: Alexander Reinthal <email@reinthal.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-16 16:22:32 +02:00
Jakob Wennberg 982fe77f72 fix(import): actionable hint when a bank CSV lands in the opening-balance importer (#953)
* fix(import): hint when a bank statement is uploaded as opening balances

Uploading a bank statement CSV to the opening-balance importer produced
the generic 'Inga konton med belopp hittades' error with no clue that
the file belongs in the bank-transactions importer (#918, users got
stuck together with #915).

When the opening-balance parse yields zero account rows, the parser now
runs the registered bank-file format detectors over the CSV content
(the generic CSV fallback never auto-detects, so any match is a real
bank format) and reports the matched format name as
detected_bank_format on the parse result. The upload step then shows an
actionable Swedish error naming the bank plus a button that routes to
the bank-transactions importer (/import?mode=bank).

Closes #918

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(import): use the standard bank-import CTA wording (CodeRabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:10:09 +02:00
Jakob Wennberg ed7a178ac6 fix(import): scope bank account picker to the active company (#922)
The chart_of_accounts query in BankFileConfirmStep had no company_id
filter, so RLS returned bank accounts from every company the user is a
member of, duplicating 1930/1940/etc. in the dropdown and making the
selected value ambiguous across companies.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 14:17:35 +02:00
Jakob Wennberg ac1529e413 fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors (#902)
* fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors

- Supplier invoice form (#863 item 1): onSubmit now blocks any
  non-reverse-charge line whose VAT rate is outside the legal Swedish
  set {25, 12, 6, 0} with a destructive toast naming the line and the
  legal rates. Server-side schema tightening stays out of scope.
- Opening balance import (#837): the summary derives isBalanced from
  the running totals (0.01 epsilon) and renders the AlertCircle
  destructive pattern with the differens amount when unbalanced;
  canExecute includes isBalanced so the commit button is disabled
  instead of funneling users into a server-side rejection.
- EditAccountDialog (#838): a failed account save now shows a
  destructive toast with the server-provided message instead of being
  silently swallowed; the dialog stays open for retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): compare opening-balance totals in whole ore, map account-save errors to Swedish

Review findings on the first pass: the < 0.01 epsilon misclassified exact
1-ore imbalances as balanced (0.03 - 0.02 evaluates just under 0.01), and
the save-failure toast surfaced raw English server text instead of routing
through getErrorMessage like the sibling handlers in the same file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:20:46 +02:00
Mattsson 2c2743eb79 Check/salary bankid api (#892)
* fix(bankid): harden login/signup flow — polling, signup rollback, metadata merge, enrichment lookup

- middleware: read BankID enrichment from the bankid_enrichment table (the
  extension_data path has been dead since the multi-tenant refactor), so
  company-less BankID users land on /select-company instead of the manual wizard
- BankIdAuth: hard 6-min poll deadline; every failed poll counts toward the
  give-up limit; guard overlapping ticks so completion runs exactly once
  (a double /complete regenerated the magic link and invalidated the first,
  failing logins intermittently); retry clicks wait out the start cooldown
  instead of silently no-oping; Swedish messages for 429/unknown start errors
- bankid/complete: all-or-nothing signup — delete the created user when the
  identity insert, app_metadata update, or magic-link generation fails, so a
  retry starts clean instead of hitting account_exists with an unusable account
- bankid/unlink: read-merge-write app_metadata so has_password survives unlink
  (BankID-only users could otherwise strand themselves with no login method)
- login: BankID "create account" CTA now links to /register instead of
  dismissing the notice; sv.json: fix missing å/ä/ö in settings_bankid strings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: move secondary guides into docs/, delete dead root files

Move DOCKER.md, SELF-HOSTING.md, WHITELABEL.md and extensions.md
(renamed EXTENSIONS.md) into a new docs/ folder and update all path
references (README, setup.sh, .dockerignore image rules, docker-publish
workflow comment, _example-branding, lib/branding/service.ts).

Delete two dead root files: customer.json (stray API-test payload) and
findings.md (point-in-time swarm audit export, criticals already filed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(api): security & correctness hardening + withRouteContext MFA migration across API routes

Audit of ~100 app/api routes. Highlights:

Security
- agent/conversations: list leaked colleagues' titles + message previews
  (company-scoped RLS, no user filter) -> user-scoped
- calendar/feed PUT: raw body into .update() allowed feed_token fixation on a
  public unauthenticated URL -> strict schema, content toggles only
- bokslutsdispositioner: unbounded schablonintaktRate could inflate the
  IL 30 kap 25% periodiseringsfond cap base -> bounded
- agent profile/composer/onboarding: viewers could rewrite the agent profile
  while sibling /verify blocked them -> role-gated

Correctness
- account-totals / listAssets: unbounded queries silently truncated at 1000
  rows (under-counted money; skipped assets at year-end depreciation) ->
  fetchAllRows with stable order (+3 more pagination fixes)
- voucher-gaps: swallowed detect_voucher_gaps RPC errors (BFNAR gap view could
  show "no gaps" when the check never ran) -> surfaced
- 5 phantom-success writes (OK on zero matched rows) fixed
- assets K3 component-sum validated against stale acquisition_cost -> fixed
- invite silent email-send failure -> response carries email_sent;
  deadlines/calendar cast-then-check JSON crashes -> Zod

Convention
- ~44 legacy routes converted to withRouteContext (MFA); added Zod validation,
  corrected status codes, console.* -> lib/logger

Response shapes preserved for existing callers. ~110 new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): save a booking as a reusable template from Bokför direkt

Add a "Spara som mall" action to the manual booking dialog so users can
capture a kontering they just worked out as a booking template — right
where they figured out how something should be booked.

- derive amount-parameterised template lines from the concrete booking
  (settlement = the non-VAT leg nearest the total, 26xx = a VAT line with
  its rate snapped to the nearest standard rate, the rest = business
  ratios; line labels come from the loaded BAS chart)
- extract the shared TemplateForm out of BookingTemplatesPanel so the
  booking dialog reuses the same editor, live preview and convertibility
  hints instead of duplicating them
- save via the existing POST /api/settings/booking-templates endpoint

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

* fix(bokslut): render arsredovisning RR/BR at ÅRL post level — no kontonummer

Bolagsverket rejected a user's filed årsredovisning with "Balansräkning
och resultaträkning ska inte innehålla kontonummer": the PDF built every
statement row as per-account "1930 Företagskonto" lines while the iXBRL
filing path already aggregated to statutory posts, so the two artifacts
diverged.

The PDF statements now derive from the same K2 risbs mapping the iXBRL
document uses (mapTrialBalancesToK2), via a new statement-rows.ts that
emits post-level rows in uppställningsform order for both the K2 and K3
templates. Also fixed along the way:

- Jämförelseår column (ÅRL 3:5 §) — previous-year trial balances now load
  and render; the old PDF had no comparatives at all.
- mapping.warnings (unmapped accounts, RR ≠ 2099, obalans, reclass
  nudges) flow into ArsredovisningData.warnings so the wizard flags a
  non-fileable document before download.
- Flerårsöversikt current/previous year overridden with the mapper's
  strict-3000–3799 Nettoomsattning, mirroring build-input's
  duplicate-fact rule, so the FB table ties to the RR.
- FB eget kapital-table is post-level and drops obeskattade reserver
  (never eget kapital); K3 equity-changes statement uses real prior-year
  opening balances with derived utdelning/nyemission residuals that tie
  the roll-forward exactly to booked UB.
- build-input dedupes warnings now that the PDF path runs the same
  mapping.

Regression test asserts no RR/BR label ever contains a four-digit
account number again.

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

* fix(reports): diagnose untransferred prior-year results behind balance-sheet differens

Prod incident (97 kr): a multi-year SIE migration lacked one year's
omforing av arets resultat; the residual corrupted every later derived
opening balance and Balansrakningen showed a bare "Differens: 97 kr"
with no explanation. Continuity checking cannot catch this failure mode
(prior-year UB and derived IB match per-account by construction) - the
invariant that actually breaks is per-year P&L = 0 for all non-latest
years.

- lib/reports/imbalance-diagnosis.ts: shared detector
  (findUntransferredResults + buildImbalanceDiagnosis)
- Balansrakning/Balansrapport attach imbalance_diagnosis when unbalanced,
  naming the exact culprit years; rendered in web views + PDF; MCP
  gnubok_get_balance_sheet inherits the field via spread
- SIE import: parse-time warning when a completed year's vouchers leave
  a P&L residual, plus a post-import DB walk surfacing culprits as
  warnings and structured details.untransferredResults; the Arcim
  migration workspace previously dropped result.warnings entirely and
  now renders them
- opening-balance/correct: pre-flight the company lock date and return
  409 OB_COMPANY_LOCK_DATE (retryable: false, lock date interpolated in
  the client message) instead of the retryable 500 that invited blind
  retries; catch-path maps a raced trigger rejection to the same code

Diagnosis runs only on unbalanced paths (zero cost when healthy) and
never fails the report or the import. No migration, nothing persisted.

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

* fix: production error remediation — FX rates, deadlines, log levels, correction relink

Batch of fixes for recurring Vercel runtime errors:

- Riksbanken FX rates: persistent read-through cache (exchange_rates
  table), one retry honoring Retry-After on 429/5xx, bounded ingest
  concurrency, and an honest fallback — most recent cached observation
  or null, never a hardcoded rate silently booked into amount_sek.
  Unrated transactions stay repairable via refresh-exchange-rate.
- Tax deadline regeneration inserts replacement rows before deleting
  the superseded set, so a failed insert no longer wipes a company's
  deadlines (the 23502 user_id regression did exactly that). Migration
  makes deadlines.user_id nullable for system-generated rows.
- Route wrappers + errorResponse log 4xx outcomes at warn so only
  genuine 5xx reach Vercel's runtime-error clustering; client-supplied
  /api/log telemetry demoted to warn as well.
- application/json documents (raw PSD2 responses archived per BFL)
  validate as parseable JSON with object/array root instead of always
  failing the magic-byte check.
- correctEntry surfaces document-relink failures to callers, and the
  BFL document-immutability trigger now allows relinking underlag from
  a reversed entry to its correction (migration + pg test).
- Middleware clears stale session cookies on /api requests too, using
  scope 'local' so cleanup doesn't re-trigger the failed token refresh.

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

* fix(skatteverket): persist token health and stop retrying dead consents

Terminal auth errors (SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE,
TOKEN_CORRUPTED) mark the token row needs_reconsent with the error code
and timestamp — SKV per-flow refresh tokens live 65 minutes, so once
expired nothing recovers without a fresh BankID consent. The AGI
kvittens and skattekonto sync crons skip flagged connections instead of
failing every night, and the settings panel prompts for re-consent
proactively. A successful reconnect resets the row to active.

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

* fix(banking): allocate distinct BAS ledger slots for PSD2 mirror accounts

A bank returning N same-currency accounts used to map them all onto the
currency default (1930/1932/1933/1934), tripping the UNIQUE
(company_id, ledger_account) constraint per-account — swallowed errors
left accounts silently unmirrored. allocatePsd2LedgerAccount now hands
out the currency default first, then free 1931–1959 sub-account slots,
skipping slots held by any existing row.

- Callback persists allocations to accounts_data so the picker pre-fills
  reality; reconnect reuses previously mirrored ledgers instead of
  re-deriving (a user remap to 1935 survives).
- Selection save resolves effective ledgers up front and rejects
  duplicates or cross-connection conflicts with a 400 instead of
  silently skipping the mirror.
- Bank error codes + psu_type are forwarded to the settings page for
  every OAuth error, keying the Handelsbanken corporate fullmakt
  guidance.

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

* fix(agent): stage exact journal lines on categorization previews

Categorization previews only carried debit/credit accounts, the GROSS
amount, and separate VAT rows — read together that looks like an
unbalanced 'gross on cost account + VAT debit' entry, and it misled
both users and agents into rejecting correct proposals. The MCP
preview and the pending-operation PATCH now materialize the exact
lines the commit executor will post (net cost line, VAT line, gross
bank line, SEK) via buildTransactionEntryLines, and PATCH re-derives
them from the new mapping instead of spreading stale staged lines.
ApprovalCard and /pending render the verifikat lines, falling back to
the legacy summary only for operations staged before this fix.

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

* feat(bookkeeping): prune unused imported accounts from the chart

SIE imports routinely bring in hundreds of accounts that were never
used and clutter the kontoplan. New account_usage_counts RPC (one
grouped query instead of a count per account) backs GET
/api/bookkeeping/accounts/usage, and POST /api/bookkeeping/accounts/prune
deletes zero-usage accounts — dry-run first, then an explicit account
list capped at 2000. Accounts with journal lines are skipped, never
deleted. The chart manager shows a usage column and a prune dialog
grouping custom accounts vs unused BAS-seeded ones.

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

* feat(api): carry dimensions through v1 invoice and supplier-invoice surfaces

Credit-note creation now copies default_dimensions and per-line
dimensions from the original, so the reversing journal entry nets
against the same dimension cells instead of dropping them. List/detail
responses expose the dimension fields, and the OpenAPI spec snapshot
follows.

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

* perf: batch serial Supabase round-trips on hot dashboard paths

Every dashboard render pays the layout's query chain, so serialized
awaits are direct wall-clock: the layout, chat conversation, invoice
detail, supplier detail, select-company, and agent-onboarding pages now
run their independent lookups in parallel batches, and
getCompanyCapabilities folds its disabled-config read into the same
round-trip. JournalEntryList hydrates the saved fiscal-year scope
optimistically instead of serializing the first entries fetch behind
the fiscal-periods request. The supplier detail page filters invoices
server-side via a new supplier_id query param instead of fetching the
whole company ledger, and the invoice editor (with its framer-motion
dependency) lazy-loads so it stops shipping with the invoice list
bundle.

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

* feat(salary): one-click runs, payslip delivery, payments settings, run cockpit

Salary P1 batch, driving the 20-click flow toward 3 clicks:

- One-click 'Starta lönekörning': POST /api/salary/runs accepts an
  empty body and resolves defaults server-side — period follows the
  latest non-corrected run, payment date from the new
  salary_pay_day setting, series from the per-source-type map. The
  separate /salary/runs/new page is gone.
- Run detail page rebuilt as a step-railed cockpit (progress rail,
  KPI cards, employee ledger, journal preview) on a deliberately
  wider canvas; components extracted to components/salary/run/.
- Payslip delivery: tokenized public payslip pages (/payslip/[token],
  backed by salary_payslip_links) plus per-employee email send with
  PDF — employees need no account, and the middleware exempts the
  route from auth redirects.
- Payments settings: salary pay day, default bank, and pain.001 vs
  Bankgirot Lön format with per-bank upload instructions and an LB
  sunset warning (banks retire LB during 2026).
- AGI panel: full submission status flows (stale drafts, signing
  links, kvittens polling, error reports); tax payment panel with
  skattekonto shortcut and mark-as-paid.
- Salary calendar bulk editing, employee benefits/tax-card polish,
  municipality tax-table lookup improvements.

messages/sv+en also carry the strings for the account-prune,
skatteverket-reconsent, and banking surfaces committed just before
this.

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

* chore: adopt Next 16 proxy.ts convention + repo housekeeping

- Rename middleware.ts to proxy.ts with the proxy() export (Next 16
  renamed the middleware convention; behavior unchanged).
- Exclude dev_docs/ from tsconfig so stray snippets in planning docs
  don't break the build type-check.
- Ratchet antipatterns-baseline down (raw-route-auth 165 → 119) to
  lock in the withRouteContext migration from 5cfd2b76.
- template-library uses roundOre() instead of inline rounding.
- database.md: drop account_balances from the key-tables list.

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

* fix(bookkeeping): robust service-role detection in correction document relink

relink_documents_to_correction() keyed its service-role branch on auth.role(),
which reads the singular request.jwt.claim.role GUC that PostgREST v10+ and the
pg-real harness no longer populate. Genuine service-role callers (pending-ops
executor / MCP approve) landed in the auth gate and could not relink underlag.
Read the role from the request.jwt.claims JSON directly, mirroring the canonical
link_voucher_rpcs_tenant_guard convention. Validated on staging.

Also: harden the salary run page's error paths (res.json().catch) against
non-JSON error bodies, and roll back the pg-real service-role case in finally so
an aborted transaction cannot poison a pooled connection for the next test.

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

* fix(documents): restore journal_entry_line_id link durability (BFL 7 kap)

Migration 20260704103000 rewrote enforce_document_journal_entry_immutability to
guard journal_entry_id but left journal_entry_line_id to the metadata trigger,
which exempts draft-linked docs -- and the entry-level trigger only fired on
UPDATE OF journal_entry_id, so a line-id-only UPDATE never invoked it at all.
That let a set journal_entry_line_id be cleared to NULL, breaking the "link
durable from first set" invariant (document-immutability.pg regression).

Widen the trigger to fire on journal_entry_line_id too and guard it with the
same uuid-durability rule as journal_entry_id (setting NULL -> uuid stays
allowed; clearing/re-pointing a set value is blocked, status-independent). The
correction-relink GUC path, which legitimately clears line_id when moving
underlag to the posted correction, stays exempt. Validated on staging.

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

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 03:05:09 +02:00
Jakob Wennberg 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 Wennberg fb3fe82a56 feat(dimensions): PR5 SIE round-trip — lossless dimension import + undo lockstep (#866)
* feat(dimensions): PR5 SIE round-trip — lossless dimension import, registry upsert, undo lockstep

SIE import previously parsed and silently DISCARDED all dimension data
(object lists at sie-parser.ts:651-654, #DIM/#OBJEKT in the ignore list at
:689). Import is now lossless — the dimensions plan PR5 milestone.

Parser: #TRANS object lists ({1 "KS01" 6 "P001"}) land on the line as an
SIE-dim-no → code map (canonical numeric keys, quoted codes, malformed
pairs warn); #DIM/#UNDERDIM/#OBJEKT parse into registry records. OIB/OUB
stay ignored (dimension reporting is P&L-only in v1).

Importer (lib/import/sie-dimensions.ts): upserts missing dimensions/
dimension_values rows — never renames existing ones (ON CONFLICT DO
NOTHING); undeclared reserved numbers synthesize their SIE-standard names
(mirroring the export's orphan synthesis); codes violating the registry
CHECK are skipped with a warning but survive verbatim on lines (documented
legacy-free-text exception). Bulk voucher insert now writes the dimensions
jsonb + cost_center/project mirrors via the sanctioned dual-write helpers
(no trigger suppression needed — the immutability trigger guards
UPDATE/DELETE, not INSERT). Import auto-enables dimensions_enabled with a
result-card notice (pre-authorized by the column comment). arcim-migration
provider syncs inherit all of it via the shared parser/importer.

Undo lockstep (migration 20260702154500): created_by_import_id provenance
on both registry tables (ON DELETE SET NULL); undo_sie_import deletes the
values/dimensions the undone import introduced when no remaining
posted/reversed line references them — user-created rows and rows other
bookkeeping references are untouched. The registry guard triggers act as
backstop. replace_sie_import deliberately skips the lockstep (re-import
re-upserts the same codes). Six pg-real tests cover the lockstep.

Round-trip pinned by test: parse → import state → export → parse preserves
declarations (#UNDERDIM parent links included), values, and per-line object
lists — including synthesis of referenced-but-undeclared values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dimensions): restate function-local statement_timeout on undo_sie_import

CREATE OR REPLACE resets proconfig, so the 290s timeout from 20260629160100
was silently dropped — regressing service-client bulk deletes to the
authenticator role's 8s limit. Caught by sie-import.replace.pg.test.ts in CI.
Full pg-real suite green (483/483, TZ=UTC).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dimensions): surface OIB/OUB drops and dimension presence as parse-level info (#866 review)

Dropping object-balance records must never be silent — one info issue counts
the skipped #OIB/#OUB rows (object-level balances are P&L-out-of-scope in
v1), and a second announces dimension data before the user executes the
import (the preview step renders parse issues), so the auto-enable notice is
no longer purely post-hoc.

Triage notes for the remaining findings: the RPC's opening SELECT is the
company-ownership check the swarm asked for; registry writes are RLS-bound;
line-verbatim codes are the documented legacy-free-text exception; export
emits no #KSUMMA so there is nothing to recompute; SIE dims 3–5 are
"reserved for future use" with no standard names, so generic synthesis is
spec-correct; ON DELETE SET NULL is deliberate — provenance is operational
metadata for undo, not räkenskapsinformation (the guarded journal lines
are), and RESTRICT would block legitimate post-retention housekeeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:05:13 +02:00
Mattsson f63d3e3100 Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects

We never sent auth_method to Enable Banking, so it fell back to the ASPSP's
visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate*
PSUs the redirect flow does not support Mobile BankID, so authorization failed
right after the user approved in the BankID app. Mobile BankID at Handelsbanken
is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses
when requested explicitly.

Resolve the bank's preferred auth method before /auth: query the ASPSP's
auth_methods and pick the DECOUPLED (Mobile BankID) method when present,
otherwise leave auth_method unset so banks that already work are untouched.
The method name is read dynamically per psu_type, so it is robust across
sandbox/production naming.

- api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods
  field name (was available_auth_methods, never populated), add
  getPreferredAuthMethod(), thread optional authMethod through startAuthorization
- index: resolve authMethod in /connect and pass it on both fresh + reconnect
- tests: cover method selection and request-body shaping

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

* refactor(invoice-inbox): clean up bulk-selection toolbar UI

Redesign the selection toolbar shown when inbox items are checked:
one solid primary "Bokför valda" button with outlined secondary
actions ("Fråga assistenten", "Ta bort") and a plain selection
count. Removes the redundant "Avmarkera" button (users uncheck the
still-visible box), fixes label clipping, and gives the toolbar more
breathing room.

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

* chore(entitlements): bypass paywall in local development

Add isPaywallBypassed() so all gated capabilities are testable locally
without a subscription. Fires only on NODE_ENV=development (npm run dev)
or an explicit DISABLE_PAYWALL=true escape hatch — production builds run
under NODE_ENV=production and the entitlement suite runs under 'test',
so both keep exercising the real gate.

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

* fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer

TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand.

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

* feat(transactions): implement categorize core for bank transaction categorization

- Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations.
- Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing.
- Implemented fiscal period validation and duplicate booking detection.
- Enhanced logging and error handling for transaction categorization.

feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata

- Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken.
- Outputs metadata for business and personal PSU types, including default authentication methods.

fix(migrations): increase statement timeout for SIE bulk delete operations

- Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports.

feat(migrations): add bulk book inbox items to pending operations

- Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`.
- Supports bulk booking of matched inbox items against bank transactions.

test(pg): add tests for replace_period_opening_balance_link RPC

- Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow.
- Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries.

* fix(sie-export): update journal entries and lines handling in SIE export tests

* fix(migrations): resolve version collision on 20260629160000

The SIE bulk-delete statement_timeout migration shared version
20260629160000 with journal_entries_list_series_filter (merged from
main via #798/#823), causing a schema_migrations_pkey duplicate key
error on apply. Rename the branch's migration to 20260629160100.

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

* fix(compliance): resolve compliance-swarm + review findings

- opening-balance/correct: compensating rollback for the non-atomic
  storno+rebook so a mid-sequence failure never leaves two posted OB
  entries (ASVS V2.3); durable audit event on every failure path
  (V16); reference the original verifikationsnummer in the corrected
  entry per BFL 5 kap 5§; document that requireWrite already enforces
  write-role + membership (V8.2.1 was a false positive)
- reports sources routes: validate the cursor date component as ISO
  (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2),
  applied to both the VAT-declaration and trial-balance routes
- AgentSessionList: await the rename PATCH, revert the optimistic
  title and toast on failure (ASVS V4.5)
- bank booking: exclude same-batch siblings from the booking-time
  duplicate guard so bulk-booking distinct same-(date,amount)
  transactions no longer false-positives; pre-existing duplicate
  detection is preserved
- BulkBookInboxDialog: drop the unsafe currency-based reverse_charge
  default, add an omvänd skattskyldighet advisory, and type VAT
  options to the backend VatTreatment union
- OpeningBalanceRowEditor: hold onChange in a ref (synced in effect,
  not during render) so an unstable callback can't cause a render loop

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:13:00 +02:00
Jakob Wennberg 2da9c71eb3 UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns:

- style(ui): badge audit + cleanup across 45 files — real-status chips use Badge variants (raw Tailwind colors dropped), non-status count/type/label chips demoted to muted text, clustered badges consolidated; 20 unused imports removed.
- feat(bookkeeping): Kontoplan moved to a dedicated /chart-of-accounts route (nav + command palette wired); /bookkeeping shows the journal list only.
- chore(skills): loop-* automation skills + design-scan workflow under .claude/.

fix(reports): restored the destructive count badge on blocking errors in the periodisk sammanställning (EC Sales List) — a genuine status cue the audit had wrongly flattened; flagged by the PR reviewer and Swedish compliance bot, now clean.

All CI green; compliance bots report no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:29:23 +02:00
Jakob Wennberg b800dcd403 style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup

Multi-agent scan of all 404 UI files against the locked design system, then
141 verified surgical fixes across 109 files (net -32 lines):

- Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale
  bounce, hover:shadow on list items, transition-all -> transition-colors.
- Drop font-medium from single-weight Hedvig display headings/numerals.
- Replace raw rainbow Tailwind status colors with Badge variants / brand tokens /
  neutral surfaces (achromatic chrome, semantic colors stay data-only).
- Route raw dates through formatDate(), hand-rolled currency through formatCurrency(),
  add tabular-nums to financial figures; text-gray-* -> text-foreground tokens.
- Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale.
- Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total,
  collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake),
  correct mismatch-password toast, correct supplier currency-field label.
- Remove PII-leaking debug console.log on register, stray console.logs.

Verified: tsc clean on all changed files, eslint clean, production build passes.

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

* fix(auth): sanitize residual error logs in register flow

Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the
remaining console.error calls in the register flow passed raw error
objects, which Supabase may populate with PII (email) in nested fields.
Log only sanitized message strings instead.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:14:13 +02:00
Jakob Wennberg 2d6ddeafc5 feat(import/export): article import + register export (xlsx/csv) (#750)
* feat(import/export): article import + register export (xlsx/csv)

Add CSV/Excel import for the article register (artiklar), mirroring the
existing customer/supplier import pipeline, plus Excel + CSV export for
articles, customers and suppliers.

Import (lib/import/articles + app/api/import/articles):
- Column auto-detection tuned to Fortnox/Visma/Bokio export headers,
  Swedish-decimal price parsing, VAT snapped to {0,6,12,25}, type/unit
  normalization.
- Dedup by article number then name; 23505 soft-skip; auto-number
  backfill; revenue-account override kept only when active, otherwise
  dropped with a warning (never mutates the chart of accounts).
- New "Artiklar" flow in the /import hub.

Export (app/api/export/* + lib/export/register-export):
- Read-only xlsx (default) / csv (?format=csv, UTF-8 BOM) downloads.
- Headers chosen so files round-trip back through the importer.
- "Exportera" menu added to the articles, customers and suppliers pages.

Refs #746. Direct Fortnox/Visma API article fetch tracked in #749.

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

* fix(import/export): address PR review — lint ratchet + export hardening

- xlsx-export: keep `SheetSpec<any>` on the eslint-disabled line (fixes the
  core-only lint ratchet regression: no-explicit-any 16 -> 15) and define
  UTF8_BOM as an explicit `` escape instead of a raw BOM character.
- export routes (articles/customers/suppliers): move the data queries inside
  the try/catch, add `Cache-Control: no-store`, and emit a `register exported`
  audit log line (entity, format, rowCount).
- articles parse route: validate `column_overrides` against a Zod schema before
  trusting it to drive the parser.

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

* fix(import): drop öre-round pattern on article column-detector confidence

The confidence score is a 0-1 heuristic, not money, and is only compared
against the 0.8 skip-mapping threshold. Removing the Math.round(x*100)/100
form clears the core-only antipattern ratchet (naive-ore-round 660 -> 659).

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

* feat(import): flag adjusted VAT rows in the article import edit step

Surface VAT snapping/defaulting per row, not just as a file-level warning:
the parser sets `vat_rate_adjusted`, the edit step highlights those rows'
VAT selector and shows a count banner, and confirming a rate clears the flag.
Addresses the Swedish-compliance review note that silent snapping could
otherwise store a wrong VAT rate at scale.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:38:44 +02:00
Jakob Wennberg 4dfd790de5 feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions (#698)
* feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions

Verifikat UX
- "Ny verifikat" opens in a modal (NewJournalEntryDialog) instead of an inline tab;
  the review step renders inline in the dialog rather than stacking a second dialog.
- JournalEntryForm: konteringsrader are the focus, with a compact pre-filled metadata
  bar (datum/serie/text/valuta/period) on top; verifikationstext auto-fills from the
  first row's account.
- JournalEntryList: belopp shown on collapsed rows; expanded view is an aligned
  Konto/Benämning/Debet/Kredit table.

SIE imports no longer flood "Att hantera: saknade underlag"
- Import gains an opt-in (off by default) toggle to mark imported verifikat as "Inget
  underlag krävs"; a "Rekommenderas vid migrering" badge nudges it for historical years.
- Multi-select batch-mark in the list for selective cleanup.
- Filter-scoped bulk mark (POST /api/bookkeeping/no-doc-required/bulk-missing): marks
  every missing-doc verifikat matching the active filters across all pages, with a
  dry_run count to confirm scope — the scalable remedy for a post-import flood.
- Shared helper markEntriesNoDocRequired + per-entry batch route.

Tests: no-doc helper, batch route, bulk-missing route.

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

* fix(bookkeeping): address PR #698 review findings

- JournalEntryForm: restore the explicit "no underlag" acknowledgement in the
  modal's inline review. When no document is attached, the confirm button reads
  "Bokför utan underlag" (BFL 5 kap 6-7 §§), equivalent to the blocking dialog the
  non-bare flow shows — the bare path no longer posts behind only a passive banner.
- batch no-doc route: guard the ownership query with source_type IN
  NEEDS_DOC_SOURCE_TYPES so a crafted request can't exempt non-document-requiring
  entries (defense in depth on top of company + posted scoping).
- bulk-missing route: resolve doc/exemption status by querying only the candidate
  ids (chunked) instead of loading the company's full document_attachments and
  journal_entry_no_doc_required tables into memory — data minimisation + bounded
  memory for large migrations (the most-repeated reviewer finding).

Triaged as non-issues (left as-is): partial-import exemption (gated on
result.success == zero errors), reason write-back (sidecar row is FK-linked and
carries the reason), and "bulk-exempting manual entries" (consistent with the
existing per-entry NoDocRequiredToggle). No DB migration — reuses the existing
journal_entry_no_doc_required table.

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

* fix(bookkeeping): centralize bulk-missing date/series validation in Zod

Move the ISO-date and verifikationsserie format checks into the Zod schema so
malformed input is rejected with a clean 400 instead of being silently nulled
(or, for a shaped-but-invalid date, throwing a 500 via fetchAllRows). The date
refinement rejects values like 9999-99-99 / 2026-02-30 that a bare
/^\d{4}-\d{2}-\d{2}$/ regex lets through. Addresses the PR #698 reviewer nit on
split schema-vs-runtime validation. +2 route tests.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:41:30 +02:00
Mattsson 0ca9c25aba Add/user feedback (#679)
* feat(bookkeeping): make blocked fiscal-year creation actionable

When creating a new räkenskapsår is blocked because a prior period is
still open, the "Skapa räkenskapsår" dialog no longer dead-ends on an
English toast. The API now returns the canonical bilingual error envelope
with the blocking periods (id/name/dates) under details, and the dialog
renders a Swedish panel that locks them inline (reversible locked_at) via
the existing /lock endpoint and retries creation.

The guard rule is unchanged and remains BFL-compliant: BFL 6 kap allows
löpande bokföring of the new year in parallel with the prior year's
bokslut, so a lock (not a full close) is sufficient and reversible.

- Add PERIOD_CREATE_BLOCKED_BY_OPEN_PERIODS structured error code
- Return envelope + details.blockingPeriods from the 409 (was English string)
- CreatePeriodDialog: inline "lås och skapa" panel + lock-and-retry
- Update route tests for the new envelope shape

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

* fix(ui): prevent mouse wheel from mutating number inputs

A focused <input type="number"> would change its value on scroll,
silently turning e.g. a 20000 salary into 19998. Blur number inputs
on wheel so the page scrolls instead of editing the value. Applied
at the Input primitive so all number fields are protected.

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

* feat(salary): auto-derive skattetabell and kolumn for employees

Replace the opaque manual "Skattetabell (29-42)" and "Kolumn (1-6)" inputs
on the employee form with a self-deriving flow: the user picks their
folkbokföringskommun from a searchable dropdown and the tax table fills
itself in, while the column derives from the personnummer we already collect.

- Add a searchable municipality picker (MunicipalityCombobox) backed by a
  new cached GET /api/salary/tax-tables/kommuner endpoint.
- Wrap the whole "Skatt" card in a self-contained EmployeeTaxCard used by
  both the create and edit pages, with InfoTooltips and named column options.
- deriveTaxColumn(): auto-select column 1 for under-66 employees; leave the
  ambiguous 66+ case (pension vs working senior) to a clearly-named manual
  choice.
- Fix fetchKommunTaxRates() to page through all ~1300 församling rows instead
  of a single 500-row page (which silently dropped ~200 kommuner, incl.
  Göteborg) and normalize the uppercase names to title case.

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

* fix(import): correct CSV amount-column guess and surface skipped rows

Manual CSV column-mapping auto-guess walked each data row right-to-left
and picked the first numeric cell as the amount, so on the common
...;Belopp;Saldo layout it grabbed the trailing running-balance column.
Extract the guess into a pure, tested suggestColumnMapping(): match
header labels first (belopp/amount -> amount, saldo/balance -> balance),
auto-fill the balance field, and fall back to value heuristics that skip
the balance column and prefer a column carrying negative values.

Also surface stats.skipped_rows + parse warnings in BankFileConfirmStep -
the manual-mapping path skips the preview step that was the only place
they showed, so skipped rows were silently dropped from view.

Add a unit test reproducing the Saldo-as-amount regression.

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

* feat: add "Save as draft" functionality for invoices

- Implemented a new feature to allow users to save invoices as unnumbered drafts without generating an invoice number until finalized.
- Added a `save_as_draft` flag to the CreateInvoiceInput schema to handle draft saving logic.
- Updated the invoice creation API to skip number allocation when saving as a draft.
- Introduced a new endpoint for finalizing drafts, which allocates an invoice number and emits an `invoice.created` event.
- Enhanced the UI to include a "Save as draft" button, with loading states and tooltips.
- Updated tests to cover the new draft saving and finalization logic, including race conditions for concurrent modifications.
- Added relevant error handling for draft finalization and deletion scenarios.

* feat(employee): add employment start and end date fields to employee forms

* feat: enhance invoice and salary run handling with improved validation and event logging

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:26:40 +02:00
Jakob Wennberg f7cd1b86e7 fix(import): preserve customized SIE #KONTO account names (#669)
* feat(import): add syncMappedAccounts helper for account create + rename

Single home for the create-missing-accounts logic that exists in three
near-identical copies (executeSIEImport, the SIE execute route, and the
arcim-migration extension), plus a new rename pass that carries customized
SIE #KONTO names into accounts that already exist (e.g. K1-seeded defaults).

The file's name applies only to identity mappings (source === target);
remapped targets keep their BAS/current name. With updateAccountNames=false
the behavior matches the legacy code exactly. Not wired up yet.

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

* fix(import): preserve SIE #KONTO account names; add updateAccountNames option

Customer report: account names customized in Fortnox did not follow into
Accounted via SIE import. The import always used BAS default names for
accounts in the BAS reference and never touched accounts that already
existed (the K1-seeded chart), so the file's names were silently dropped.

executeSIEImport now routes account creation through syncMappedAccounts,
which prefers the file's #KONTO name for identity-mapped accounts and
renames existing accounts whose name differs (surfaced as a warning).
New option updateAccountNames (default true) restores the old behavior
when disabled. The duplicated pre-create blocks in the execute route and
the arcim-migration extension are removed — executeSIEImport owns account
sync on every path now, including the Fortnox re-sync (idempotent renames).

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

* feat(mcp): expose update_account_names on gnubok_import_sie

Optional boolean on the tool schema, staged into the pending operation and
threaded through commitImportSie to executeSIEImport. Defaults to true at
both stage and commit time — the commit-side default also covers operations
staged before the param existed (Boolean(undefined) would have silently
flipped it off).

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

* fix(api): v1 SIE import generated no account mappings

The route passed [] as mappings to executeSIEImport, which the
mapping-coverage guard (added in #613) rejects for any real file — and
before that guard, every voucher was silently skipped as unmapped. The
route has never produced a working import for files with vouchers.

Generate mappings server-side from the file's #KONTO records plus stored
per-company overrides (same as the dashboard execute route), reject
unmappable files with a clean 400 before the operation row is created,
and expose options.updateAccountNames (default true).

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

* feat(import): "Använd kontonamn från filen" toggle in import review step

New switch (default on) controlling whether the SIE file's #KONTO names
are carried into the chart of accounts. Helper text shows how many
identity-mapped accounts carry names that differ from the BAS defaults.
The page already serializes the whole options object to the execute
route, so no further wiring is needed.

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

* fix(import): address PR #669 review — parallel renames, rename audit trail

- Rename pass now runs UPDATEs concurrently in bounded batches of 25
  (greptile P2): a re-sync with many custom names no longer serializes
  N round trips, and a pathological full-chart rename cannot stampede
  the API. Per-rename failures stay non-fatal via Promise.allSettled.
- Persist the per-account rename detail (number, from, to) into
  sie_imports.migration_documentation as accountRenames — the
  behandlingshistorik record per BFNAR 2013:2 (swedish-compliance
  review); the result warnings only carry the count.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:35:49 +02:00
Mattsson 3e42fc6f32 Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality

* feat: implement self-billing invoice functionality

- Added support for registering self-billed invoices received from customers.
- Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`.
- Created API route for handling self-billed invoice submissions, including validation and error handling.
- Implemented database migrations to add necessary columns and constraints for self-billing invoices.
- Developed tests to ensure correct behavior of self-billing invoice creation and validation rules.
- Updated Swedish localization files to include new terms related to self-billing.

* feat: enforce SIE import requirement for non-Fortnox providers in migration process

* feat: streamline invoice processing and enhance error logging across APIs
2026-06-04 13:14:57 +02:00
Mattsson c6c86cded4 Mcp/template data feedback (#617)
* fix(booking-templates): scope template list to the active company

GET /api/settings/booking-templates relied solely on the btl_select RLS
policy, which is membership-wide (user_company_ids) and returns templates
from every company the user belongs to. A user who owns multiple companies
saw all their templates merged regardless of which company was active.

Narrow the list in the API layer (mirroring counterparty-templates) to
system + the active company + the active company's team. RLS stays the
security backstop; this fixes the cross-company merge within a single
user's own view (it was never a cross-tenant data leak).

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

* fix(import): show proper message for duplicate bank file upload

The bank file import page mis-parsed the structured error envelope
({ error: { code, message, details } }), so a BANK_FILE_DUPLICATE
(409) fell through to the generic "Kunde inte läsa filen" fallback.
The upload step also hardcoded that same string as the error heading,
so duplicates were doubly misreported as parse failures.

- Parse the structured envelope by error.code; surface error.message
  for all codes instead of rendering the error object.
- Add a dedicated BANK_FILE_DUPLICATE message using the importedAt /
  importedCount details the route already returns.
- Add an optional errorTitle prop to BankFileUploadStep (defaults to
  the previous text) and pass "Filen är redan importerad" for dupes.

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

* feat(tests): add comprehensive tests for recordateEntry, inbox-linking, and external-id handling

- Implemented unit tests for recordateEntry in the bookkeeping module to validate various scenarios including date changes, non-posted entries, and fiscal period restrictions.
- Created tests for inbox-linking status in pending operations to ensure correct handling of invoice inbox items and supplier invoices, addressing historical bugs related to status updates.
- Added tests for external-id utilities to ensure consistent handling of monetary amounts and deduplication keys across different transaction sources.
- Introduced new functions in external-id.ts for stable external ID generation and normalization of imported descriptions, enhancing transaction deduplication reliability.

feat(migrations): add new database migrations for transaction handling

- Created migration to exclude storno and correction vouchers from unmatched GL lines, ensuring accurate reconciliation.
- Added a migration to preserve original bank transaction descriptions in a new immutable column, allowing for user edits while maintaining audit trails and deduplication integrity.

* feat(migrations): add function to exclude storno/correction vouchers from unmatched GL lines

* feat(transactions): enhance transaction handling with improved description normalization and preloaded original entries

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:45:49 +02:00
Jakob Wennberg ccdfed5fea feat: voucher linking, recovery ops, and salary overrides (#591)
* feat: voucher linking, recovery ops, and salary overrides

Adds reversible/correction-style write paths that customers and agents have
been asking for, plus per-run salary employee overrides.

Invoice → voucher linking
- POST /api/invoices/[id]/link-to-voucher and
  GET /api/invoices/[id]/voucher-candidates
- lib/invoices/voucher-matching.ts with full + pg test coverage
- LinkVoucherPicker UI in PaymentBookingDialog
- pending_operations.operation_type expanded with link_invoice_voucher
  (medium risk) and a (journal_entry_id, invoice_id) unique guard
- MCP: gnubok_find_voucher_candidates_for_invoice and
  gnubok_link_invoice_to_voucher tools

SIE undo
- POST /api/import/sie/[id]/undo + undo_sie_import RPC
- sie_imports.status gains 'undone'
- ImportResultStep surfaces the action; structured error SIE_UNDO_FAILED

Edit-recreate journal entries
- POST /api/bookkeeping/journal-entries/[id]/edit-recreate
- Bookkeeping detail page wires it into the existing edit flow

Delete-last-voucher clears IB link
- Trigger + pg test ensure deleting the last voucher of a period nulls the
  opening_balance_journal_entry_id link so a re-import lands cleanly

Salary employee overrides
- salary_run_employees gains per-run override fields + migration
- lib/salary/effective-values.ts centralises resolved values; all payslip,
  payment, AGI, KU, and booking routes read through it
- SalaryOverridePanel on the employee detail page

Account classifier
- lib/bookkeeping/account-classifier.ts + tests; AddAccountDialog uses it
- backfill-import-accounts script updated

Misc
- toast: minor styling tweak
- AGI generate-declaration: respect effective values
- structured-errors: new LINK_INVOICE_VOUCHER namespace

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

* feat: add link_invoice_voucher operation type to pending_operations

* feat: refactor salary run calculations and update error handling for SIE imports

* fix: PR review feedback on voucher linking and SIE recovery

pg-real (blocking):
- tests/pg/delete-last-voucher-ib: drop posted_at = now() from the seed
  UPDATE — journal_entries has no posted_at column.
- lib/invoices/__tests__/voucher-matching.pg: seed the posted voucher
  before closing the fiscal period so enforce_period_lock doesn't block
  the INSERT during setup.

voucher-matching error codes and rollback:
- Add LINK_VOUCHER_DB_ERROR (HTTP 500) and return it on real invoice
  UPDATE / payment INSERT failures. Previously these returned
  LINK_VOUCHER_VOUCHER_NOT_FOUND (404) which the pending-op dispatcher
  auto-rejects on transient DB errors.
- Log rollback failures explicitly so an invoice left in a half-linked
  state (advanced status, no payment row) surfaces for manual
  reconciliation instead of disappearing silently.

resyncNextPeriodOpeningBalance ordering:
- Create the new IB first, relink the period FK, then storno the old IB.
  Previously the storno ran first; if createJournalEntry failed the next
  period was left with a reversed IB and nothing to replace it, and
  executeSIEImport swallows the error as a non-fatal warning.

replace_period_opening_balance_link:
- Tighten role check to owner/admin (was owner/admin/member). Matches
  delete_last_voucher and undo_sie_import.

Data minimisation:
- /api/invoices/[id]/voucher-candidates and the matching MCP tools now
  project only the invoice and customer fields the matcher reads, instead
  of returning the full customer row.

Schema bounds:
- SalaryEmployeeOverrideSchema caps each numeric override at 10 MSEK to
  catch typos before they reach the ledger or AGI.

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

* fix(tests): supply user_id when seeding voucher_sequences

voucher_sequences.user_id is NOT NULL (per the multi-tenant refactor in
20260330130000). The previous test seed only set company_id /
fiscal_period_id / voucher_series, which made the seed fail with a
constraint violation on the latest pg-real run. Pass the same userId
used elsewhere in the seed helper.

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

* fix(tests): scope delete-last-voucher RPC assertions inside the tx

withUserContext always ROLLBACKs, so any DELETE the RPC performs is
discarded when the callback returns. The previous test then queried
journal_entries via a fresh getPool() connection that only saw the
pre-RPC committed seed state — hence "expected '1' to be '0'".

Move every post-RPC assertion (entry count, period FK clear,
opening_balances_set flip, audit log entry, sie_imports clear) inside
the same withUserContext callback so they observe the uncommitted state
before ROLLBACK fires.

Also fix the sie_imports INSERT: the column is `filename`, not
`file_name`, and `sie_type` is NOT NULL.

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

* fix(tests): assert against the IB-marker audit row directly

DELETE on journal_entries fires two audit_log writes: the generic
write_audit_log() trigger row ("Deleted journal_entries record") and the
delete_last_voucher RPC's explicit "(was period IB)" entry. Both land
at the same statement_timestamp(), so ORDER BY created_at DESC LIMIT 1
returned the trigger row non-deterministically in CI.

Switch to a presence check with a LIKE filter on the IB marker so the
test verifies what it actually cares about — that the RPC's IB-aware
audit row exists.

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

* fix(db): set company_id on delete_last_voucher audit_log rows

20260528120000_delete_last_voucher_clears_ib_link.sql inserts directly
into audit_log without setting company_id. audit_log's SELECT policy
filters company_id IN user_company_ids(), so those rows landed with
company_id=NULL and were invisible to every reader — only the generic
write_audit_log() trigger row remained visible. That broke BFL audit-
trail intent: the "(was period IB)" provenance row was never readable.

Republish delete_last_voucher with p_company_id populated on both
audit_log INSERTs (draft path and posted path). Behavior is otherwise
unchanged; the pg-real test for the IB-clear flow now sees the
RPC-written marker row as expected.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
2026-05-28 21:09:43 +02:00
Jakob Wennberg 39204cc0de UX polish bundle: Enable Banking lookback + sync progress, invoice inbox, matching previews (#548)
* fix(import): dedup opening-balance rows when account numbers differ only in whitespace

The parser's merge map keyed on the post-strip account_number, but rows like
"1930", " 1930 " and "1.930" could leak as separate entries when the
upstream string contained non-breaking spaces or zero-width chars that the
old .replace(/[^0-9]/g, '') ran on already-stripped output. Strip those
explicitly in the raw string and use /\D/g for the digit extraction.

Also adds defense-in-depth dedup inside OpeningBalanceEditStep so any
duplicates that survive the parser collapse before the user sees them.

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

* feat(enable-banking): anchor lookback picker to fiscal year, not days

Replaces the 90/180/365 days dropdown on the account-selection screen
with three explicit modes:

- "Senaste 90 dagar (snabbt)" — fastest path, matches PSD2 ceiling
- "Sedan räkenskapsårets början" (default) — resolves via
  fiscal_year_start_month, surfaces the literal date inline
- "Anpassat datum" — free date picker OR "Föregående räkenskapsårets start"

When the resulting range exceeds 90 days, the picker now surfaces a
quiet helper that points users at the SIE/bankfil import for older
history, so they don't waste an account-selection round-trip discovering
that banks usually cap at ~90 days.

The PATCH /accounts handler accepts initial_lookback_from_date alongside
initial_lookback_days; the new helper getCurrentFiscalYearStart() in
lib/company/fiscal-year.ts is reused.

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

* feat(enable-banking): dedicated sync progress modal replaces silent spinner

After the user confirms account selection, transactions fetch in the
background for 30–60 seconds. Previously this showed only the Spara-button
spinner with no indication of duration or what was happening — users
described being stuck on the page.

The new BankSyncProgressDialog opens immediately on Save, lists the enabled
accounts being synced, and disables manual close until the PATCH resolves.
On completion it shows the imported count and the actual date range the
bank returned, plus an amber escape hatch to SIE/bankfil import when the
returned range was truncated by >7 days from what was requested.

Failure path surfaces in the same modal rather than as a destructive toast
that disappears.

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

* refactor(invoice-inbox): drop duplicate Skapa leverantör button

The inbox detail panel had its own supplier-creation button that fired
/api/suppliers + match-supplier. The same action is reachable from the
supplier-invoice form's "Skapa & välj" card (showAISupplierHint), which
also prefills more fields. The duplicate button is gone; a quiet inline
hint replaces it so the user still knows why no supplier matched.

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

* ui(invoice-inbox): surface currency and totals above the long metadata tail

Move Valuta / Totalt / Moms in FIELD_DEFS so they sit immediately under
Leverantör / Org.nr / VAT-nr. These are the fields the user reads first
when triaging an inbox item; burying them after nine metadata fields
forces unnecessary scrolling on every single invoice.

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

* feat(invoice-inbox): accept .eml forwards and log rejected attachments

Gmail's "Forward as attachment" packages the original email as message/rfc822,
which our MIME allowlist silently dropped. Adds mailparser so we can unwrap
the inner attachments and ingest them under the inner email's subject/from.

Also persists every rejected attachment as an invoice_inbox_items row with
status='error', so users can see what was dropped instead of guessing why
nothing showed up in their inbox.

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

* fix(supplier-invoices): redirect back to inbox after creating from invoice-inbox

When the leverantörsfaktura form is opened from an invoice-inbox item, every
successful create previously kicked the user out to /supplier-invoices or the
just-created invoice's detail page — derailing the "process the next
document" workflow. The Tillbaka button likewise routed to the
supplier-invoice list rather than the inbox they came from.

Adds an afterCreate helper that lands inbox-originated submissions at
/e/general/invoice-inbox and preserves the original target everywhere else.

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

* ui(pending): show transaction/document context for match-and-attach reviews

The granskning page previously rendered attach_document_to_transaction and
match_transaction_invoice operations through the generic key/value preview,
so reviewers saw "document file name: Faktura.pdf / transaction amount:
-216 USD" without any visual indication of which two things were being
paired. The MCP tool already returns enriched preview data; we just
needed dedicated layouts.

Adds:
- AttachDocumentPreview — two-card layout (Transaktion | Dokument) with
  a "Visa dokument" button that fetches a signed download URL on demand
- MatchTransactionInvoicePreview — same layout (Transaktion | Faktura)
- DocumentViewButton — reusable signed-URL opener

Also tightens the matching tools' descriptions so AI clients are nudged
to verify human-readable context before staging.

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

* fix(review): address PR #548 review feedback

- invoice-inbox: hoist mailparser to a static import. The extension system
  generates a static import tree via setup:extensions and disallows dynamic
  imports — await import('mailparser') worked in dev but could fail in
  production standalone builds.
- enable-banking AccountPickerDialog: guard the Save path when "Anpassat
  datum" + "Specifikt datum" is selected with an empty date. Without this,
  lookback.body resolves to null and the PATCH silently falls back to the
  backend's 120-day default, ignoring the user's intent.
- enable-banking BankSyncProgressDialog: drop the empty-body useEffect.
  Close-prevention is already handled inline via the onOpenChange guard +
  onPointerDownOutside + onEscapeKeyDown handlers.
- lib/company/fiscal-year: pin both operands of daysBetween() to UTC when
  parsing ISO date strings. Mixing a UTC-parsed date with new Date() (local
  time) drifts by one day in any timezone east of UTC.

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

* fix(review): address compliance swarm + Swedish review feedback

Three actionable items from the post-fix compliance scan; the rest were
false positives or out of scope.

- enable-banking PATCH /accounts: reject future initial_lookback_from_date
  with 400 instead of silently falling through to the 120-day default.
  Compliance V2.2.
- AttachDocumentPreview: promote the overwrite warning to a destructive
  banner with BFL 7 kap context when the existing document is marked as
  räkenskapsinformation. A muted footnote was too easy to skip past for
  a verifikationsunderlag replacement.
- MatchTransactionInvoicePreview: surface transaction_date + invoice_date
  in the staged preview so reviewers can spot date drift before approving
  (BFL 5 kap 6§ — verifikation date must align with affärshändelse). Also
  shows a quiet hint when the two dates differ by > 31 days. Tool's SELECT
  + stage payload extended accordingly.

Skipped (with rationale):
- V5.3 inner.filename path traversal — lib/core/documents/document-service.ts
  already sanitizes filenames before constructing storage paths.
- V5.2 magic-number MIME — pre-existing pattern for all email attachments;
  scope is codebase-wide.
- V1.2 att.id composite ID — only used as a DB column value, never a path.
- V13.1 / CM-8 SBOM/SCA — repository-wide policy, not this PR.

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

* fix(review): address second-round compliance + Swedish review feedback

Compliance Swarm (defense-in-depth + valid finds):
- invoice-inbox: sanitise .eml inner attachment filenames and content-types
  before they flow into uploadAndExtract or the raw_email_payload JSONB.
  document-service already strips bad chars before constructing storage
  paths, but the swarm flagged the upstream input as unsanitised — easier
  to add a thin sanitiseFilename/sanitiseMime layer than to argue about
  defense-in-depth. Caps lengths too.
- DocumentViewButton: validate documentId as a UUID before interpolating
  into /api/documents/:id — staged preview_data is Record<string, unknown>
  on the wire, so refusing junk early gives a clearer error and keeps the
  internal API from seeing oddly-shaped path segments. (Compliance V1.2.)

Swedish review:
- MatchTransactionInvoicePreview: drop the BFL 5 kap 6§ citation from the
  date-drift hint — that section governs verifikationsinnehåll, not a 31-day
  tolerance. The hint stays (the practical concern is real) but no longer
  pretends to quote a legislated threshold.
- fiscal-year: document the implicit assumption that entity_type reflects
  the company's current tax-year status, not a mid-conversion state.

Skipped (with rationale):
- V5.2 magic-number MIME — pre-existing pattern across all email attachments.
- A.8.12 signed URL via window.open — pre-existing pattern shared with
  JournalEntryAttachments.tsx; refactor to server-side redirect is broader scope.
- A.8.15 logRejection failure path — pre-existing console.error pattern.
- CC9.2 mailparser vendor review / SBOM — out of PR scope.
- CC6.1 IDOR — /api/documents/:id already enforces company_id; false positive.
- Swedish #1 räkenskapsinformation flag origin — server-side already derives
  the flag from document_attachments.journal_entry_id in the staging tool;
  not caller-trusted.

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

* fix(review): fail-safe BFL warning + preserve merge validation errors

Two findings from the third compliance pass; both worth addressing.

- AttachDocumentPreview: treat an absent
  existing_document_is_rakenskapsinformation flag as räkenskapsinformation
  rather than as "safe to overwrite". The MCP staging tool sets the flag
  deterministically from document_attachments.journal_entry_id today, but
  a future code path that forgets it would silently downgrade the BFL 7
  kap warning. Only an explicit `=== false` from the server keeps the
  muted note path.
- Opening-balance merge: union validation_errors when collapsing duplicate
  account_number rows, both in the parser and the EditStep useState
  initializer. Previously a warning that fired on row 5 (e.g. BAS-class
  mismatch) was silently dropped if row 2 of the same account had no error,
  risking misclassified IB data downstream. Added a parser test covering
  the union behaviour for two rows of a class-3 (resultatkonto) account.

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-21 12:10:12 +02:00
Mattsson 566ed72984 Bug/mcp connection issue (#541)
* feat(api): implement caching and logging in health check endpoint

- Added in-memory caching for health check responses to reduce load on Postgres.
- Introduced logging for error handling in health check.
- Updated response structure to exclude error details from public responses.

feat(api): enhance OAuth consent UI and scope handling

- Improved consent UI to reflect exact requested scopes and added better user guidance.
- Updated scope handling logic to ensure least-privilege access.
- Enhanced styling for better user experience and accessibility.

chore(docker): improve security and resource management in Docker setup

- Updated Docker Compose configuration to enforce read-only file systems and resource limits.
- Added health checks and logging options for better observability.
- Introduced optional Caddy reverse proxy for TLS termination.

fix(migrations): resolve ambiguity in create_company_with_owner function

- Dropped orphaned 3-arg overload of create_company_with_owner function.
- Recreated canonical 4-arg version with cash account seeding logic.
- Ensured proper permissions for function execution in Postgres.

* feat: enhance security checks for team membership in company creation

* test: add CSP tests for OAuth authorization endpoint

* feat: enhance error handling and reporting in bank file import process
2026-05-20 10:08:32 +02:00
Jakob Wennberg a9c98da243 feat(api): Phase 3 — transactions + reconciliation vertical (#464)
* feat(api): Phase 3 — transactions + reconciliation vertical

Closes out Phase 3 of the plan in one PR. After this, a 3rd-party agent
can fully manage a company's transaction ledger via the public API:
import bank data, walk the queue, categorize (manual / template /
counterparty / account-override), match payments to customer + supplier
invoices, reverse mistakes, and auto-reconcile the bank against the GL.

ENDPOINTS (12)

Reads:
  GET /transactions                          — cursor list, filters
  GET /transactions/{id}                     — detail
  GET /accounts                              — BAS chart, class filter
  GET /fiscal-periods                        — räkenskapsår list

Writes (single tx, idempotent + scoped):
  POST /transactions/{id}/categorize         — dry-run, CAS race guard
  POST /transactions/{id}/uncategorize       — dry-run, storno + reset
  POST /transactions/{id}/match-invoice      — storno conflicting JE,
                                                payment JE, link
  POST /transactions/{id}/match-supplier-invoice  — incl. FX diff handling

Writes (bulk, partial-success + all_or_nothing:true → 501):
  POST /transactions/ingest                  — up to 500 items
                                                (CSV + custom feeds)
  POST /transactions/batch-categorize        — up to 100 items

Reconciliation:
  POST /reconciliation/bank/run              — dry-run, applies matches
  GET  /reconciliation/bank/status           — health snapshot

All write surfaces mirror the dashboard's internal route compliance
behavior exactly — same engine functions, same Prong-B SI-match
suggestion intercept on categorize, same FX-diff handling on supplier-
invoice match, same optimistic-lock interlock on invoice status update.
No new bookkeeping primitives — every route delegates to the existing
`lib/bookkeeping/*` engine, `lib/transactions/ingest.ts`, and
`lib/reconciliation/bank-reconciliation.ts`.

SCOPES + ERRORS

Adds 12 entries to lib/auth/scopes.ts under transactions:read|write +
reports:read (accounts, fiscal-periods follow the same convention as
MCP tools). Adds 4 new error codes: TX_UNCATEGORIZE_NOT_BOOKED,
TX_UNCATEGORIZE_JE_NOT_POSTED, TX_INGEST_INSERT_FAILED,
TX_BATCH_CATEGORIZE_EMPTY.

TESTS

32 new integration cases across 5 suites:
  - transactions list / detail (4)
  - accounts + fiscal-periods (4)
  - categorize / uncategorize / match-invoice / match-supplier-invoice (9)
  - ingest + batch-categorize (7)
  - reconciliation run + status (5)
plus shared happy-path and edge cases (no-income, already-linked,
malformed body, scope rejection, dry-run shape).

Full suite green: 3270 passing (234 files). Build + lint clean.

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

* fix(api): address PR #464 review — Phase 3 hardening

Greptile P1 — cursor pagination broken in GET /transactions.
  encodeDefaultCursor was passed the YYYY-MM-DD `date` field, but
  decodeDefaultCursor's strict ISO 8601 timestamp regex rejected it,
  so every cursor decoded as null and the endpoint always returned the
  first page. Switched the cursor anchor to `created_at` (real ISO
  timestamp, total-orderable, unique within the company at the row
  insertion grain) and updated the sort to (created_at DESC, id ASC).
  The `date` column remains in every row + filterable via ?date_from /
  ?date_to. Updated the registry description to reflect the change.

Greptile P1 — JE soft-fall in match-invoice + match-supplier-invoice.
  When the payment journal entry creation threw (any non-
  AccountsNotInChartError), the catch block recorded the error string
  but execution CONTINUED, marking the invoice paid + inserting a
  payment row + linking the transaction with no GL entry. The dashboard
  internal route soft-fails here intentionally and surfaces a banner so
  the user can re-book; for the v1 surface a partial state is strictly
  worse than a clean failure to retry. Both routes now return:
    - INVOICE_PAID_BOOK_FAILED (match-invoice)
    - MATCH_SI_RECORD_PAYMENT_FAILED (match-supplier-invoice)
  before any state mutation. Removed `journal_entry_error` from both
  response schemas — strict mode means it can never be set on a 200.

Greptile P1 — `overdue` supplier invoices fail the optimistic lock.
  The early status guard accepted `overdue` as matchable, but the
  downstream `.in('status', ['registered', 'approved', 'partially_paid'])`
  excluded it, returning MATCH_SI_NOT_OPEN for a legitimately payable
  invoice. Added `overdue` to the optimistic-lock list.

Greptile P1 + Swedish-compliance — CAS-race orphan cancellation.
  Direct `.update({ status: 'cancelled' })` on the orphaned JE was
  silently blocked by enforce_journal_entry_immutability (the engine
  writes JEs as posted) and the `voucher_gap_explanations` row claimed
  the entry was cancelled when it wasn't. BFL 5 kap 5 § requires
  corrections via a reversing entry. Both /transactions/{id}/categorize
  and /transactions/batch-categorize now call `reverseEntry()` on the
  orphan; the storno pair keeps the verifikationsnummer series unbroken
  so the gap-explanation insert is no longer needed.

Greptile P2 + Swedish-compliance — hardcoded category on match-invoice.
  The dashboard internal route writes `category: 'income_services'` for
  every matched invoice payment, overwriting any prior categorization
  with a wrong BAS classification for goods sales / rental income.
  Fixed by preserving the existing transaction.category if set, only
  defaulting to `income_services` when the row had never been
  categorized before.

Compliance Swarm V2.4 — reconciliation date range guard.
  Added a 366-day cap on date_from / date_to via Zod refine. Longer
  reconciliations should be paged.

Greptile P2 — dry-run dedup limitation.
  Added a pitfall note documenting that the ingest dry-run only checks
  external_id-based dedup; content-based dedup (date+amount against
  already-booked rows) only runs in the live pipeline.

Swedish-compliance — BFL chapter typo on fiscal-periods registry.
  "BFL 6 kap" → "BFL 5 kap 2 §" (the löpande bokföring deadline).

Deferred (with rationale documented):
  - OWASP V8.2.1 cross-tenant via path: false positive — wrapper sets
    ctx.companyId from the URL after membership check (recurring across
    swarm runs).
  - OWASP V4.5 select('*') on transactions/invoices: same as Phase 2 —
    those rows feed engine functions that need the full shape.
  - OWASP V2.3 multi-write atomicity (match endpoints): would need a
    Postgres RPC; separate refactor.
  - Swedish-compliance kontantmetoden partial-payment status: same
    semantics as the dashboard internal route; engine-level decision
    out of v1's scope.
  - Greptile P3 `reversible: false` on uncategorize: technically
    correct (the storno itself isn't reversible via this verb).

Tests + build green: 3270 passing, lint clean.

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

* fix(import): distinguish network errors in the SIE upload step

Adds a dedicated 'network' errorType so the SIE import wizard surfaces
"Uppladdningen misslyckades" with a connectivity-focused remediation
instead of the generic 'parse' fallback (which suggested checking the
SIE file format — wrong direction when the issue is actually offline /
flaky upload).

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

* fix(api): address PR #464 swedish-compliance re-run findings

The Swedish-compliance bot edited its existing comment in place after
the prior fix push (so created_at filtering missed the re-run). The
re-run flagged 6 new substantive findings against the post-fix code.

Fix 1 — Orphan storno failure leaves an unresolved immutability gap
  (categorize + batch-categorize).
  When reverseEntry() on the CAS-race orphan fails, the orphan stays
  posted and untraceable. BFL 5 kap 5 § requires every correction be
  traceable. Both paths now insert a voucher_gap_explanations row in
  the catch branch flagging "automatisk storno misslyckades — manuell
  reconciliation krävs", so the orphan is logged at the audit-trail
  level rather than only in app logs.

Fix 2 — Period-lock pre-check (categorize + batch-categorize).
  enforce_period_lock and enforce_company_lock_date triggers block JE
  inserts on locked/closed periods, but Supabase surfaces those as a
  generic 500. Added a new lib/api/v1/check-period-lock.ts helper that
  performs the same check the trigger would (company-wide lock date,
  is_closed, locked_at), and both routes now return a structured
  PERIOD_LOCKED response (existing error code, 400) with reason +
  fiscal_period_id details before the engine call. Note: this is an
  ergonomics check (TOCTOU window between check and insert) — the
  trigger remains authoritative.

Fix 3 — Ingest dry-run now performs content-based dedup too.
  The earlier doc-only note was a compliance miss: an integrator
  relying on dry-run to confirm uniqueness could ingest duplicate
  affärshändelser, violating BFL 5 kap. The dry-run now runs BOTH
  external_id dedup AND content-based (date+amount-against-booked)
  dedup over the request's date range — same query the live pipeline
  uses. Pitfall doc updated accordingly.

Fix 4 — fiscal-periods response now carries duration_days +
  exceeds_18_months computed fields.
  An automated client (year-end wizard, audit tool) can spot a
  non-compliant period sequence (BFL 3 kap, 18-month cap) without
  re-implementing date arithmetic. 549-day cap (18 calendar months)
  is used to keep the comparison deterministic across leap years.
  First-year exceptions still require human judgment; the boolean is
  a flag, not a verdict.

Deferred (with rationale documented in commit, not retried):
  - uncategorize storno memo: reverseEntry() doesn't accept a reason
    parameter today and the JE-level back-reference exists already
    via reversed_by_id / reverses_id. Engine signature change is
    out of v1's scope.
  - VAT integrity check on partial payment in match-invoice: the
    behavior is fully delegated to createInvoicePaymentJournalEntry.
    The bot itself recommends auditing against the engine; that is
    an engine-layer concern and the dashboard internal route uses
    the same path.
  - 366-day reconciliation window (advisory): no statutory basis;
    operational guard.
  - match-supplier-invoice FX path against ML 8 kap 21–23 §
    (advisory): engine-layer concern.

Tests + build green: 3270 passing, lint clean. Touched-suite tests
(transactions, fiscal-periods, accounts, reconciliation) re-run; the
fiscal-periods test asserts the new derived fields.

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

* fix(api): PR #464 round-3 review fixes (re-run after period-lock + dedup)

Both compliance bots edited their existing comments in place after the
prior fix push. New findings against the post-fix code:

Fix — VAT account suppression too broad on account_override.
  categorize/route.ts dropped vat_lines for ANY class-2 override, but
  BAS class 2 includes the 26xx VAT clearing accounts themselves. Result:
  a user override TO a VAT account silently lost the auto-VAT line.
  Tightened to `account_class === 2 && !account_override.startsWith('26')`.
  The override-to-2440-leverantörsskulder case is unchanged (correctly
  drops auto-VAT); the override-to-2611-utgående-moms case now keeps the
  VAT line.

Fix — fiscal-periods 18-month cap uses calendar arithmetic.
  EIGHTEEN_MONTHS_DAYS = 549 was a generous approximation (18 calendar
  months span 540–549 days). Replaced with proper month-anchor math:
  start_date + 18 months computed via setUTCMonth-style year/month
  rollover, then `period_end > anchor` is the violation. Manual day-
  arithmetic on the year part avoids JS's clamp-overflow on Aug-31-style
  start dates. duration_days helper preserved for the response field.

Fix — match-invoice no longer hardcodes 'income_services'.
  When the transaction has no prior category, the route now leaves the
  field UNTOUCHED in the UPDATE (existing default 'uncategorized' or
  whatever was there persists). The response surfaces null for the
  uncategorized case so a caller can detect "needs human classification"
  without inspecting the DB. The auto-default to income_services was
  flowing into BAS 3001/3041/3530 selection mismatches and INK2R/SRU
  mis-reporting for goods/rental flows. Existing-category transactions
  still propagate their value.

Doc — accounts.ts BAS 5/6 description tightened.
  Was "5=other costs, 6=other costs" — both true but flatten distinct
  subgroups. Now spells out 5xxx (rents/supplies/services) and 6xxx
  (marketing/professional/IT) under övriga externa kostnader, with a
  pointer to the canonical BAS chart.

Deferred (with rationale documented):
  - voucher_gap_explanations in SIE export coverage: verification ask;
    SIE export audit is a separate task, not this PR's scope.
  - Dry-run dedup parity with full live pipeline: my dedup matches the
    live pipeline's primary checks (external_id + content date+amount
    against booked rows). Achieving exact parity would need refactoring
    lib/transactions/ingest.ts to expose a shared dedup helper.
  - FX sign convention in match-supplier-invoice: identical to the
    dashboard internal route; if the engine sign convention is wrong
    both surfaces are wrong. Engine-layer audit, not v1 surface.
  - OWASP V8.2.1 cross-tenant via path: recurring false positive — the
    wrapper sets ctx.companyId from the URL only AFTER company_members
    membership check.
  - V2.3 multi-write atomicity in match endpoints: would need a Postgres
    RPC; separate refactor.
  - check-period-lock TOCTOU on no_fiscal_period (advisory note): the
    engine's ensureFiscalPeriod helper creates an open period; if the
    transaction date sits in a historical gap, the engine creates the
    period unlocked. The trigger remains the authoritative gate.

Tests + build green: 3270 passing, lint clean.

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

* fix(api): PR #464 round-4 review fixes (compliance bot re-run)

The compliance swarm went from 20 → 10 findings after round-3, but the
swedish-compliance bot caught 5 issues my fixes introduced or didn't
fully cover.

Fix — VAT account suppression narrowed to BAS 2610–2649.
  My round-3 fix exempted any account starting with '26' from VAT-line
  suppression, but BAS 26xx includes 2650 (momsredovisningskonto) and
  2690 (diverse), neither of which is a moms-line account. Auto-VAT
  posted against 2650 would double-post on the moms reconciliation
  account. Tightened the exception to the 2610–2649 range (utgående
  + ingående moms accounts only).

Fix — exceedsEighteenMonths month-end overflow.
  My round-3 manual month math still passed `startD` raw to Date.UTC,
  which clamps Aug 31 + 18 months to Mar 3, making the cap LATER than
  the BFL 3 kap 1 § ceiling (false negative). Now clamps `startD` to
  the last valid day of the target month using `Date.UTC(year, m+1, 0)`.

Fix — ingest dry-run dedup float-key normalization.
  Built the content-dedup set from `${tx.date}|${tx.amount}` where
  amount is a JS number stringified directly — `-349.5` from JSON vs
  `-349.50` from a Postgres numeric round-trip miss-match. Normalized
  both sides to .toFixed(2). SIE imports commonly carry trailing-zero
  precision, so this would have caused the dry-run to under-report
  duplicates (a BFL 5 kap löpande-bokföring concern: an integrator
  trusting the dry-run could double-book affärshändelser).

Fix — CAS-race voucher_series fallback no longer files under 'A'.
  Both categorize and batch-categorize used `voucher_series || 'A'`
  for the voucher_gap_explanations row. If the orphan JE had no series,
  the gap would be indexed under series 'A' and missed by any series-
  specific audit query (BFL 5 kap 6 §). Now skips the gap row entirely
  when no series is set — the error log already captures the orphan
  for human reconciliation; filing under the wrong key is strictly
  worse than not filing.

Fix — match-invoice rejects kontantmetoden partial payments.
  Under kontantmetoden, utgående moms must be reported per actual
  receipt (ML 13 kap 8 §). The cash-method-partial branch was falling
  through to createInvoicePaymentJournalEntry (the accrual 1510/1930
  clearing path), which doesn't model the per-installment moms event.
  Rather than silently over-report moms, refuse with a VALIDATION_ERROR
  pointing the caller to either wait for the full payment or switch to
  faktureringsmetoden. Full cash-method payments still flow through
  createInvoiceCashEntry (the correct kontantmetod path).

Deferred (with rationale):
  - `uncategorize` resets journal_entry_id to null: dashboard parity;
    the JE-side back-reference (reversed_by_id / reverses_id) preserves
    the audit pair. Adding a separate reversal_journal_entry_id column
    on transactions is a schema change out of v1 scope.
  - OWASP V8.2.1 cross-tenant: recurring false positive.
  - OWASP V2.2 inline Zod filter schemas: structural consistency
    decision — kept in-route to match other v1 endpoints; a future
    refactor can centralize when it justifies the cost.
  - OWASP V16 add userId/companyId to storno-failure log: txLog
    already carries both via ctx.log.child; not changing call-site
    syntax for compliance theatre.
  - Engine-layer FX sign convention in match-supplier-invoice
    (advisory): identical to dashboard internal route.

Tests + build green: 3270 passing.

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

* fix(api): match-supplier-invoice storno conflicting JE before booking

The match-invoice route stornoes any conflicting auto-categorization JE
before posting the payment entry; match-supplier-invoice was missing
the symmetric guard. If a transaction was previously auto-categorized
(e.g. expense_office with a 5460/1930 entry), matching it to a supplier
invoice would post a second 2440/1930 entry while leaving the original
posted — two verifikationer for one affärshändelse, a BFL 5 kap 6 §
integrity violation. Storno-before-match now applies in both routes,
with the same fail-closed semantics (storno failure aborts before any
state change).

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-13 15:22:57 +02:00
Jakob Wennberg 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
Mattsson 81e9dd224e Add/csv import options (#420)
* feat(import): add customer and supplier parsing functionality

- Implemented customer file parsing in `lib/import/customers/parser.ts` with support for Excel and CSV formats.
- Created types for detected customer columns and parsed customer rows in `lib/import/customers/types.ts`.
- Added tests for customer classification logic in `lib/import/shared/__tests__/classify.test.ts`.
- Developed classification functions for customers and suppliers in `lib/import/shared/classify.ts`.
- Introduced shared column utility functions in `lib/import/shared/column-utils.ts`.
- Implemented supplier file parsing in `lib/import/suppliers/parser.ts` with validation for various fields.
- Created types for detected supplier columns and parsed supplier rows in `lib/import/suppliers/types.ts`.
- Added tests for supplier column detection and parsing in `lib/import/suppliers/__tests__/column-detector.test.ts` and `lib/import/suppliers/__tests__/parser.test.ts`.

* fix(labels): update 'Svenskt företag' to 'Svenskt företag eller organisation' for clarity

* feat(import): refactor encoding handling for Swedish files and add tests for character preservation

* feat(recapt): implement clearRecaptIdentity function and integrate into logout flow

* feat(bookkeeping): implement copy functionality and next voucher sequence retrieval

* feat(import): enhance customer and supplier import functionality with normalization and event handling
2026-05-08 15:42:06 +02:00
Mattsson 24107338fa Fix/balance inconsitency (#306)
* feat: implement fiscal period date fields component and validation logic

* feat: update fiscal period validation and naming logic

* feat: implement RPC for computing prior opening balances

- Added `compute_prior_opening_balances` RPC to aggregate opening balances for balance-sheet accounts when no opening balance entry is set.
- Updated tests across various reports to utilize the new RPC for fetching prior balances.
- Refactored `getOpeningBalances` to call the RPC when necessary, improving performance and reliability.
- Introduced a script to repair fiscal period chains for companies with broken periods, ensuring proper linking and continuity.
- Enhanced error handling and validation in the repair script to ensure data integrity during the process.

* feat: implement duplicate opening-balance repair for multi-year SIE imports

* feat: enhance SIE entry listing and deduplication logic for opening balances

* fix: refine companyHasPriorActivity logic to exclude storno entries and improve balance counting
2026-04-21 21:39:57 +02:00
Mattsson 11621bb79f Feat/skv integration full (#284)
* feat: add script to import Skatteverket monthly tax tables as fallback TypeScript module

- Implemented a new script `import-tax-tables.ts` to parse fixed-width TXT tax tables from Skatteverket (SKV 434).
- The script generates a TypeScript module for emergency fallback when the Skatteverket open-data API is unavailable.
- Supports command-line argument for specifying the year and handles parsing of B-rows only.
- Outputs a structured TypeScript file containing tax data for specified years.

* feat: gate salary module behind dev-only flag

Temporarily disable the Lön module in production while the feature is
being completed. Sidebar entries ("Löner", "Anställda") still render but
are not clickable and show a "Kommer snart" badge. Middleware redirects
/salary* to / and returns 404 on /api/salary/* so the feature can't be
reached by direct URL. All gates check NODE_ENV === 'development' so
local dev keeps full access for continued development.

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

* feat: refactor bank file import wizard to streamline column mapping and enhance CSV handling

* fix: bump migration timestamp to avoid collision with logos_bucket

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

* feat: enhance AGI generation and salary entry calculations with improved status checks and error handling

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:03:23 +02:00
Jakob Wennberg 2ea5a72b3d feat: dynamic voucher series dropdown in SIE import (#274)
* feat: dynamic voucher series dropdown in SIE import

Populate the voucher series picker on the import review step from the
company's own data instead of a hardcoded A/B/C/I list. Shows all A–Z
series with inline labels for the company default ("standard") and any
series that already have a running sequence ("används redan"), and
preselects the company default (falling back to B, then the first
existing series, then A).

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

* fix: address voucher series dropdown review feedback

- Log non-PGRST116 Supabase errors from company_settings and
  voucher_sequences fetches instead of silently swallowing them.
- Disable the series Select until the async load completes so users
  don't briefly see the 'B' fallback before it snaps to the real
  company default.
- Replace the seriesInitializedRef guard with a seriesLoaded state that
  resets on company.id change, so switching companies re-runs the
  preselection instead of sticking with the prior company's choice.

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-04-20 09:47:37 +02:00
Jakob Wennberg d741c46d4e fix: show Ersätt befintlig import button for duplicate-file SIE uploads (#270)
The duplicate (file-hash) error path returned an importId but the UI only
captured it for the duplicate_period branch, so users saw a misleading
"ta bort under Bokföring" message with no way to act on it. The replace
flow (and its BFL 5:5 audit trail) is identical in both cases, so expose
the existing button for both.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:29:36 +02:00
Mattsson a3fea6fb7c feat: add opening balance import functionality (#238)
- Implemented OpeningBalanceResultStep component to display results of the import process, including success messages and error handling.
- Created OpeningBalanceUploadStep component for file upload with drag-and-drop support, including validation for accepted file types.
- Developed column detection logic in column-detector.ts to identify account number, name, debit, credit, and balance columns based on headers and data.
- Added parser functionality in parser.ts to handle parsing of opening balance files, including validation and BAS account matching.
- Created tests for column detection and parsing logic to ensure accuracy and reliability.
- Defined types for detected columns and parsed rows in types.ts to improve type safety and clarity in the codebase.
2026-04-14 15:35:50 +02:00
Mattsson cd376e1cad feat: implement viewer role permissions for bank transaction imports and connections (#234) 2026-04-13 19:43:28 +02:00
Jakob Wennberg 9753f18533 fix: address user feedback — RC preview, bank sync lookback, CSV import robustness (#233)
Three confirmed issues from user feedback:

1. Reverse charge preview now uses per-item VAT rates and correct accounts
   (2645/2647, 2614/2624/2634) instead of hardcoded 25%/2614
2. Bank sync uses 90-day lookback on first sync (when last_synced_at is null)
   instead of hardcoded 7 days for all syncs
3. Bank file import improvements:
   - Shared date normalizer supporting DD.MM.YYYY, DD/MM/YYYY, YYYYMMDD
   - Silent row skips now reported with reason in issues[]
   - Decimal separator mismatch detection in generic CSV
   - Swedish error message with format diagnostics on detection failure
   - Date format selector in column mapping UI

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:34:50 +02:00
Jakob Wennberg 7bf7565852 feat: delete last voucher, notes field, schema cache fix (#230)
* feat: delete last voucher, notes field, schema cache fix

Address three customer feedback items from William (wigu.se):

1. Delete last voucher per series (Fortnox model):
   - New `delete_last_voucher` RPC with full safety checks (last-in-series,
     open period, no references, owner/admin only)
   - Session variable bypass for immutability/retention/line triggers
   - Full JSONB audit trail (BFNAR 2013:2 behandlingshistorik)
   - DELETE endpoint + UI with confirmation dialogs
   - Storno restoration when deleting a reversal entry

2. Notes/comment field on vouchers:
   - `notes` column on journal_entries (always-editable internal metadata)
   - Immutability trigger updated to allow notes-only updates on posted entries
   - PATCH endpoint, inline-edit UI on detail page, form textarea

3. Schema cache fix:
   - NOTIFY pgrst applied to production (immediate fix)
   - Retroactive migration + CLAUDE.md migration rule added

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

* fix: address Greptile review — tighten trigger, lock voucher sequence

P1: The notes-only exception in enforce_journal_entry_immutability was
too broad — it only checked 7 verifikation fields, allowing silent
mutation of correction_of_id, reverses_id, reversed_by_id, committed_at,
and user_id on posted entries. Now guards all metadata fields; only
notes and updated_at may differ.

P2: Lock voucher_sequences row FOR UPDATE before the MAX(voucher_number)
check in delete_last_voucher to serialise against concurrent
commit_journal_entry calls, preventing voucher number gaps.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:12:03 +02:00
Mattsson ade4ad5971 Fiscal period and multi bank (#228)
* feat: add fiscal period backward chaining and entry date validation

Support creating fiscal periods before the earliest existing period
(backward chaining) for backfill scenarios, alongside the existing
forward chaining. The engine now validates that entry dates fall within
the selected fiscal period, with a Swedish error message. The journal
entry form auto-selects the matching period and shows a warning with
a CreatePeriodDialog when no period covers the entry date.


* feat: support multi-bank-account for imports and reconciliation

Plumb a configurable settlement account through the entire bank import
pipeline — mapping engine, transaction entries, ingest, and
reconciliation — so secondary bank accounts (e.g. 1931, 1932) work
correctly instead of hardcoding 1930. Adds a get_unlinked_bank_lines
RPC that generalizes the existing get_unlinked_1930_lines with a
fallback for backwards compatibility. The bank file import UI now shows
a bank account selector when multiple 19xx accounts exist. Also adds
default_vat_code/sru_code to account creation and fixes uploadDocument
argument order in enable-banking sync.
2026-04-13 11:13:02 +02:00
Jakob Wennberg 258a64a849 feat: allow replacing completed SIE imports (#227)
* feat: allow replacing completed SIE imports

Users who import a SIE file, make adjustments in the source system, and
re-export can now replace the old import instead of being permanently
blocked by the "overlapping fiscal year" guard.

The old import's entries are cancelled (posted → cancelled) and the
import is marked as 'replaced'. Nothing is deleted — full audit trail
preserved per BFL 5 kap 5§ (rättelse) and BFNAR 2013:2 kap 8.

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

* fix: address Greptile review — atomic RPC, locked_at check

- P1: Wrap entry cancellation + import status update in a single DB RPC
  (replace_sie_import) to prevent inconsistent state on partial failure
- P2: Check locked_at in addition to is_closed for fiscal period guard
- P2: Use RPC return value for accurate cancelled entry count

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:48:49 +02:00
Mattsson a1a816b4a5 Delete features (#218)
* Implement company and account deletion features

- Add event types for company and account deletion to CoreEvent.
- Enhance Supabase middleware to handle company context resolution and cookie management for archived companies.
- Create API routes for deleting accounts and companies, including necessary validations and event emissions.
- Implement tests for account and company deletion endpoints to ensure proper functionality and error handling.
- Add retention notice component to inform users about bookkeeping data retention during destructive actions.
- Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws.

* feat: enhance account deletion process and update user notifications

* Add service client for onboarding completion check and update escape hatch visibility

* Enhance invite flow and email handling for company members

* Refactor company context and RLS policies for active company isolation

- Update `switchCompany` to remove unnecessary revalidation as client handles navigation.
- Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships.
- Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility.
- Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership.
- Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization.
- Implement `CompanyTabSync` component for real-time active company enforcement across tabs.
- Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`.

* feat: implement viewer role enforcement for write permissions

- Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company.
- Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions.
- Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers.
- Created tests to verify the behavior of the viewer role and write permissions.
- Added database migration to enforce read-only access for viewers at the database level.
2026-04-11 17:06:32 +02:00
Mattsson 211033410c Fix/import data (#200)
* fix: enhance import data handling and consent management across components

* feat: Enhance SIE import functionality with validation and error handling improvements

- Added validation errors and warnings state management in SIEImportWizard.
- Improved error handling for duplicate, validation, and parsing errors during SIE file import.
- Enhanced user feedback with actionable guidance for common import errors.
- Updated SIEUploadStep to display validation errors and warnings.
- Improved error messages in API routes for better clarity and user experience.
- Added file size and type validation in the SIE parse route.
- Enhanced parsing logic to provide more detailed error messages for unbalanced vouchers and missing amounts.
- Created a new storage bucket for SIE file archival in Supabase with appropriate policies for user access.
- Updated tests to reflect changes in error messages and validation logic.

* fix: Improve type assertion for response in getPage method

* Update extensions/general/arcim-migration/lib/migration-orchestrator.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update supabase/migrations/20260408130000_sie_files_storage_bucket.sql

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: Add company ID verification for consent handling in accept and disconnect endpoints

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-08 18:17:00 +02:00
Jakob Wennberg d23cb4c859 fix: SIE import duplicate check, performance, and UX improvements (#162)
* fix: catch duplicate SIE import early with clear error message

- Add duplicate check in execute route before doing any work (defense in depth)
- Handle duplicate error from execute route in frontend
- Show "Filen har redan importerats" heading instead of generic "Kunde inte läsa filen"

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

* fix: catch duplicate SIE import early and batch account creation for performance

- Add duplicate check in execute route before doing any work (defense in depth)
- Handle duplicate error from execute route in frontend with clear Swedish message
- Replace sequential ensureAccountExists loop (50-100 DB round trips) with single
  batch SELECT + batch INSERT — reduces import time from 3+ min to seconds

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

* fix: update mappings optimistically after creating missing accounts

Previously re-parsed the SIE file after account creation, which could fail
with a 409 duplicate error (leaving the "create accounts" card stuck).
Now optimistically marks created accounts as self-mapped and updates the
preview stats immediately.

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

* fix: address Greptile P2 feedback — error handling and typed errorType prop

- Check batch insert error in executeSIEImport account creation safety net
- Replace brittle string-match error detection with typed errorType prop
- Remove stale file dependency from handleCreateAccounts useCallback

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:21:51 +02:00
Jakob Wennberg d0b3f21bde feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157)
Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr,
invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/
Anthropic/OpenAI deps) to simplify core and reduce bundle size.

Restructure monolithic settings page into dedicated sub-pages (company,
bookkeeping, invoicing, tax, banking, api, account, team, templates) with
shared layout and sidebar navigation.

Add atomic commit_journal_entry RPC so voucher number increment and status
update happen in a single transaction — prevents burned numbers on constraint
failures. Add continuity check report and voucher gap explanation tracking.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:08:00 +02:00
Jakob Wennberg 5cb8c3c1e4 feat: support contact links and SIE import UX polish (#138)
* feat: event log, pending operations, and MCP staging

- Event log system: persist bus events to event_log table for external
  automation platforms. Batch insert for transaction.synced. Daily
  cleanup cron at 02:00 UTC.
- Pending operations: MCP write tools (categorize, create customer,
  create invoice) now stage to pending_operations instead of executing
  directly. Users review and commit/reject from /pending in the web UI.
- Granskning page: card-based review UI with expandable previews,
  commit/reject dialogs. Only shown in nav when pending ops exist.
- Commit route re-executes using core lib functions (no extension
  imports). Guards against stale state (double-commit, deleted entities).

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

* feat: stage new MCP write tools after main merge

Add staging for 4 new write tools from #133:
- mark_invoice_paid, send_invoice, mark_invoice_sent,
  match_transaction_invoice
- Expand pending_operations CHECK constraint
- Add commit executors with full execution logic
- Add UI labels and generic preview component
- Remove confirm parameter from categorize (single-call staging)
- Fix UUID in pending op title (fetch transaction description)
- Hide Granskning nav when no pending ops

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

* fix: address PR review feedback

- Fix TS build error: use `select('*, customer:customers(*)')` for
  match_transaction_invoice to avoid array type inference
- Add status guard to commitSendInvoice (prevents duplicate sends)
- Replace auth.admin.getUserById with user email from session auth
- Restore optimistic lock check in commitMatchTransactionInvoice
- Fix tool description typo: expense_software → expense_office

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

* feat: add support contact links and improve SIE import UX

Add a SupportLink component with a contact dialog throughout the app
(nav, help page, settings, MFA, error pages, empty states). Improve
SIE import flow with phased loading states, structured skip breakdowns,
and an elapsed-time counter. Fix MFA enroll stale factor cleanup and
URL encoding for settings return path.

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

* fix: address PR review — open redirect, XSS, test cleanup, fallback email

- Validate returnTo is a relative path in MFA enroll (prevents open redirect)
- Add afterEach import to event-log-handler tests (fixes handler leak)
- HTML-escape user-supplied subject and message in support email body
- Replace hardcoded personal email with support@gnubok.se fallback

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:14:17 +01:00
Mattsson 8976cd812d Vat onboarding fixes (#39)
* fix: enhance validation for moms_period and conditionally set vat_number and moms_period based on vat_registered

* Enhance preprocessing for first year fields, fiscal year end month, and accounting method in schema validation

* fix: simplify schema validation by removing unnecessary preprocessors

* fix: prevent setting invalid values in select components across multiple forms

* fix: improve layout responsiveness and conditional rendering in onboarding steps
2026-03-17 14:28:58 +01:00
Jakob Wennberg a7be1aab17 feat: comprehensive UI design audit and normalization (#28)
* feat: import system improvements, INK2 fix, and Swedish text corrections

- SIE parser: Windows-1252 and CP437 encoding detection and decoding
- Bank file parser: add Nordea Business (Företag) CSV format
- Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV
- INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years
- Dashboard: parallel Supabase queries, simplified dashboard page
- Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures
- Import wizard UI improvements across all steps
- Migration: add 'bas_range' match type to sie_account_mappings constraint
- Extensive new tests for SIE parser encoding and bank file parser

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding

Arcim migration wizard improvements:
- Progress bar now excludes non-interactive steps (migrating/result)
- Fix OAuth text to match target="_blank" behavior (new tab, not redirect)
- Display month names instead of "Månad X" in preview
- Fix Swedish typo "förifylla" in no-company-info message
- Replace native checkboxes with shadcn Switch in options step
- Add ConfirmationDialog before starting migration
- Show progress percentage during migration
- Add "Nästa steg" guidance and navigation links in result step
- Add "Försök igen" button in error state (returns to options)
- Add Bokio company ID help text (GUID from URL)
- Add Fortnox integration add-on hint on connection failure

Also includes: SIE import system improvements, INK2 fixes, Swedish text
corrections, Sentry error tracking setup, and arcim-migration extension
scaffolding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback

- Fix OAuth error recovery blank page (restore provider from URL params)
- Pass real userId to MigrationWizard instead of empty string
- Remove ~50 debug console.log statements from sie-import.ts
- Fix comment referencing account 3740 → 3741

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: comprehensive UI design audit and normalization

Dashboard audit:
- Fix muted-foreground contrast (4.31:1 → 5.08:1) for WCAG AA
- Add prefers-reduced-motion media query for all animations
- Replace border-l-2 accent anti-pattern with subtle full-border colors
- Add aria-expanded to toggle buttons, role="status" to live counters
- Fix touch targets on deadline buttons (28px → 36px)
- Vary section spacing for rhythm (mb-12/mb-10/mb-8)
- Remove unused imports and dead code

Transactions audit + hardening:
- Add pagination (200 per page) with "Ladda fler" button
- Replace height animation with transform-only exit animation
- Show batch progress in floating action bar during processing
- Fix batch bar mobile overlap (bottom-20 on mobile)
- Replace clickable badges with proper button elements
- Add safe area padding to fullscreen swipe view
- Add response.ok check to suggestion fetch
- Add truncation to invoice number buttons

Invoicing audit:
- Remove border-l-4 accent pattern from invoice cards
- Replace string concatenation with cn() utility

Systemic sweep (34 files):
- All page headings: font-bold → font-display font-medium (Fraunces)
- All stat numbers: font-bold → font-display font-medium tabular-nums
- All hard-coded blue/amber/emerald colors → design tokens
- Remove all dark mode overrides (tokens handle automatically)
- Tint pure white card background to 99%

Design context added to CLAUDE.md with brand personality,
aesthetic direction, and 5 design principles.

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

* fix: bookkeeping flow audit — design system, accessibility, UX

- Replace raw <select> with shadcn Select component (JournalEntryForm)
- Add confirmation dialog for account deletion (ChartOfAccountsManager)
- Remove console.error from production code (JournalEntryList, JournalEntryForm)
- Fix contradictory h-7/min-h-[44px] button sizing → h-10 (ChartOfAccountsManager)
- Increase BAS catalog "Lägg till" touch target h-7 → h-9
- Improve loading state with spinner (JournalEntryList)
- Improve empty state with icon, description, and guidance (JournalEntryList)
- Add response.ok check on journal entry fetch
- Add aria-expanded to entry expand buttons
- Add tabular-nums to desktop debit/credit columns

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

* fix: onboarding and empty state improvements

Onboarding:
- Replace font-serif with font-display (Fraunces) for brand consistency
- Remove console.error calls from production code

Empty states:
- Fix broken /transactions/new link in EmptyTransactions (route doesn't exist)
- Add actionHref fallback to EmptyCustomers when no onAction prop provided
- Improve EmptyTransactions description copy

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

* fix: clarify Swedish UX copy — terminology, errors, descriptions

Terminology consistency:
- "Försenad" → "Förfallen" for overdue invoices (customers/[id])
- "bokföringsorder" → actionable description in bookkeeping page
- "verifikation har bifogats" → "underlag har bifogats" in doc warning
- "Fortsätt ändå" → "Bokför utan underlag" (specific action)

Error messages — replace generic "Fel" + "Något gick fel" with specific:
- "Något gick fel vid bokföring" → "Transaktionen kunde inte bokföras"
- "Något gick fel vid matchning" → "Transaktionen kunde inte matchas"
- "Kunde inte hämta X" → "Kunde inte ladda X" + recovery hint
- Add "Försök igen" guidance to all error toasts

Page descriptions — replace redundant with actionable:
- Invoices: "Skapa och hantera" → "Skicka, följ betalningar, skapa kreditnotor"
- Bookkeeping: list of features → actionable description

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

* fix: design critique — dashboard affordance, reports description

Dashboard:
- Add ChevronRight indicator to clickable summary cards
  (Att få betalt, Koppla bank) to distinguish from static cards
- Add cursor-pointer to linked cards

Reports:
- Replace feature list description with actionable guidance
  "Huvudbok, grundbok..." → "Generera skattedeklarationer..."

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

* fix: replace generic "Fel" error toasts with specific messages

Deadlines: 5 generic "Fel" → specific per-action titles
  (create, toggle, edit, delete, load)
Expenses detail: 5 generic "Fel" → specific per-action titles
  (load, approve, pay, credit, delete)
Expenses new: 3 generic "Fel" → instructional validation messages
  (supplier name, supplier selection, invoice number)
Customers: 1 generic "Fel" → specific load error with recovery hint

All error toasts now follow pattern:
  title = what failed, description = how to recover

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

* fix: replace all remaining generic "Fel" error toasts (37 instances)

Systematic sweep across 12 dashboard pages replacing generic
title: 'Fel' with context-specific error titles:

- Load errors: "Kunde inte ladda [resurs]"
- Action errors: "[Åtgärd] misslyckades"
- Validation: "[Fält] saknas"

Every error toast now tells the user what failed without needing
to read the description. Recovery hints added where missing.

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

* fix: import flow — normalize stat typography, remove console.warn

- Replace font-bold with font-display font-medium on 13 stat numbers
  across SIEPreviewStep, BankFilePreviewStep, BankFileConfirmStep,
  ImportResultStep (missed by systemic sweep since these are in
  components/import/, not app/(dashboard)/)
- Add tabular-nums to stat numbers displaying counts/currency
- Remove console.warn in ArcimMigrationWorkspace

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

* fix: final cleanup — console statements, remaining font-bold stats

Remove production console statements:
- Step1EntityType: remove debug console.warn (dead code after onNext)
- TransactionBookingDialog: remove console.error on doc link failure
- JournalEntryAttachments: remove 3 console.error calls

Normalize remaining font-bold stat displays:
- SwipeCategorizationView: 3 instances (completion, amount displays)
- NEDeclarationView: yearly result heading + value

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

* fix: address Greptile review feedback

loadMoreTransactions: add inbox item enrichment matching fetchTransactions
- Paginated transactions now fetch invoice_inbox_items in parallel
- Fixes missing document indicator, template suggestions, and inbox
  match card for transactions loaded via "Ladda fler"

fetchAllPages: add maxPages guard (default 500) to prevent infinite loop
- If Arcim gateway returns hasMore:true indefinitely, the loop now
  exits after 500 pages instead of running forever

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:48:40 +01:00
Jakob Wennberg 2ad8731dc9 feat: arcim migration wizard UX, import fixes, Sentry setup (#22)
* feat: import system improvements, INK2 fix, and Swedish text corrections

- SIE parser: Windows-1252 and CP437 encoding detection and decoding
- Bank file parser: add Nordea Business (Företag) CSV format
- Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV
- INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years
- Dashboard: parallel Supabase queries, simplified dashboard page
- Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures
- Import wizard UI improvements across all steps
- Migration: add 'bas_range' match type to sie_account_mappings constraint
- Extensive new tests for SIE parser encoding and bank file parser

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding

Arcim migration wizard improvements:
- Progress bar now excludes non-interactive steps (migrating/result)
- Fix OAuth text to match target="_blank" behavior (new tab, not redirect)
- Display month names instead of "Månad X" in preview
- Fix Swedish typo "förifylla" in no-company-info message
- Replace native checkboxes with shadcn Switch in options step
- Add ConfirmationDialog before starting migration
- Show progress percentage during migration
- Add "Nästa steg" guidance and navigation links in result step
- Add "Försök igen" button in error state (returns to options)
- Add Bokio company ID help text (GUID from URL)
- Add Fortnox integration add-on hint on connection failure

Also includes: SIE import system improvements, INK2 fixes, Swedish text
corrections, Sentry error tracking setup, and arcim-migration extension
scaffolding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback

- Fix OAuth error recovery blank page (restore provider from URL params)
- Pass real userId to MigrationWizard instead of empty string
- Remove ~50 debug console.log statements from sie-import.ts
- Fix comment referencing account 3740 → 3741

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:24:54 +01:00
Jakob Wennberg e8fb84b4fd feat: import system improvements, INK2 fix, and Swedish text corrections (#10)
- SIE parser: Windows-1252 and CP437 encoding detection and decoding
- Bank file parser: add Nordea Business (Företag) CSV format
- Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV
- INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years
- Dashboard: parallel Supabase queries, simplified dashboard page
- Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures
- Import wizard UI improvements across all steps
- Migration: add 'bas_range' match type to sie_account_mappings constraint
- Extensive new tests for SIE parser encoding and bank file parser

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:05:37 +01:00
Jakob Wennberg a1ca96453c feat: mobile UX improvements across dashboard (#9)
* feat: mobile UX improvements across dashboard pages

- Responsive layouts with stacked mobile forms and grid desktop views
- Safe area handling (viewportFit cover, bottom nav insets, main padding)
- Touch-friendly dialogs, tabs, and page headers
- Mobile nav drawer open/close animation
- Dashboard layout parallel Supabase queries
- SIE parser: handle quoted VER/TRANS fields
- Skip account override for template-based transaction booking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback from Greptile

- Fix nav drawer close timer race condition with useRef + clearTimeout
- Show locked VAT rate as read-only label on mobile instead of hiding it
- Remove dead bottom-16 class overridden by inline style

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 15:36:52 +01:00
Jakob Wennberg 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:05:49 +01:00
Jakob Wennberg f3ec634a46 feat: open-source under AGPL-3.0, redesign UI to grayscale palette, add uncategorize API, fix VAT account names
Add LICENSE (AGPL-3.0-or-later), CONTRIBUTING.md, SECURITY.md, DCO, and NOTICE files.
Rewrite README for open-source audience with self-hosting instructions.
Redesign color palette to grayscale chrome theme across all components.
Add transaction uncategorize API route with tests.
Fix VAT account name mismatches in migration 052.
Improve import page with SIE file support and loading skeleton.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:18:11 +01:00
Jakob Wennberg 66a4027f1e feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup
- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:19:56 +01:00
Jakob Wennberg 03b569d708 refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export,
  hotel, restaurant, tech) — only general-purpose extensions remain
- Move NE-bilaga and SRU export from extensions to core reports (lib/reports/)
- Move moms-box-mapping from extensions/export/shared to lib/vat/
- Replace per-extension API routes with catch-all dispatcher
  (app/api/extensions/ext/[...path]/route.ts)
- Add manifest.json for each extension with metadata, env vars, and deps
- Add api-routes.ts pattern for extension-defined API endpoints
- Add code generation scripts (generate-extension-registry, create-extension)
- Add extensions.config.json for opt-in extension loading
- Add extensions.schema.json for config validation
- Add email service interface with noop default (lib/email/service.ts)
- Add CI workflow (core-build.yml) to verify core builds with zero extensions
- Add migration 045: expand account_type CHECK for untaxed_reserves
- Update CLAUDE.md with comprehensive extension system documentation
- Update all report engines and bookkeeping services for new imports
- Clean up extensions.schema.json to only list existing extensions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:32:56 +01:00
Jakob Wennberg bbb82866ee feat: add 4 new Swedish bank CSV parsers and improve transaction categorization
Add auto-detecting CSV parsers for Länsförsäkringar, ICA Banken, Skandia,
and Lunar. Refine SEB detection to avoid false matches. Update bank file
upload UI with new bank options and export instructions. Include booking
templates, improved AI categorization, and transaction review enhancements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:38:44 +01:00