15af0cd7eadcecafd6d17618d04bf67b4f736fd2
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb01b946fc |
fix(bokslut): map computer/vehicle depreciation to standard 7832 so it resolves (#755) (#822)
* fix(bokslut): map computer/vehicle depreciation to standard 7832 so it resolves (#755) The computer asset category mapped depreciation expense to 7833 and vehicle to 7834, but neither is in the standard BAS catalog (7834/7835 were removed as non-standard in #463, guarded by bas-reference.test.ts). Because backfillStandardBASAccounts only seeds accounts present in BAS_REFERENCE, the engine threw AccountsNotInChartError on minimal charts and annual depreciation was blocked. - Remap computer and vehicle depreciation expense to 7832 (Avskrivningar på inventarier, verktyg och installationer). Both 1240 (Bilar) and 1250 (Datorer) sit in the maskiner-och-inventarier asset range, so 7832 is the correct standard depreciation account — same one equipment already uses. The asset register still separates them via 1240/1249 and 1250/1259 on the balance sheet. - A regression guard surfaced a second gap: other_tangible mapped to 1280/1289, but 1280 is 'Pågående nyanläggningar/förskott' and 1289 is not a BAS account. Remap other_tangible to 1290/1299 ('Övriga materiella anläggningstillgångar' + its ack. avskrivningar) — the BAS-correct accounts, and the range the iXBRL K2 mapper already classifies other_tangible under. Keeps accumulated = asset + 9. - Add a guard test asserting every DEFAULT_ACCOUNTS_BY_CATEGORY account resolves in BAS_REFERENCE, so a future missing account fails CI instead of a user's depreciation run. No new BAS accounts are added, so the non-standard-accounts guard stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): drop the duplicate next-best-action hero for a single CTA The agent-built dashboard showed a 'next best action' hero card AND the unified 'Att göra' worklist below it — two surfaces pointing at the same work (book transactions, unpaid invoices). Remove the hero so the page leads with metrics + the single 'Att göra' worklist, giving one unambiguous CTA surface instead of two. Drops the now-unused nextBestAction computation and the Receipt/ ArrowLeftRight/Clock imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
fd4274787c |
feat(bokslut): Phases 4-7 — accruals + årsredovisning + EF + MCP tools (#509)
* feat(bokslut): Phases 4-7 — accruals + årsredovisning + EF + MCP tools Builds on the Phase 1-3 PR (#508). Same K2-AB scope decisions hold (iXBRL deferred, K3 deferred, koncernredovisning out of scope) — what ships: PHASE 4 — Periodiseringar - lib/bokslut/accruals/accrual-detector.ts: auto-proposes the vacation- liability change (delta on 2920 against 7090 + 31,42 % avgifter on 7519/2940) by reading lib/reports/vacation-liability.ts. Manual prepaid (17xx) / accrued (29xx) / audit-fee builders for entries the heuristic can't derive — supplier-invoice service_period detection is deferred until the data model grows the field. - /api/bookkeeping/fiscal-periods/[id]/accruals (GET + POST) — posts each accrual as a separate manual-source journal entry with the next-day reverse date embedded in the description. - New AccrualsStep wizard step between Preflight and Dispositions. - Auto-reversal cron is follow-up infra; for now reverses_on is metadata + a visible UI badge. PHASE 5 — Årsredovisning PDF + signing - lib/bokslut/arsredovisning/{types,build-data,arsredovisning-pdf,signature -service}: pre-fills flerårsöversikt from prior 3 fiscal periods, eget- kapital-förändring from journal data, K2-minimum noter with avskrivningstider auto-derived from the asset register and medelantal anställda from the employees table. PDF via @react-pdf/renderer mirroring the income-statement/pdf pattern. - Migration 20260516170000: arsredovisning_signature_requests with RLS, signed-immutability trigger, and DELETE policy that blocks signed rows. Signature-service exposes list/create/markSigned/isFullySignedOff. - BankID call itself is not wired here — the table + service make the request layer available so a follow-up can hook lib/auth/bankid.ts to the sign action without rework. - /api/.../arsredovisning (data + pdf + signatures) endpoints. - /bookkeeping/year-end/arsredovisning page with editable narrative, flerårsöversikt table, signer slots, PDF download, and Bolagsverket Mina Sidor link. Explicit warning about the FY2026 iXBRL mandate. PHASE 6 — Enskild firma NE-bilaga UI - lib/bokslut/enskild-firma/{egenavgifter,rantefordelning,periodiseringsfond -ef,expansionsfond}-calculator.ts. All declaration-only — never produce a journal entry. Egenavgifter 28,97 % / pensionärssats 10,21 % / passive SLP 24,26 %, schablonavdrag 25/10/20 %. Räntefördelning SLR+6 / SLR+1 with the -500 000 negative threshold. P-fond EF cap 30 % (vs 25 % för AB). Expansionsfond 125,94 % av kapitalunderlag, 20,6 % skatt. - EfDeclarationSection mounted inside DispositionsStep when entity_type is enskild_firma — live recompute as the user adjusts kapitalunderlag, prior-year amounts, p-fond desired, expansionsfond change. Each card shows the NE-bilaga ruta the number lands in. NE-bilaga preview link. PHASE 7 — Agent-native MCP tools - gnubok_propose_dispositioner: read-only AB dispositions proposal - gnubok_propose_accruals: read-only accruals proposal - gnubok_propose_annual_depreciation: read-only depreciation per asset - gnubok_post_annual_depreciation: stages depreciation commit (high-risk) - gnubok_preview_arsredovisning: structured K2 ÅR preview - gnubok_preview_ef_declaration: EF skattemässiga justeringar preview - All ≤280-char descriptions, additionalProperties:false, conform to STAGED_OPERATION_SCHEMA for write tools. Read tools return the same shapes as their HTTP counterparts so agents and the UI share a contract. - The existing GET /bokslutsdispositioner endpoint now calls the shared buildDispositionsProposal helper that the MCP tool also uses, removing the duplicate logic that lived in both. Verification - 149 unit tests pass (was 125 on Phase 1-3; +24 across accruals, EF calculators, expansionsfond, periodiseringsfond-ef) - Zero lint or typecheck errors on any new file - Migration 20260516170000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bokslut): address PR #509 round-1 — CI blocker + 3 P1s + compliance bundle CI BLOCKER (output-schema test) - The 5 new read-only MCP tools (gnubok_propose_dispositioner / _accruals / _annual_depreciation, _preview_arsredovisning / _ef_declaration) were missing outputSchema, which the existing strict-schemas guard reads as a hard failure on core-only CI. Added a permissive { type: 'object', additionalProperties: true } outputSchema to each — the return shapes are the same as their HTTP counterparts and trying to mirror them inline would duplicate the type tree across two boundaries. P1 — vacation accrual: two real bugs in one entry - Delta was anchored on the OPENING balance of 2920, so any mid-year postings (partial accruals, reversals) were ignored. Now anchors on the current closing balance via tb.rows.find(2920).closing_credit - closing_debit. Updated the computation field label to current_2920. - More importantly: 2920 is a balance-sheet carry-forward (semesterlöneskuld persists until the actual vacation is paid). The original implementation set reverses_on to Jan 1 of the next year, which would zero the liability on day 1 of the new year — a known Swedish bookkeeping error. The vacation proposal now ships with reverses_on = '' to suppress the reversal badge, the API route emits a "Bokslutsjustering" description instead of "Periodisering (vänds …)", and AccrualsStep renders "Rullas vidare (ingen vändning)" so the user knows the liability carries forward. P1 — signature_requests immutability gap (security) - The existing trigger only guarded role / signer_name / signed_at / status on signed rows, leaving bankid_signature_data, signer_personnummer_encrypted, and signer_personnummer_hash mutable. An UPDATE on a signed row could silently alter the BankID proof. New migration 20260517090000 replaces the trigger function to cover the full audit-critical column set (plus fiscal_period_id and company_id as belt-and-braces). P1 — narrative edits never reached the PDF - The /bookkeeping/year-end/arsredovisning page let the user edit description / important_events / resultatdisposition but the download link pointed at a plain GET that regenerated boilerplate. Wired the PDF endpoint to accept description / events / disposition as query params (length-capped) and the page now constructs the download URL with the current narrative state — only fields the user actually changed are included, keeping the URL short for the unchanged-defaults case. Compliance quick wins - Added period_lock check to gnubok_post_annual_depreciation MCP tool (matches the existing accruals POST guard). - Added explicit fiscal-period ownership pre-check to the signatures POST route (RLS would reject anyway; the route layer just makes the 404 envelope cleaner). - Replaced free-text role on the signatures schema with an enum allowing only Styrelseledamot / Styrelseordförande / VD / Verkställande direktör. - Added Cache-Control: no-store + Pragma: no-cache to the ÅR PDF response so the document (officer names + financials = personal data) isn't cached by any intermediary. - Sanitized period_end in the PDF Content-Disposition header to dodge header-injection via stray chars (defensive — period_end is a date, but the cost is one regex). - Softened the iXBRL warning text on the ÅR page: digital filing is proposed by Bolagsverket but not yet enacted; PDF is still valid today. False positives I'm intentionally not chasing on this round - Greptile P2 `sate` typo in the address city field — the rename would touch the type and every consumer; defer. - Greptile P2 "computation field label" — already addressed as part of the P1 vacation fix above. - Compliance V2.2 "silent skip" in accruals POST — the silent skip is in the UI's empty-row filter, not in the server route. Server validation already returns 400 via Zod for any invalid item. Verification - 154 unit tests pass (was 149; +5 from re-running the MCP strict-schemas + output-schema suites that now include the new tools) - Zero typecheck errors on any touched file - Zero lint errors on any touched file - Migration 20260517090000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bokslut): address PR #509 round-2 — 3 real bugs + 5-item polish 3 real bugs from the round-1 Swedish review + Compliance Swarm re-eval: - Soliditet inflated by obeskattade reserver. build-data.ts filtered equity with startsWith('20') || startsWith('21') — but 21xx (periodiseringsfonder, överavskrivningar) are partially deferred tax, not eget kapital. Splitting them out per K2 / ÅRL. Filter is now startsWith('20') only. - Resultaträkning omitted bokslutsdispositioner + skatt rows. K2 RR must include 88xx (dispositioner) and 89xx (skatt) before "Årets resultat" per ÅRL 3:2 — without them, the printed RR doesn't reconcile to BS 2099 and the document is non-compliant for any AB that posted bolagsskatt or periodiseringsfond. flattenIncomeStatement now splits the financial sections on title (Bokslutsdispositioner / Skatter och årets resultat) and emits the K2-required intermediate subtotals: "Resultat efter finansiella poster" → dispositioner → "Resultat före skatt" → skatt → "Årets resultat". - Accruals POST had no idempotency. Re-running the wizard (or a retried POST after a flaky network) would create duplicate accrual entries that distort both the balance sheet and trial balance. New findExistingAccrualEntry helper queries the period for an existing posted entry whose description matches the kind's stable prefix (or for manual prepaid/accrued, the user-supplied description). Duplicates land in a new `skipped` array in the response with a reference to the existing entry id, rather than producing a second posting. 5-item compliance polish: - signer_name on the signatures schema now has .max(200) per GDPR Art.25.2 data-minimization. Swedish personal names are well under that — the bound is a defense against an unbounded-string injection. - Audit-fee accrual: 6420 is BAS-specific to lagstadgad revision. Bokslut fees for a non-revisionspliktigt bolag (liability_account = 2991) now debit 6590 (övriga externa tjänster) instead — Skatteverket may query a 6420 debit when there is no revisor i bolaget. - Räntefördelning ne_ruta label: was 'R30 / INK1 kapital' (confusing — INK1 is a separate form). Positive now reads 'R30 (avdrag i näringsverksamhet)' with the INK1 T4 cross-reference moved to the description; negative reads 'R30 (tillägg till resultat)'. - gnubok_post_annual_depreciation MCP tool now checks the caller's company_members role and throws on viewer. RLS would reject the underlying INSERT anyway; failing fast here produces a cleaner error than the cascaded RLS rejection. Mirrors the HTTP route's { requireWrite: true } guard. - Signature DELETE policy now blocks both 'signed' AND 'declined' rows. A declined signature is auditable evidence (board member refused) and is material under ABL 8 kap. New migration 20260517100000. Bot-flagged items I'm deliberately not chasing on this round: - V8.2.1 × 2 cross-tenant findings on ÅR routes — same false-positive class I've responded to repeatedly: buildArsredovisningData internally filters by company_id, bot can't see past the route handler. - V4.5 / V2.2 MCP arg Zod redundancy — MCP server's central handler validates against each tool's inputSchema. - Narrative-in-URL GDPR concerns — proper fix is POST + body or server-side persistence; tracked as follow-up. - `sate` typo (Greptile P2) — type-wide rename, deferred. - Vacation avgifter age-tier rate split — inherited from upstream generateVacationLiability which doesn't expose age; needs upstream work. - Medelantal anställda proper monthly average — needs salary-run aggregation across the year, follow-up. - K2 noter aktiekapital + fastställelseintyg blocks — real K2 gaps, tracked as follow-up. Verification - 94 tests pass (subset for bokslut + MCP suites; full Phase 1-7 suite unchanged in scope) - Zero typecheck errors on any touched file - Zero new lint errors on any touched file (the 2 server.ts warnings are pre-existing) - Migration 20260517100000 applied to remote Supabase via MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
05078c9d8e |
feat(bokslut): year-end wizard with bokslutsdispositioner + asset register (#508)
* feat(bokslut): year-end wizard with bokslutsdispositioner + asset register
Ships the first user-visible bokslut surface for K2 aktiebolag. The year-end
engine, INK2/INK2R/INK2S generator, and reconciliation reports already existed
in lib/core/bookkeeping/ and lib/reports/; this work wires them into a real
multi-step UI, adds the missing dispositioner calculators (bolagsskatt,
periodiseringsfond, överavskrivningar, SLP), and introduces a fixed-asset
register that feeds planenliga avskrivningar into the same flow.
PHASE 1 — Wizard around the existing year-end engine
- Replaces the "Kommer snart" stub at /bookkeeping/year-end with a 4-step
wizard (Kontroll → Dispositioner → Förhandsgranska → Verkställ) plus a
Klart result view
- New aggregator lib/bokslut/readiness-aggregator.ts composes
validateYearEndReadiness with bank-reconciliation status and entity-typed
reminders into one fetch backing the preflight step
- New endpoint GET /api/bookkeeping/fiscal-periods/[id]/bokslut-readiness
PHASE 2 — Bokslutsdispositioner calculators
- lib/bokslut/tax-provision/{bolagsskatt,sarskild-loneskatt}-calculator.ts —
20.6 % on taxable result → 8910/2512 (with non-deductible / non-taxable
manual adjustments and schablonintäkt pass-through) and 24.26 % SLP on
posted pension costs → 7533/2514
- lib/bokslut/reserves/periodiseringsfond-service.ts — proposeAvsattning
(25 % cap, BAS 212X cohort accounts) + proposeAteforing (FIFO 6-year
mandatory reversal with schablonintäkt computation) + balance lookup
- lib/bokslut/reserves/overavskrivningar-service.ts — 30-rule + 20-rule
helpers + proposeOveravskrivningar (8853/2153)
- New endpoint /api/bookkeeping/fiscal-periods/[id]/bokslutsdispositioner
(GET ordered proposals, POST commits user-chosen ones as separate
year_end vouchers via the journal engine)
- New DispositionsStep UI: per-card accept/skip + editable amount where
meaningful; mandatory p-fond reversals can't be skipped
- INK2 bug fix: ink2-engine.ts SRU mapping ranges previously pointed at
accounts BAS doesn't seed (8810/8830/8840). Corrected to 8811 (avsättning),
8819 (återföring), 8830 (lämnade koncernbidrag) so calculator output now
flows into INK2 correctly. Regression-locked with 6 new mapping tests.
PHASE 3 — Anläggningsregister + depreciation engine
- New migration 20260516120000_assets_and_depreciation.sql: assets table
(category, BAS-triple, K3 components JSONB reserved) and
depreciation_schedules (asset+period+journal_entry link). RLS via
user_company_ids(), immutability triggers after disposal/posting.
- lib/bokslut/assets/asset-service.ts — CRUD + disposal that posts a proper
gain/loss entry against 3973/7973
- lib/bokslut/assets/depreciation-engine.ts — computeAnnualDepreciation
(linear, pro-rata at acquisition/disposal/end-of-life) +
proposeAnnualPostings + commitAnnualPostings (one entry per asset)
- New endpoints /api/assets (CRUD + dispose) and
/api/bookkeeping/fiscal-periods/[id]/depreciation (preview + commit)
- /assets list+create page with K2 schablon defaults (3y datorer,
5y inventarier, 25y byggnader); sidebar entry added
- DepreciationPanel mounted at the top of DispositionsStep; posting
refreshes dispositions so bolagsskatt picks up the new result
Out of scope (per the agreed plan): K3 framework, iXBRL filing to
Bolagsverket (manual export only for now — regulatory risk flagged for
FY2026 closings), inventory module, koncernredovisning, revisor workflow.
Verification
- 116 unit tests pass across lib/bokslut/, lib/reports/ink2/, and the
existing lib/core/bookkeeping/year-end-service suite
- Zero lint or typecheck errors in any new file
- Migration applied successfully via Supabase MCP
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address PR #508 review — P1 correctness + P2 conventions
P1 — stale reminders in readiness-aggregator (greptile)
Remove the depreciation_manual / bolagsskatt_manual / periodiseringsfond_manual
nudges. After Phase 3, the wizard handles all three automatically via
DepreciationPanel and the dispositioner calculators — surfacing them as
manual reminders told users to repeat work the page already did. Only the
accruals_manual reminder (Phase 4 hasn't shipped) and the EF-only
ef_skatt_via_ne reminder remain.
P1 — netBookValueAfter ignored prior accumulated depreciation
proposeAnnualPostings now fetches all prior posted depreciation_schedules
for the company (excluding the current period) and sums them per asset, so
the displayed restvärde reflects every previously-booked year of avskrivning
instead of only this year. Without the fix, a 5-year asset in year 3 would
have shown 48 000 instead of the correct 24 000 net book value.
P1 — ordering bug in dispositioner POST handler
The 25 % p-fond avsättning cap derives from the current trial balance, so
mandatory återföring entries must post first. Added a server-side sort by
canonical bokslut order (återföring → överavskrivningar → avsättning → SLP
→ bolagsskatt) regardless of the client array order. The cap can no longer
be evaluated against a stale pre-återföring net result.
P2 — depreciation_schedules missing updated_at
New migration 20260516140000_depreciation_schedules_updated_at.sql adds the
column + trigger via update_updated_at_column(). Per CLAUDE.md migration
conventions, never modified the original migration. DepreciationSchedule
type updated.
P2 — addMonths end-of-month overflow
Replaced setUTCMonth (which overflows: Jan 31 + 1 month → Mar 3) with a
day-clamping implementation that produces Feb 28/29. Without the fix,
lifeEndExclusive landed one day too late and slightly over-depreciated.
New regression test asserts Jan 31 + 12 months stays in January.
P2 — pg-real tests for new triggers and RLS
tests/pg/assets.pg.test.ts (13 tests) covers:
- enforce_asset_post_disposal_immutability blocks every financial field
after disposal, allows notes/name through
- assets_disposal_atomic CHECK requires both disposed columns set together
- enforce_depreciation_schedule_immutability blocks edits after
journal_entry_id is linked, allows them before
- depreciation_schedules delete RLS policy filters out posted rows
- assets + depreciation_schedules RLS isolates across companies
Verification
- 117 unit tests pass (was 116, +1 for the addMonths regression)
- New pg-real suite syntactically + type-correct; will execute in CI
- Zero lint or typecheck errors in any touched file
- Migration 20260516140000 applied to remote Supabase via MCP
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance-swarm + swedish-review findings
Real bugs surfaced by the bots on the first push that weren't covered by
greptile's inline P1/P2 set:
- Immaterial asset disposal hit the wrong BAS accounts. disposeAsset always
posted gain/loss to 3973/7973 regardless of category. For category
'immaterial' it must use 3013 (vinst) / 7813 (förlust) per BAS — using the
tangible accounts misclassifies in INK2R. Now branches on category. Two
new regression tests pin each branch.
- acquisition_cost CHECK was too loose. CreateAssetSchema accepted 0 (just
nonnegative). Tightened to z.number().positive() — a zero-cost asset
creates a no-op depreciation row and a balance sheet line that nothing
reconciles against.
- UpdateAssetSchema let users remap BAS accounts arbitrarily. Bot flagged
this as a defense-in-depth gap (V4.5). Added BAS_RANGES_BY_CATEGORY
validation at both the schema layer (Create) and the service layer
(Update) so user-supplied account overrides must stay inside the
category's expected BAS range. INK2R mappings and the depreciation
engine's category-driven defaults now can't drift.
Swedish accounting review:
- Building/markanläggning defaults — clarified UI copy. The 25-year
schablon is K2-redovisning, not the IL skattemässig rate. New helper text
spells this out. Markanläggning default lowered from 20→10 years
(Skatteverket guidance allows 10 % rate; 20 was on the upper bound
without justification).
- createAsset doesn't post the acquisition entry by design — that gap
wasn't called out anywhere in the UI. Added a tip box in
CreateAssetDialog explaining that the acquisition must already be in the
books; the register only drives depreciation.
- Disposal VAT (ML 3:3 / 7:3) not handled — sale of a deduct-eligible
anläggningstillgång is in principle 25 % momspliktig. Documented this as
a known limitation in the disposeAsset docstring so any future UI
surfacing the disposal endpoint warns the user.
Documented (not fixed yet) — bot was right but wider-scope work:
- SOC 2 PI1.3: dispositioner POST loop is not transactional. A failure
midway leaves partial postings. Added a code comment explaining the
recovery path (re-POST omitting committed kinds — each calculator
re-derives from current TB). Real atomicity via an RPC wrapper is Phase
5+ work.
False positives intentionally not changed:
- 4× OWASP V8.2.1 cross-tenant findings — service functions already filter
by company_id; the bot can't see past the route handler.
- V2.3 client-supplied amount clamping — proposeAvsattning and
proposeAteforing both clamp via Math.min already.
- A.8.15 audit events — withRouteContext already logs completion.
- Schablonintäkt journal entry — per IL 30:6a it's a skattemässig
justering, never booked. Current implementation is correct.
- Voucher series 'A' — matches existing executeYearEndClosing convention;
not changing here in isolation.
Verification
- 119 tests pass (was 117, +2 for the immaterial-disposal branches)
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance round 4 — BAS account overlap + method gate
Two real bugs the compliance bot caught after my round-3 fixes (both new on
its re-run, not in the original report):
- BAS account overlap not prevented: my BAS_RANGES_BY_CATEGORY uses the same
class range for asset and accumulated (e.g. immaterial: both 1010–1099,
building: both 1100–1199). Nothing stopped a user from picking the same
account for both, which would silently net acquisition cost against
accumulated depreciation in one bucket and corrupt INK2R 720x mappings.
CreateAssetSchema now rejects bas_asset_account === bas_accumulated_account
in a superRefine cross-field check; updateAsset enforces the same invariant
by reading the existing asset and validating the merged result.
- declining-balance methods silently fell back to linear. The DB enum allowed
declining_balance_30 / declining_balance_20, but the engine's
computeAnnualDepreciation only implements linear math. A determined caller
(MCP, curl, future UI) could create an asset labelled as räkenskapsenlig
avskrivning and get linear charges — silently wrong numbers under a
misleading method. Both CreateAssetSchema and UpdateAssetSchema now refine
the depreciation_method enum to require 'linear'. The DB enum stays open
for a future phase to add proper support. Stale comment in
depreciation-engine.ts updated to reflect the new invariant.
False positives I'm explicitly not chasing further on this round:
- 3× repeated OWASP V8.2.1 cross-tenant — services already filter by
company_id; bot can't see past the route handler. Round 3 already added
service-layer tests and inline reasoning.
- V2.3 atomicity upgrade to high — bot now flags it harder *because* I
documented it in round 3. The existing executeYearEndClosing has the same
non-transactional sequential-write pattern; wrapping just this endpoint
in an RPC while leaving the rest inconsistent is worse than the doc
comment. Real atomicity is Phase 5+.
- Disposal VAT user-facing warning — no UI surfaces dispose yet; docstring
in the service is sufficient until the UI ships.
Verification
- 119 tests pass
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance round 5 — disposal integrity + öavskr accounts
Round-5 bot feedback after my round-4 push (the sticky reports re-edited
themselves with two new genuine findings; bot count: 18 → 14 active).
P1 — accumulated_depreciation was client-supplied (OWASP V8.2.1, swedish
compliance review): the dispose endpoint accepted accumulated_depreciation
as a request-body number. A malicious or buggy caller could inflate it to
manipulate the book-value calculation and pocket a phantom gain. Now:
- DisposeAssetSchema no longer accepts accumulated_depreciation
- disposeAsset sums planned_depreciation from depreciation_schedules where
journal_entry_id IS NOT NULL for the asset, server-side
- New regression test "server-derives accumulated_depreciation — caller
cannot inflate gain" pins the server-derivation against the prior attack
- Limitation: manual avskrivningsverifikationer posted outside the engine
aren't captured. Phase 5+ can swap this for a trial-balance scan on
bas_accumulated_account if that gap matters.
P2 — överavskrivningar hardcoded 8853/2153 regardless of asset category
(swedish-asset-accounting): for buildings BAS uses 8852/2152 and for
immateriella tillgångar 8851/2151. Edge case for K2 SME (öavskr on
buildings is rare; on immateriella rarer still) but worth not lying about
the accounts. Now:
- New OVERAVSKRIVNING_ACCOUNTS table maps category → expense/accumulated
pair (machinery_equipment, building, immaterial, group)
- proposeOveravskrivningar accepts optional category, defaults to
machinery_equipment (the dominant K2 case — no behaviour change for
existing callers)
- POST handler item schema accepts optional category
- Label + description strings now name the actual accounts used
- 3 new tests cover the building, immaterial, and default branches
False positives I'm still declining to chase (already covered in prior
commit messages):
- 3× repeated OWASP V8.2.1 cross-tenant — services scope by company_id;
bot can't see past route handler
- V2.3 atomicity — existing executeYearEndClosing has the same pattern;
wrapping just this endpoint is inconsistent; real fix is Phase 5+ RPC
- Disposal VAT user-facing warning — no UI surfaces dispose yet
Verification
- 123 tests pass (was 119, +3 for öavskr category branches and +1 for the
server-derivation regression test, with one prior test rewritten to use
the new server-supplied accumulated path)
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): pre-merge polish — building disposal accounts, SLR-2026 rate
Last polish round before merge. All three from the round-5 sticky
Swedish-accounting-review update:
- Building / markanläggning disposal posted gain/loss to 3973/7973. BAS
2026 routes those to 3971/7971 (the SRU mapping points them at a
different INK2R field, so the existing accounts misclassify). Extended
the existing immaterial branch (3013/7813) into a three-way:
immaterial → 3013 / 7813
building / land_imprv → 3971 / 7971
other tangible → 3973 / 7973
Two new regression tests pin the building and land_improvement branches.
- DEFAULT_SCHABLONINTAKT_RATE was 0.03, based on SLR 2024-11-30 (1.96 %).
For closings of inkomstår 2026 the rate is SLR 2025-11-30 (2.55 %) + 1 pe
= 3.55 %. The wrong rate under-taxes the schablonintäkt, which feeds into
bolagsskatt. Updated to 0.0355 and rewrote the doc comment to track both
years so the next bump is obvious.
- Jämkning of input VAT for buildings / markanläggning disposed within the
10-year jämkningsperiod (ML 9 kap 8–11 §§) is out of scope for this PR
but should not be silently absent — added a KNOWN LIMITATION block to the
disposeAsset docstring so any future UI surfacing disposal checks the
10-year window and warns the user.
After this push the PR has 125 passing tests, all CI green, no merge
conflicts, and the only remaining bot complaints are repeat false
positives or Phase 5+ scope (RPC atomicity, full asset disposal UI,
K3 component depreciation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|