Commit Graph
6 Commits
Author SHA1 Message Date
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

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

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

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

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

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
Jakob WennbergandClaude Opus 4.8 2da9c71eb3 UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns:

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

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

All CI green; compliance bots report no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:29:23 +02:00
Jakob WennbergandClaude Opus 4.8 46039f14f4 fix(reports): valid two-file NE-bilaga SRU submission (#318, #319) (#844)
* fix(reports): generate valid two-file NE-bilaga SRU submission (#318, #319)

The NE-bilaga "Ladda ner SRU" export produced a file Skatteverket rejects: it
was served as UTF-8 text/plain (å/ä/ö mojibake, #319) and was structurally
invalid — a single blob with #PRODUKT KONTROLLUPPGIFTER (the KU code), no
INFO.SRU/BLANKETTER.SRU split, a #SKAPAT typo, no #FIL_SLUT, and suspect field
codes 7310–7350 (#318).

Rewrite the generator to mirror the working INK2 generator: a two-file
INFO.SRU + BLANKETTER.SRU submission, ISO 8859-1 encoded and zipped, with
#PRODUKT SRU, #DATABESKRIVNING_*/#MEDIELEV_*, #BLANKETT NE-<år>P<x>,
#IDENTITET <personnummer12> <date> <time>, and #FIL_SLUT. Field codes use the
authoritative BAS NE_EJ_K1 coupling table (R1→7400 … R10→7505, R11→7440;
period dates 7011/7012). Enskild-firma identity is the owner's 12-digit
personnummer (birth-century prefix, not INK2's juridisk-person "16").

- Extract the shared ISO-8859-1 encoder to lib/reports/sru-encoding.ts (was
  inline in the INK2 route).
- Extend the NE engine/types to carry address/postort/email for INFO.SRU.
- Frontend: NE SRU download uses the INK2 blob pattern; fix a pre-existing
  param bug in EfDeclarationSection (fiscal_period_id → period_id, +format=sru).
- Add generator tests (structure, BAS field codes, zero-omission, ISO-8859-1).

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

* fix(reports): address review feedback on NE-bilaga SRU generator (#318)

- getZipFilename uses the income year (fiscal year END) so the filename matches
  the blankett type/identity for broken fiscal years.
- Refuse to generate a submission when the personnummer is missing/invalid
  (compute + validate the 12-digit identity once in generateNESRUSubmission and
  throw) instead of silently emitting a placeholder #IDENTITET that Skatteverket
  would reject after upload.
- validateBlanketterSru now asserts the mandatory räkenskapsår date fields
  (#UPPGIFT 7011/7012) — their absence is a level-2 rejection.
- 10-digit personnummer century is inferred from adult age (≥18, <110) at the
  income year, fixing the e.g. 1924-born/yy=24 edge that mapped to 2024.

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

---------

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

* delete docs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:29:41 +02:00
Jakob WennbergandClaude Opus 4.7 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>
2026-05-17 14:50:48 +02:00