64ea0fef0200e38fdbd142ec2a646d2db5cec6f4
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
f8504f3bd0 |
fix: audit batch — pagination truncation, MFA/dead-code cleanup, mark-paid fail-closed (#841)
* fix(reports): paginate 8 more report/ledger queries (1000-row truncation) Raw .select() without fetchAllRows() silently caps at PostgREST's 1000-row limit, producing wrong statutory output for high-volume companies. Following #806 (trial-balance/VAT), wrap the remaining offenders in fetchAllRows + a stable .order('id') + dedupeBy: - ink2-engine / ne-engine: INK2 & NE-bilaga tax declarations under-counted - ar-reconciliation (1510/1513), supplier-reconciliation (2440): phantom "Ej avstämd" gaps - full-archive-export: 7-year DR archive (added a unique total order so rows are not silently skipped/duplicated across pages) - avgifter-basis, currency-revaluation, vat-declaration Adds a regression guard test asserting >1000 ledger lines are summed, not truncated at 1000. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): close extension-dispatcher MFA gap, scope /api/events to API key, sweep dead code Security/correctness: - ext/[...path] dispatcher now uses requireAuth() instead of inline supabase.auth.getUser(), enforcing MFA (AAL2) on hosted across the whole enabled-extension surface (banking sync, document upload/booking, supplier invoices, migration). Ratchets antipatterns-baseline raw-route-auth 168->165. - /api/events now filters by the API key's bound company_id instead of the user's active company (was a cross-company read with a scoped key). - enable-banking OAuth callback calls ensureInitialized() at module load so the PSD2 consent audit event (ASVS V16 / GDPR Art.30) isn't dropped on a cold-start instance. Dead-code sweep (all confirmed zero importers): - delete lib/tax/calculator.ts, lib/salary/engangsskatt.ts (+test), lib/email/resend.ts, lib/salary/salary-transaction-matcher.ts, lib/webhooks/diff.ts, lib/salary/effective-values.ts, lib/bookkeeping/template-prompt.ts - trim unused lib/vat/eu-countries.ts helpers (keep EU_COUNTRIES) - remove dead getAutomaticStatus() and the abandoned Activepieces CSP entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): fail closed when a payment journal entry doesn't post Three mark-paid paths (legacy route, v1 API, agent commit) diverged on the "mark paid but the JE failed" case — two would flip the invoice to paid (or leave an orphaned posted voucher) with no booking, silently diverging the GL from the AR/AP sub-ledger. Unify on fail-closed: - legacy + v1 + agent commitMarkInvoicePaid: never mark paid without a posted voucher; on a null/failed JE return INVOICE_PAID_BOOK_FAILED before any state mutation (v1 mirrors the match-invoice strict mode). - agent path: add the .in('status',[...]).select('id') CAS guard and cancel the orphaned voucher (cancelOrphanedPaymentEntry) on a lost race or update error, matching the web route. - legacy route: cancel the orphan on a non-race update error too (was only handled on the race branch). - supplier mark-paid: stop swallowing a failed supplier_invoice_payments insert — that row drives the reversal amount in payment-sync; roll back the status flip and cancel the voucher instead. - pending-ops orchestrator: error-check the terminal 'committed' write so an op stranded in 'committing' (the expire sweep only targets 'pending') is at least logged loudly. Adds a guard test for the legacy fail-closed path. Full unit suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): unblock core build + address compliance-review findings - avgifter-basis.ts: fix the core-build TypeScript error — PostgREST's type-level select parser models the salary_run embed as an array, which wasn't assignable to the object-typed generic. Type it `unknown` (rows are read via an explicit cast), making it robust across postgrest-js versions. - /api/events: add a non-null companyId guard before the event_log query (defense-in-depth for the API-key-bound scope) — addresses ASVS V8.2.1 / ISO A.5.15. - supplier mark-paid: add a CAS guard (.eq('status', newStatus)) to the payment-insert-failure rollback so a concurrent settlement can't be clobbered — addresses ASVS V2.3. - dispatcher: add an AAL2 regression test asserting a non-MFA session is rejected (403) and the extension handler never runs — addresses the GDPR Art.32 review ask for the single extension chokepoint. Verified deletions are safe: effective-values.ts was a dead duplicate — the live AGI/payslip path inlines the same `?? override` coalescing (generate-declaration.ts), so AGI correctness is unaffected. next build: exit 0. Full unit suite: 6147 passing. ESLint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fce6faff2c |
fix(api): stabilize report pagination + declare real { data, meta } envelope on v1 single/write endpoints (#811)
* fix(reports): stabilize fetchAllRows paging to stop doubled/dropped balances (#790, #791) PostgREST `.range()` paging is only correct when the underlying query has a stable TOTAL order. Several aggregating report queries (general ledger, trial balance, grundbok, supplier/AR ledgers, etc.) paginated without `.order()`, so on datasets larger than one 1000-row page Postgres could return rows in a different order between requests — silently DUPLICATING or SKIPPING rows on a page boundary and doubling or dropping financial totals. - fetch-all.ts: document the ordering invariant and add an optional `dedupeBy` defense-in-depth that drops cross-page duplicates and warns when it fires (surfaces a missing `.order()` in logs instead of corrupting money). - Add a stable `.order()` (line PK or account_number) to every paginated query in lib/reports/ and the account-balances route; pass `dedupeBy` on the money-aggregating line queries. - Add fetch-all unit tests and update report test fixtures to carry row ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): declare the real { data, meta } envelope on v1 single/write/204 endpoints (#794) The OpenAPI generator derives each endpoint's documented body purely from its registered `response.success` Zod schema, and that schema is never validated at runtime — so a route could advertise a shape its handler never sends. #802 fixed this for list endpoints; the same drift was latent on single-resource and write endpoints, which declared the bare resource schema instead of the `{ data, meta }` envelope the handlers actually return. - registry.ts: extend `ResponseMetaSchema` with the optional `audit` block and `partial_expansions` list that writes/expansions emit; add the `NoBodyResponse` sentinel so 204 DELETE handlers document a bare 204 instead of a phantom 200. - Wrap every single/write endpoint's `response.success` in `dataEnvelope(...)` (or `NoBodyResponse` for 204s) across the v1 routes. - Add a response-envelope contract test that fails CI if any JSON endpoint forgets to wrap its schema, with binary downloads and 204s as the only exemptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): extend paging dedupeBy to rc-basis-gaps and opening-balances Address PR review: these two money-aggregating line queries already had the stable `.order('id')` (so paging was correct) but didn't carry `id` in the select, so they couldn't use the `dedupeBy` defense-in-depth that general-ledger and trial-balance got. Select `id` and pass `dedupeBy: r => r.id` so the whole report layer applies the ordering invariant consistently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (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>
|
||
|
|
3fa871c742 |
Bug/accounting suggestion (#456)
* feat: add bike benefit handling and optional vacation accrual - Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value. - Updated schemas to include new benefit types and validation rules. - Implemented API routes for creating, updating, and deleting employee benefits. - Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual. - Added UI components for managing employee benefits, including input for bike benefit specifics. - Created database migrations for employee benefits and updated salary line items to support new benefit types. * chore: remove Langfuse env var checks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: enhance OAuth callback URL handling and update default scopes for Visma integration * feat: remove trade_name field and simplify company naming in invoices * refactor: destructure canWrite from useCanWrite for consistency across components * feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates * feat: add missing label for bike benefit in salary line item types --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
064fb7f7a9 |
Add/white label (#381)
* feat(branding): add BrandingService with default-preserving env layer Introduce lib/branding/service.ts mirroring lib/email/service.ts. Defaults match current gnubok values exactly, so production behaviour is unchanged unless an env var (NEXT_PUBLIC_BRANDING_*, BRANDING_*) or extension override (via registerBrandingService) is set. Resolution order: defaults < env vars < extension override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(branding): route root layout, manifest, and PWA assets through branding service - app/layout.tsx now reads title, description, themeColor, and apple-touch-icon from getBranding() instead of hardcoded values. - public/manifest.json replaced by dynamic app/manifest.ts so PWA name, short_name, description, theme_color, background_color, and icon paths are resolved at request time. The manifest now serves at /manifest.webmanifest (Next.js convention for the metadata file route). The previous /manifest.json URL is no longer populated; nothing in core references it after this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(branding): route email service and templates through branding service - resend-service.ts: From line uses getBranding().appName instead of hardcoded "Gnubok" in both the with-fromName and bare cases. - invite-templates.ts: subject, HTML header, body, plain text, and the team-invite variants all read from branding (sentence case in prose, uppercased for the styled <p> header). - consent-notification-templates.ts: signature fallback (companyName || branding) for both HTML and plain text variants. Defaults preserve the exact current strings ("Gnubok", "GNUBOK", "gnubok" in their respective contexts) so no email content changes for production. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(branding): route OAuth consent page through branding service The MCP OAuth consent page rendered for Claude Desktop / Claude.ai connector flows now reads the app name from getBranding() for both the HTML <title> and the body copy. Default still produces "gnubok" in lowercase prose, matching current behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(branding): route auth, dashboard, and onboarding text through branding service Replace user-visible "gnubok" / "Gnubok" references with calls to getBranding(). Touches: - Auth pages (login, register, mfa/enroll): logo src/alt, MFA TOTP friendlyName. - Onboarding (companies/new, invite, sandbox, WelcomeOnboarding, Step2CompanyDetails, NewUserChecklist, BankIdCompanyPicker, ArcimMigrationWorkspace): logo, headings, error/help text. - Dashboard fallback (companyName="gnubok") and settings (backup copy, ApiKeysPanel MCP connector name + login note, CompanyDangerZone, retention-notice). - API routes (support contact subject prefix, enable-banking consent email companyName fallback, AI inbox receipt-request appUrl, pain001 messageId prefix). - MCP server "open the gnubok web app" review message. - Salary/reports filings (AGI Programnamn, KU10 Programnamn, payslip footer, full-archive system metadata, SRU #PROGRAM line). Internal identifiers (cookie names gnubok-company-id / gnubok-invite-token, API key prefix gnubok_sk_, invite token prefix gnubok_inv_, MCP tool names, npm package gnubok-mcp, GNUBOK_API_KEY env name) are deliberately left unchanged — they're stable contracts that whitelabels must not break. Defaults match current behaviour exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(branding): support legal page field-level swaps for entity and contact Privacy and DPA pages now interpolate appName, legalEntity, and privacyEmail from the branding service instead of hardcoding "Gnubok", "Arcim", and "privacy@gnubok.se". Page metadata uses generateMetadata() so titles also reflect the brand. lib/support.ts now falls back to getBranding().supportEmail when SUPPORT_RECIPIENT_EMAIL is unset, so a single BRANDING_SUPPORT_EMAIL env var configures both the support form recipient and the displayed support address. Whitelabels with a different legal jurisdiction or entirely different DPA text should override the page route from an extension. Phase 1 intentionally only supports field-level swaps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(branding): add WHITELABEL.md and example branding extension WHITELABEL.md: fork checklist, env var reference, the "do not change" list (cookies, API key prefixes, invite token prefixes, MCP tool names, gnubok-mcp npm package, GNUBOK_API_KEY env name), out-of-scope items, the upstream sync workflow YAML to copy into a fork, conflict avoidance guidance, and a verification checklist. extensions/general/_example-branding/: copy-paste starter extension with index.ts (commented placeholder values for registerBrandingService), manifest.json, and README.md. Disabled by default (not added to extensions.config.json); whitelabels cp the folder, edit, and enable. sectors.test.ts: bumped expected extension count 12 -> 13 to account for the new starter extension on disk. The generated registry is unchanged because the example is disabled. The sync workflow YAML is documented inline in WHITELABEL.md rather than checked in as a workflow file. It's only meaningful in a fork -- gnubok itself has nothing to sync from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(branding): address PR review — lazy support email + escape brand in HTML/XML Three issues from code review: P1 — lib/support.ts: SUPPORT_RECIPIENT_EMAIL was a module-level const, evaluated at import time before extensions register branding overrides via ensureInitialized(). Convert to getSupportRecipientEmail() lazy accessor; update the only caller in app/api/support/contact/route.ts. Extension-supplied supportEmail values now route correctly. P2 — app/api/mcp-oauth/authorize/route.ts: appName was interpolated into the consent page HTML without escapeHtml(), inconsistent with the existing escaping of companyName. Wrap appName.toLowerCase() in escapeHtml() at use sites in <title> and the body paragraph. P2 — lib/salary/agi/xml-generator.ts and lib/salary/ku/ku10-generator.ts: appName placed inside <gem:Programnamn> / <Programnamn> XML elements without escapeXml(), the helper already used for other admin-controlled fields in the same files. Wrap accordingly to prevent malformed XML if a brand name contains XML reserved characters. All admin-controlled inputs only — no user-exploitable path. Defense in depth, not a known incident. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(branding): security follow-up — lazy metadata, SRU/email header sanitization Self-audit after the PR review surfaced four more concerns. Fixes them with the same defense-in-depth posture as the prior review fixes. 1. app/layout.tsx — same eager-evaluation class as P1 support.ts. The module-level `const branding = getBranding()` froze branding before extensions registered, so extension-based overrides for title, description, themeColor, and apple-touch-icon silently never applied. - Convert to generateMetadata() / generateViewport() (lazy, run per request, see extension-registered overrides). - Inline getBranding() inside RootLayout for the apple-touch-icon href so it picks up overrides too. - Add ensureInitialized() at module level so extensions are loaded before the first metadata call. Mirrors the API route pattern. 2. app/manifest.ts — same class. The dynamic manifest function reads getBranding() per request, but if the manifest is requested before any other module has triggered ensureInitialized(), extensions are still unloaded. Add ensureInitialized() at module level. 3. lib/reports/ink2/sru-generator.ts — appName interpolated into the SRU `#PROGRAM` directive without sanitization. SRU's reserved char is `#` (directive marker) and CRLF injects new directives. Wrap in the existing sanitizeString() helper to match the pattern used for other admin-controlled fields in this file (#NAMN, #ADRESS, etc.). 4. extensions/general/email/lib/resend-service.ts — appName and the user-controlled fromName both flow into the From header. Resend's API does its own validation, but defense in depth: strip CRLF and angle brackets via a small sanitizeHeaderPart() helper before building the header string. fromName was a pre-existing surface; appName is new with this whitelabel work. All four are admin-controlled inputs (env vars or extension code), not user-exploitable. No known incidents — defense in depth, and correctness for extension-based whitelabels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
24466c6e94 |
fix: correct INK2R SRU sign convention and field ordering (#249)
* fix: correct INK2R SRU sign convention and field ordering Cost fields (7511-7528) were negated to negative values, but Skatteverket expects positive values on the INK2R form. Updated the engine to keep debit-normal income statement balances positive and adjusted all result calculations accordingly. Also switched SRU field output from Object.keys() to canonical Skatteverket ordering arrays. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — deduplicate code arrays, add disposition test Remove duplicate ASSET_CODES/EQUITY_LIABILITY_CODES arrays from the engine and import the canonical arrays from types.ts. Add test coverage for bokslutsdispositioner fields (7524, 7525, 7419) to exercise the new subtraction sign path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
73fb97052b |
fix: resolve INK2/NE entity_type from companies table fallback (#217)
* fix: resolve INK2/NE entity_type from companies table fallback (#193) The entity_type check in INK2 and NE-bilaga engines read from company_settings where it is nullable, causing "only for aktiebolag" errors when the column is null. Now falls back to companies.entity_type (NOT NULL, always set). Reports page uses useCompany() context instead of /api/settings for tab visibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — surface fallback errors, avoid direct mutation - Surface Supabase errors in entity_type fallback queries instead of silently swallowing them (ink2-engine, ne-engine) - Use spread instead of direct mutation on Supabase result object (settings route) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use separate variable to avoid const reassignment in settings route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
bb336eba88 |
Fix/user feedback (#210)
* Add delete policies for provider consent tokens and provider OTC * Add trade name support for companies in settings and documents * Resolved currency selection issue * Enhance invoice line display with foreign currency support and update delivery date schema to allow empty values * Add currency display for journal entries and include currency metadata in transaction creation * Add trade_name column to company_settings for external display |
||
|
|
7a18d89c70 |
feat: INK2 declaration improvements, invoice delivery date & Swedish compliance skills (#204)
* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills Expand INK2 engine with full INK2S/INK2R support and improved SRU generation. Add delivery_date field to invoices and corresponding PDF/migration support. Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3 - Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse varuinköpskostnader) to SRU 7512 so they are not silently dropped from INK2R declarations - Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields - Document P3 period suffix limitation for brutet räkenskapsår Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511 Per the official BAS-to-SRU mapping, these account ranges are cost of goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6c79f21679 |
fix: INK2 rounding tolerance and clearer warnings (#197)
* fix: add rounding tolerance to INK2 balance check and clarify warnings INK2/SRU rounds each ruta independently to whole kronor, so with 11+ rutor the accumulated rounding can produce a 1-2 kr difference that triggered a false "balance sheet not in balance" warning. Add a 2 kr tolerance. Also clarify the unclosed fiscal year warning to indicate that generation still works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: also warn when equity/liabilities exist but assets are zero Address Greptile review feedback — the balance check guard should trigger when either side has a non-zero total, not only when assets > 0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0dd1f5ebc1 |
feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19) Introduce companies table, company_members, and user_preferences to support multiple companies per user. All data scoping changes from user_id to company_id across the entire codebase. Key changes: - Database migration: new tables, company_id on 40+ tables, backfill, RLS rewrite from user_id to company-member-based, updated RPCs - Types: Company, CompanyMember, CompanyRole, UserPreferences types; company_id added to all entity interfaces; companyId on all events - Engine: all 7 core functions take companyId; storno, period, year-end services updated; 16 report generators updated - Middleware: company context resolution (cookie → prefs → first company) - API routes: ~120 routes updated with requireCompanyId() - Frontend: CompanyProvider context, layout/dashboard/onboarding updated - Extensions: context factory, 9 extensions, all lib files updated - Tests: 1880 tests passing, all helpers updated with company_id defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add database migrations for multi-tenant company and team system (GNU-19) Adds company_invitations, company creation RPC, team_members, account deletion RPC, and teams table refactor migrations. Updates base multi-tenant migration with cascading FKs and onboarding_step column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team types and update core infrastructure for multi-tenancy (GNU-19) Adds TeamRole, MemberSource, and Team types. Refactors Supabase service client to be stateless, updates middleware for team-aware routing, extends CompanyContext with team/role fields, and updates extension service types to accept companyId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through business logic functions (GNU-19) Replaces user_id scoping with company_id across all lib modules: bookkeeping, documents, transactions, invoices, reconciliation, tax, deadlines, and import. Updates corresponding tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through API routes and extensions (GNU-19) Updates all existing API routes to extract and pass companyId. Updates enable-banking and arcim-migration extensions for company-scoped transaction ingestion and sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add company and team management API routes (GNU-19) Adds CRUD endpoints for company members, company invitations, team members, and team invitations. Includes invite token utilities, email templates, and company switch server action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team/company UI components, pages, and dashboard updates (GNU-19) Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company members and team management panels. Updates dashboard layout for team-aware routing, onboarding for multi-step role choice, and auth callback for team invite acceptance. Ignores supabase/.branches/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in import page (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move appUrl declaration to outer scope in invite route (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for second company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in extension components (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update tests to use companyId instead of userId and improve type handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3e82295cce |
feat: semi-manual invoice payment booking dialog (#37)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2) Reversed entries (storno) must appear alongside their original posted entries in reports for a complete audit trail. Previously, filtering by status='posted' excluded them, causing discrepancies when corrections had been made. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: semi-manual invoice payment booking with editable journal lines When marking an invoice as paid, users now see a dialog where they can: - Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.) - Review and edit the proposed journal entry lines before committing - The happy path remains fast — lines are pre-filled correctly Implementation: - Pure proposePaymentLines() function for line computation (accrual + cash) - PaymentBookingDialog with AccountCombobox, balance validation, date picker - API accepts optional custom lines, falls back to auto-generation without them - 18 tests (8 unit + 10 API) all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — validation fallback, balance check, error handling - P1: Return 400 on invalid body instead of silently falling back to auto-generated lines (split JSON parse from schema validation) - P1: Add server-side balance check for custom lines before committing (debit must equal credit, totalDebit > 0) - P2: Wrap PaymentBookingDialog init() in try/catch with toast on failure and auto-close instead of silent empty state - Add 2 new tests: unbalanced lines → 400, invalid schema → 400 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
29240738fa |
feat: add INK2 declaration, full archive export, AI consent gate, fix VAT declaration rutor
- Fix VAT declaration ruta mappings to match SKV 4700 form correctly (ruta 05 = total taxable sales, ruta 10/11/12 = output VAT per rate) - Add INK2 declaration report for aktiebolag with SRU export - Add full archive ZIP export for 7-year retention compliance - Add AI consent gate requiring user approval before AI extension API calls - Add DPA and privacy policy public pages - Add audit trail API routes - Update VAT registration threshold from 80k to 120k kr in onboarding - Update CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |