Commit Graph
5 Commits
Author SHA1 Message Date
MattssonandClaude Fable 5 6782da3e9e feat(bokslut): calculate and book overavskrivningar (2150/8850) (#1379)
Add an automatic excess-depreciation calculator for machinery and
equipment under IL 18 kap: 30-rule and 20-rule residuals (fiscal-period
aware for short and long years), ledger vs asset-register
reconciliation, fail-closed blocking states, and a signed proposal that
books via the dispositions flow (8853/2153). Releases of an over-target
reserve are mandatory and not overridable; increases are optional and
capped server-side.

Fixes #323

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 17:25:30 +02:00
Jakob WennbergandClaude Opus 5 579f48752e fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base (#1253)
* fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base

sumPostedYearEndDispositions reconstructs resultat fore skatt for the tax
calculation, because generateIncomeStatement excludes every
source_type='year_end' entry. It summed class 88 and 7533 but not 78xx, so
planenlig avskrivning posted by the bokslut flow
(lib/bokslut/assets/depreciation-engine.ts) was dropped from the income
statement and never added back. The bolagsskatt base and the
periodiseringsfond 25 % cap were therefore computed on an overstated result:
tax too high by roughly 20.6 % of the depreciation.

Also exclude the period's final bokslutsverifikation from the fetch. It
carries source_type='year_end' as well and reverses every P&L account,
78xx/88xx/7533 included (verified against production closing entries), so
once the year is closed it would cancel the add-back this function exists to
produce. That hazard already applied to 88xx and 7533; the fix closes it for
all three rather than widening it.

Scope is deliberately the tax base only. Making the standalone
resultatrakning show bokslut entries is a separate, larger change: the same
exclusion is duplicated in the kpi_report_aggregates RPC, it moves displayed
profit for every company that ran the bokslut flow, and it means removing
the add-back at four call sites.

Refs #1051

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

* fix(bokslut): scope the closing-entry lookup to the company and fail loudly

Review (CodeRabbit + the compliance swarm, ASVS V8.2.1) flagged the new
fiscal_periods read in sumPostedYearEndDispositions on two counts, both fair.

It filtered only on the period id while every sibling query in the same
function carries the tenant scope. Primary key or not, service-role paths
have no RLS to fall back on and the repo's rule is to filter company_id
explicitly, so it now does.

It also discarded the query error. That mattered more than it looks: a failed
read fell through to closingEntryId = null, which silently re-admits the
closing verifikat's 78xx/88xx reversals and understates the tax base, i.e.
exactly the failure this lookup was added to prevent. It now throws, and the
surrounding catch turns it into the existing 'Failed to read posted
dispositions' error. A wrong bolagsskatt is worse than a loud failure.

Two regression tests: the lookup carries both eq filters, and a lookup
failure propagates instead of degrading to a wrong number.

Refs #1051

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:29:37 +02:00
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
MattssonandClaude Opus 4.8 8322830f46 Add/issue in absurdum (#739)
* feat(assets): allow editing fixed asset fields before depreciation

The fixed asset register only offered a "Dispose" action, so correcting a
mis-entered acquisition date/cost/category meant running the disposal flow —
which posts a real divestment voucher plus a Ch. 8a VAT adjustment.
Disproportionate and wrong for a data-entry fix.

Add an Edit action that allows correcting those fields directly, gated for
correctness:

- service: extend updateAsset() with category/acquisition_date/
  acquisition_cost; block the change once the asset is disposed or has posted
  depreciation (AssetCorrectionBlockedError) where it would desync posted
  vouchers from the register; realign the BAS triple on category change.
  Name, useful life, and method stay editable.
- api: extend the PATCH schema; annotate GET /api/assets with
  has_posted_depreciation so the UI can lock basis fields proactively.
- ui: EditAssetDialog + pencil action; disables date/cost/category when
  depreciation has been booked, with an inline explanation.
- errors: register ASSET_CORRECTION_BLOCKED (409).
- tests: unit tests for the guard; pg test for pre-disposal editability.

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

* feat(assets): also block basis edits when depreciation was hand-posted

The correction guard only consulted depreciation_schedules, so an
avskrivning booked as a manual journal entry (no schedule row) slipped
through and a basis correction was wrongly allowed.

Add a ledger scan: any posted credit to the asset's ackumulerade-
avskrivningar account (12x9) counts as depreciation. Entries that
depreciation_schedules attributes to a *different* asset are excluded, so
a sibling's engine avskrivning on a shared 12x9 account doesn't produce a
false block. What remains is depreciation tied to this asset (engine or
manual); a basis correction is blocked there and must go through storno.

Adds two unit tests: blocks on a hand-posted credit, allows when the only
12x9 credit belongs to a sibling's engine entry.

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

* fix(invoices): allow negative unit prices for discount lines

The invoice creation form rejected negative unit prices via a frontend
superRefine check, blocking valid discount lines (e.g. "Rabatt -100").
The unit_price error was never rendered inline, so submission failed
silently. The backend schema already allows negative unit prices (see
CreateInvoiceItemSchema test), so the form was simply out of sync.

Remove the non-negative constraint; empty/NaN prices are still rejected
by the base z.number() type. Drop the now-unused validation_price_positive
translation key from both locale files.

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

* feat(invoices): allow editing draft invoices

Drafts could be saved but not edited — the only way to change a draft's
lines, customer, dates or amounts was to delete and recreate it. Add a
"Redigera" action on draft invoices that opens the invoice editor
pre-filled with the draft and saves changes in place.

A verifikat is only created when an invoice is sent (or paid, under
kontantmetoden), so every status=draft invoice is uncommitted and safe to
edit; sent/paid invoices stay immutable and still require a credit note.

- Extract buildInvoiceWriteData() with the shared validation + computation
  (VAT rules, ROT/RUT, accruals, totals, currency, item rows); POST now
  uses it too, behaviour unchanged.
- Add UpdateInvoiceSchema and PATCH /api/invoices/[id], guarded to drafts
  (status=draft, no journal entry, not self-billed); number and status are
  preserved and no invoice.created is emitted.
- Extract the invoice creator into a shared InvoiceEditor with create /
  edit modes; /invoices/new is now a thin wrapper and /invoices/[id]/edit
  is the new edit page.
- Add a "Redigera" button on draft invoice detail pages + sv/en strings.
- Tests for the builder, UpdateInvoiceSchema and the PATCH route.

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

* feat(reports): make Huvudbok findable via account/saldo search terms

Searching the command palette for natural phrases like 'saldo per konto', 'kontoutdrag', 'kontoanalys' or 'transaktioner per konto' returned nothing, so users couldn't find the general ledger. Enrich the Huvudbok entry's keywords with those synonyms, and let Saldobalans and Balansrapport match 'saldo per konto' too since they are genuinely per-account balance views.

Companion change — the clearer Huvudbok report description ('Saldo och alla transaktioner per konto') — already landed in d5f474cb.

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

* feat(settings): let users edit their personal name

Add an editable Namn field to /settings/account that updates profiles.full_name and best-effort syncs auth user_metadata. Previously the personal name was only ever set from BankID's legal name at signup with no way to correct it, so users whose tilltalsnamn isn't their first given name were greeted by the wrong name (and email/password users had no name at all).

New POST /api/user/profile route (requireAuth, RLS-scoped update) mirrors /api/user/locale. sv/en strings added.

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

* feat(invoices): per-invoice öresavrundning override

Add a display-only öresavrundning flag per invoice that wins over the
company-wide setting. Resolution order in getDisplayTotal: per-invoice
override -> company setting -> default-on. The stored total and the booked
verifikat keep the exact öre; only the rendered total changes.

Supplier invoices gain the same flag but resolve a null to off (they never
had rounding historically), exposed via a toggle on the new-invoice form
and a rounding row on the detail page.

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

* feat(transactions): warn on possible duplicate before booking

Before committing a transaction (via book or categorize), detect an
already-booked sibling with the same date and amount and return a 409
TRANSACTION_BOOK_POSSIBLE_DUPLICATE instead of silently double-booking.

The user can override with force=true, which must be bound to the reviewed
sibling via expected_duplicate_transaction_id; the candidate is re-detected
server-side, so a stale or guessed id is rejected with
TRANSACTION_BOOK_FORCE_CANDIDATE_MISMATCH. Detection is fail-open on the
non-force path and fail-closed under force.

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

* feat(transactions): shadow-mode scope-drift dedup counter in bank ingest

Count rows that an enforcing same-feed scope-drift rule WOULD treat as
re-imports (the IBAN-drift re-imports the external_id check misses) and
surface it as IngestResult.shadow_scope_drift_candidates. Nothing is
blocked yet -- the counter only measures how often the rule would fire so
it can be validated against real data before enforcement.

Also gitignore scripts/delete-duplicate-transactions.ts: a destructive,
hand-run cleanup tool kept out of the repo so it can't run in CI/cron or be
mistaken for a supported feature.

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

* fix(bokslut): base bolagsskatt on post-disposition result

Bokslutsdispositioner are booked as source_type='year_end', which the
income statement excludes, so net_result alone overstates resultat före
skatt and the booked tax ignored the periodiseringsfond avsättning (too-high
tax, ÅR/INK2 mismatch).

calculateBolagsskatt now accepts resultBeforeTaxOverride. The preview builder
mirrors each proposal's P&L effect (+återföring, -avsättning, -SLP) onto the
pre-disposition result; the commit path sums the already-posted dispositions
via the new sumPostedYearEndDispositions (class 88 + 7533) since bolagsskatt
is committed last.

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

* feat(settings): fiscal years manager

Add a FiscalYearsManager to the bookkeeping settings that lists fiscal
periods with their status (closed > locked > open) and creates the next
year via CreatePeriodDialog, seeded to chain forward from the latest
period end.

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

* fix(api): return 400 when locking a period with unbooked transactions

lockPeriod() refuses to lock a period that still has uncategorized business
transactions. Detect that message in the lock route and surface it as a
clear PERIOD_HAS_UNBOOKED_TRANSACTIONS (400) instead of a generic 500.

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

* feat(invoices): implement isEditableInvoiceDraft utility and apply it across invoice edit routes
feat(transactions): log duplicate dismissal events in behandlingshistorik
test(invoices): add tests for isEditableInvoiceDraft function
test(transactions): enhance tests to verify behandlingshistorik logging
refactor(bokslut): update tax calculation test descriptions for clarity

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:42:37 +02:00
MattssonandClaude Opus 4.7 32d9978f1b Fix/chrome pdf preview csp (#572)
* feat: add option to exclude year-end closing entries in SIE export and related reports

* delete docs

* fix: allow Chrome's PDF viewer in verifikat document preview

The /api/documents/:id/inline route shipped with
`object-src 'none'` in its CSP, which blocked Chrome's built-in PDF
viewer (it renders inline PDFs via an internal <embed>). Users on
Chrome saw "Det här innehållet har blockerats" when expanding a PDF
attachment in the bookkeeping view; Firefox (PDF.js) and Edge (own
viewer) were unaffected, and JPGs worked because <img> isn't subject
to object-src.

Drops the CSP for this route to the minimum needed for embeddability:
`frame-ancestors 'self'`. X-Content-Type-Options: nosniff plus the
fixed Content-Type from the handler already block MIME confusion;
X-Frame-Options: SAMEORIGIN + frame-ancestors still block clickjacking.

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

* feat(auth): add webmail deep link to email confirmation screens

Mirrors Stripe's signup UX: after asking the user to verify their email,
detect their webmail provider from the domain and show a button that
opens the inbox in a new tab. Gmail gets a from:<sender> search
pre-populated; Outlook/Yahoo/iCloud/Proton open the inbox directly.
Unknown / custom domains fall back to the existing copy.

Sender address is configurable via NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM
(default noreply@gnubok.se) so white-label installs can match their
Supabase Auth SMTP config.

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

* fix(auth): unblock first-time password set for BankID users with MFA

Supabase rejects updateUser({password}) and mfa.unenroll with "AAL2 session
is required" whenever a TOTP factor is enrolled. BankID magic-link logins
produce AAL1, and middleware skips MFA enforcement for bankid_linked users,
so they had no path to AAL2 — leaving them unable to set a backup password
or disable MFA without going through the email-recovery escape hatch.

- /api/account/password: branch on app_metadata.has_password. First-time set
  writes via service.auth.admin.updateUserById (no existing credential to
  protect, AAL2 guard does not apply). Change-password keeps the user-session
  updateUser so AAL2 still fires for credential rotation.
- /mfa/verify: accept a safeReturnTo query param and route there after
  successful verify, so step-up flows can land back where they came from.
- SecuritySettings: detect the AAL2 error from both change-password and
  mfa.unenroll and redirect through /mfa/verify?returnTo=/settings/account
  instead of toasting a dead-end error.

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

* Add tests and rounding utility for öre precision in bokslut calculations

- Implemented `roundOre` function for rounding SEK amounts to two decimal places, ensuring consistent monetary calculations.
- Introduced `ORE_TOLERANCE` constant for comparing rounded amounts, facilitating invariant checks in financial entries.
- Created comprehensive tests for `roundOre`, covering typical cases, edge cases, and idempotency.
- Added year-end invariants tests to verify database-level guarantees for closing entries, ensuring they balance to the öre and reject discrepancies.
- Developed end-to-end tests for the dispositions chain, validating the correctness of calculations across various scenarios.

* fix: update PDF rendering to remove Swish QR code generation and set default to disable Swish visibility

* fix: enhance security by rejecting data URIs in safeReturnTo function tests

* fix: improve rounding logic in roundOre function and add customer_type migration

* fix: add customer_type column to customers and enforce CHECK constraint

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:29:41 +02:00