Commit Graph
12 Commits
Author SHA1 Message Date
Jakob WennbergandClaude Opus 4.8 679b154ad2 feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5) (#692)
* feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5)

Expose the complete Skatteverket extension as five MCP tools so VAT
(momsdeklaration) and employer (AGI/arbetsgivardeklaration) filing can be
driven from Claude. Commit = "send for BankID signing" (returns a signing
link), never "file" — the user's signature in the browser is the irreversible
act, kept outside the tooling.

Tools (extensions/general/mcp-server/server.ts):
- gnubok_vat_declaration_validate  (compliance:read) — live POST /kontrollera
- gnubok_vat_declaration_submit    (skatteverket:write) — stages submit_vat_declaration
- gnubok_vat_declaration_status    (compliance:read) — GET /inlamnat + /beslutat
- gnubok_agi_submit                (skatteverket:write) — stages submit_agi
- gnubok_agi_status                (compliance:read) — local state + live kvittenser

Architecture:
- Core (lib/pending-operations/commit.ts) cannot import @/extensions (CI guard),
  so the two submit ops dispatch into the extension via the new
  Extension.services channel (first use): registry-resolved
  commitSubmitVatDeclaration / commitSubmitAgi run the SKV chain and return a
  shared SkvSubmitResult (lib/pending-operations/skatteverket-commit.ts).
- Recoverable failures (extension disabled, no connection, rate-limited, still
  processing) release the op back to 'pending' via SkatteverketRecoverableError
  — same contract as AccountsNotInChartError — so the user reconnects and
  re-approves the SAME op. SKV business rejections reject the op.
- No-drift: parseDeclarationRequest / loadAGIXml extracted to
  lib/declaration-prep.ts (buildMomsuppgift / buildAgiUnderlag / resolveRedovisare)
  so route, preview, and commit file identical figures. writeSkatteverketAudit
  hoisted to lib/audit.ts; read tools + executors write BFL audit rows too.
- New scope skatteverket:write (opt-in, in STAGING_SCOPES so SoD ack fires),
  4 structured error codes, sv/en strings, ApiKeysPanel row.
- Migration 20260620120000 adds submit_vat_declaration / submit_agi to the
  pending_operations.operation_type CHECK (must apply to prod post-merge).

Tests: 42 new across executors, MCP tools, declaration-prep, error-map, and the
VAT commit chain. Full suite green (5287), build clean, lint-ratchet at baseline.

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

* ci: add PR-Agent AI review (SHA-pinned, dedicated Bedrock key)

Greptile went silent after #682 (app/account-side, not repo config). Add the
open-source PR-Agent GitHub Action as a replacement, hardened for supply chain:

- Pinned to the v0.36.0 commit SHA (ffe1f89), not the movable tag — the repo
  was recently transferred to a new, unverified org (The-PR-Agent), though it's
  the genuine original pr-agent (repo id 662766482, 11.5k stars).
- Runs on a DEDICATED, minimal IAM key (bedrock:InvokeModel only) via
  PR_AGENT_AWS_* secrets — never the app's general AWS credentials.
- Only /review runs automatically; /describe and /improve are disabled so PR
  descriptions are never overwritten.

Requires three new secrets before it functions: PR_AGENT_AWS_ACCESS_KEY_ID,
PR_AGENT_AWS_SECRET_ACCESS_KEY, PR_AGENT_AWS_REGION (EU region).

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

* ci(pr-agent): handle push events + restrict push to /review

PR-Agent skips synchronize (push) events by default, so the bot ran green but
posted nothing. Enable handle_push_trigger and scope push_commands to /review.

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

* ci(pr-agent): fix pr_actions (event list, not commands) + add synchronize

pr_actions is the list of PR event actions to handle, not slash-commands.
Setting it to ["/review"] removed every real event from the allowlist, so the
bot skipped everything. Restore the default events + synchronize; command
selection stays on the auto_review/describe/improve booleans (review-only).

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

* ci(pr-agent): raise max_model_tokens to 64k for fuller diff coverage

Default ~32k input window truncated large PRs. Sonnet 4.6 has 200k context.

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

* ci(pr-agent): use Claude Opus 4.8 (Sonnet 4.6 fallback)

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

* fix(skatteverket): scope AGI status flips by salary_run_id

Bot review (swedish-compliance) caught that commitSubmitAgi flipped
agi_declarations status by (company_id, period) only. A correction run sharing
the period would have its still-valid declaration co-flipped to rejected/
pending_signature. Scope both updates by salary_run_id (in scope from params) —
more precise than the period-only route handler, which has no run id.

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

* test(pg): fix gen_random_bytes assertion for modern pgcrypto

OpenSSL-backed pgcrypto (CI Postgres image) rejects gen_random_bytes(0) with
'Length not in range' rather than returning empty bytea, so the pre-existing
'returns empty bytea' assertion fails on every pg-real run (repo-wide, not
specific to this PR). Assert the real contract — exactly n bytes for a positive
n — instead of the version-dependent 0-byte edge case.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:43:15 +02:00
Mattsson 8a6ce7093e feat: implement skattekonto drift detection and alerting (#525)
* feat: implement skattekonto drift detection and alerting

- Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum.
- Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming.
- Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows.

feat: create own account transfer detection

- Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN.
- Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs.

feat: establish cash accounts as a first-class entity

- Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures.
- Implement functions for listing, upserting, and managing cash accounts, including primary account designation.

feat: enhance GL line reconciliation functionality

- Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies.
- Update related functions to ensure compatibility with the new cash_accounts structure.

feat: capture counterparty IBAN in transactions

- Add counterparty_iban column to transactions table to facilitate intra-account transfer detection.
- Create index for efficient lookups based on counterparty IBAN.

* feat: Enhance cash account handling and reconciliation processes

- Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'.
- Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes.
- Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy.
- Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731).
- Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities.
- Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates.
- Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one.
- Updated email notifications for drift detection to avoid exposing sensitive financial data.
- Enhanced bank reconciliation logic to handle multi-currency transactions correctly.
- Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage.
- Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards.
2026-05-19 16:10:18 +02:00
Mattsson 7175daee87 Bug/skv konto numbers (#498)
* feat(skattekonto): add overdue transactions handling and split logic

* feat: enhance transaction handling and loading states

- Update BalanceHero component to display last synced date and additional information about Skatteverket updates.
- Refactor BookDirectlyDialog to simplify transaction linking logic and improve UI for transaction selection.
- Revamp InvoiceInboxWorkspace layout for better responsiveness and user experience, including improved skeleton loading states.
- Introduce new loading states for ExtensionWorkspace to match the live layout and improve user feedback during data fetching.
- Implement exchange rate fetching in QuickReviewDialog, ensuring transactions are always processed in SEK with error handling for rate fetching.
- Add structured error handling for unavailable exchange rates in the transaction API.

* feat(skattekonto): add 'Skattekonto – saldo & transaktioner' scope and update authorization checks

* feat: implement reverse charge handling in supplier invoice calculations and UI
2026-05-15 16:25:05 +02:00
MattssonandClaude Opus 4.7 980f29dae8 Bug/momsdeklaration skv (#449)
* fix(salary): show birthdate in masked personnummer, hide the 4-digit suffix

Flip the personnummer display format from XXXXXXXX-NNNN to YYYYMMDD-XXXX so
the sensitive 4-digit suffix is hidden while the (public) birthdate stays
visible. Affects the employees list/detail, salary run, payslip PDF, payslip
email, and the MCP server tools (list_employees, get_salary_run). Each call
site now decrypts the stored personnummer before masking.

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

* fix(transactions): allow deleting unbooked transactions from "Alla transaktioner"

The history list only let users delete via the inbox card; once a category or
mall was picked but the verifikation hadn't been created, the row showed
"Ej bokförd" with no way to remove it. The API already permits delete while
journal_entry_id is null, so the gap was purely a missing UI affordance.

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

* fix(vat): populate ruta 20-24 for reverse charge + dishonest "Validera OK"

Three connected issues caused Skatteverket to reject momsdeklarationer
with FK004 even after our local "Validera"-knapp returned OK.

1. supplier-invoice-entries booked fiktiv moms (2614/2624/2634 + 2645/2647)
   on reverse-charge invoices but never the underlying basbelopp on 44xx/45xx.
   Ruta 30-32 filled up at SKV while ruta 20-24 stayed at 0 — SKV's FK004
   ("silent netting prohibited", ML 13 kap kräver båda sidor).

   Fix: generateReverseChargeBasisLines in vat-entries.ts emits parallel
   45xx/44xx debit + 4598 motkonto credit per rate group. Engine calls it
   from registration, cash, and credit-note paths. Skipped when the user
   booked the expense directly on a basis account to avoid double-counting.
   4598 added to BAS reference (no migration needed; account_number is
   plain text on journal_entry_lines).

2. rutorToMomsuppgift rounded each ruta independently but computed
   summaMoms from the unrounded ruta49. SKV recomputes the sum from
   integer rutor on their side, so fractional öres caused ±1 SEK drift
   and SKV rejected with FK009.

   Fix: derive summaMoms from the already-rounded VAT-amount rutor.

3. "Validera"-knappen only confirmed SKV's internal arithmetic — a
   declaration with ruta 30-32 populated and ruta 20-24 empty validated
   fine until /utkast hit FK004. Users got a false green light.

   Fix: vat-declaration-checks.ts runs locally before the SKV call,
   blocks Validera/Spara when ERROR-level findings exist, and surfaces
   them in a separate "Lokala kontroller"-section. Success message
   reworded so SKV's OK is no longer presented as filing-ready.

Tests: 4535/4536/4531/4425 lines + 4598 motkonto on EU/non-EU/byggtjänster
RC, credit-note reversal, fractional-öres summaMoms, all four pre-flight
codes (RC_BASIS_MISSING, RC_OUTPUT_MISSING, RC_INPUT_VAT_MISMATCH,
SUMMA_MOMS_DRIFT).

Backfill for already-posted entries follows in the next commit.

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

* feat: add skattekonto matching functionality

- Enhance TransactionInboxCard to display a warning for potential 1930↔1630 transfers.
- Implement match suggestions for skattekonto transactions in the backend.
- Create SkattekontoMatchDialog component for linking skattekonto rows to existing journal entries.
- Develop SkattekontoInboxCard component to handle skattekonto transactions in the inbox.
- Introduce skattekonto-match utility functions for candidate matching and linking.
- Update types to include match suggestions and enriched transaction responses.

* refactor: reorganize skattekonto types and implement bank counterpart matching logic

* docs: update CLAUDE.md to streamline integrations and clarify architecture details

* refactor: enhance reverse charge logic to handle non-basis accounts and prevent double-counting

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:04:48 +02:00
Mattsson 75043632de Fix/compliance and invoices (#433)
* feat(invoices): implement inline membership checks for invoice-number RPCs and enhance error handling

* fix(invoices): enhance unpaid amount calculation to support currency-specific rounding

* fix(api-client): handle response.text() error for 403 status in skvRequest
2026-05-10 19:09:57 +02:00
Mattsson d7a7ddae2c Bug/skv connection (#430)
* feat(invoices): implement öresavrundning logic and next invoice number preview

- Added `getDisplayTotal` utility to handle rounding for SEK invoices based on company settings.
- Updated `InvoicesPage` to utilize the new rounding logic when displaying totals.
- Introduced `peek_next_invoice_number` function to allow previewing the next invoice number without consuming the sequence.
- Modified invoice number generation to remove the year prefix and prevent truncation of numbers exceeding three digits.
- Enhanced tests for invoice number generation and rounding functionality to ensure correctness.
- Updated PDF template to reflect new rounding logic for totals and display appropriate values.
- Adjusted company switcher to hide options in sandbox mode.
- Improved error handling and logging in sandbox seeding process.

* fix(skatteverket): remove unused scope labels from SCOPE_LABELS and DEFAULT_SCOPES

* feat(skatteverket): add token revocation handling and disconnect functionality
2026-05-10 14:40:16 +02:00
Mattsson 0ee5219b6c feat(invoices): implement öresavrundning logic and next invoice numbe… (#429)
* feat(invoices): implement öresavrundning logic and next invoice number preview

- Added `getDisplayTotal` utility to handle rounding for SEK invoices based on company settings.
- Updated `InvoicesPage` to utilize the new rounding logic when displaying totals.
- Introduced `peek_next_invoice_number` function to allow previewing the next invoice number without consuming the sequence.
- Modified invoice number generation to remove the year prefix and prevent truncation of numbers exceeding three digits.
- Enhanced tests for invoice number generation and rounding functionality to ensure correctness.
- Updated PDF template to reflect new rounding logic for totals and display appropriate values.
- Adjusted company switcher to hide options in sandbox mode.
- Improved error handling and logging in sandbox seeding process.

* fix(skatteverket): remove unused scope labels from SCOPE_LABELS and DEFAULT_SCOPES
2026-05-10 14:21:26 +02:00
Jakob WennbergandClaude Opus 4.7 432a8b60dc feat(skatteverket): rewrite AGI flow against real Skatteverket RAML (#391)
* feat(skatteverket): rewrite AGI flow against real Skatteverket RAML

The previous AGI client posted JSON to URL paths that don't exist on
Skatteverket's gateway and used invented field names. POST /underlag
actually accepts application/xml, and the lock/kvittenser operations
live on the separate hanteraredovisningsperiod API. Verified against
dev_docs/arbetsgivardeklaration-inlamning(1.7.7) and
arbetsgivardeklaration-hantera-redovisningsperiod(1.2.8) RAMLs.

- Replace fictional types with real schemas (kontrollresultat,
  granskningsunderlag, kvittenser, error envelope)
- Rewrite agi-client into 9 functions matching the documented flow:
  /underlag (XML) -> kontrollresultat -> spara -> skapaGranskningsunderlag
  -> kvittenser, plus las/lasUpp on the hantera API
- Drop agi-mappers entirely; lib/salary/agi/xml-generator.ts already
  produces schema-valid XML, so the extension just feeds
  agi_declarations.xml_content to POST /underlag
- Extend skvRequest with a contentType option so AGI can post XML
- AGIPanel state machine: underlag_submitted -> awaiting_signing ->
  signed, with kontrollresultat polling and normalized findings
- Add the agd OAuth scope (confirmed from SKV's Tjanstebeskrivning
  Arbetsgivardeklaration inlamning v1.7, section 4.1.2.2)
- Add Skatteverket connect step to NewUserChecklist alongside the
  existing SIE/old-system import and bank steps; track
  hasSkatteverketConnected in OnboardingProgress
- Update orchestrator route + tests to point at the new /agi/submit
  endpoint
- Declare new optional base-URL env vars in the manifest

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

* fix(skatteverket): address PR review findings on AGI flow

- Surface INCORRECT_DATA felrapport link in AGIPanel
  skapaGranskningsunderlag returns 409 with a felrapport URL when SKV
  rejects the underlag. The link was persisted as `signeringslank` with
  status `underlag_rejected`, but the render condition only fired for
  `awaiting_signing`, leaving the link unreachable. Add a distinct
  destructive-styled block so the user can open the felrapport in Mina
  Sidor.

- /agi/underlag DELETE clears local submission state
  Add optional `period` query param. When supplied, clear
  `agi_submission_{period}` directly. When not, fall back to scanning
  recent agi_submission_* keys for the matching inlamningId. Without
  this, an aborted underlag left a stale `underlag_submitted` entry in
  extension_data and the UI couldn't progress.

- Re-add salary-run status guard inside loadAGIXml
  The orchestrator at app/api/salary/runs/[id]/agi/submit/route.ts has
  this check, but the extension endpoint is also reachable directly
  from AGIPanel and must enforce it itself. Per BFL 5 kap and SFL
  26 kap, AGI must reflect finalised payroll data; submitting from a
  draft/cancelled run would emit incorrect figures.

- Move agi_declarations.status='exported' from /agi/submit to /agi/spara
  Setting status on underlag-ingest was wrong because a DONE_REJECTED
  kontrollresultat would leave the row falsely marked as exported. The
  transition now happens only after the spara call commits the underlag
  to Eget utrymme. /agi/spara accepts salaryRunId in the body for the
  fast path and falls back to scanning agi_submission_* state otherwise.

- Move salary_runs.agi_submitted_at stamp to kvittenser observation
  The orchestrator was stamping at underlag-ingest, but no later code
  updated the column on signing. Removed the orchestrator stamp; the
  /agi/kvittenser handler now stamps salary_runs.agi_submitted_at to
  kvittens.signeradTid (mirroring SKV's own timestamp) when it pins
  the receipt to the matching agi_declarations row.

- Tighten misleading JSDoc in agi-client.ts
  taBortSparadInlamning is on the inlämning API, not hantera; the old
  layout grouped it under a "hantera API" heading and tripped an
  automated reviewer. Restructured into separate "period management"
  and "cleanup" blocks.

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

* fix(skatteverket): address Swedish compliance review on AGI flow

Follow-up to review on https://github.com/erp-mafia/gnubok/pull/391.

- Migration adds 'pending_signature' to agi_declarations.status
  Reusing 'exported' for the spara→kvittens interval misstated the
  filing outcome — Eget utrymme is a staging area, not a filing — which
  conflicts with BFNAR 2013:2 kap 8 / BFL 5 kap 5§ behandlingshistorik
  faithfulness. /agi/spara now sets 'pending_signature'; /agi/kvittenser
  later promotes to 'submitted' when a uuidKvittens is observed.

- AGIPanel auto-polls /agi/kvittenser at 30s, 2 min and 5 min after the
  signing link is created
  Previously the kvittens (and therefore salary_runs.agi_submitted_at)
  was only stamped if the user manually returned to the panel and
  clicked "Hämta kvittens". Without that follow-up the audit trail
  showed a NULL submitted-at for an AGI that had actually been filed.
  Background polls capture the kvittens for the common case where the
  user signs in Mina Sidor and never returns to gnubok. Cleanup on
  unmount via useRef + useEffect.

- Distinct MISSING_SCOPE error code on 403 invalid_scope
  Existing tokens lack the new 'agd' scope and surface as a generic
  ACCESS_DENIED today. The compliance reviewer pointed out that
  operators may interpret this as a data error and submit a corrected
  AGI with altered figures. New SkatteverketAuthError code maps SKV's
  invalid_scope body to a clear "reconnect via Inställningar →
  Skatteverket" message; routes to 401 (token-level remediation).

- Refine deadline copy in AGIPanel
  The standard AGI deadline is the 12th regardless of company size; the
  17th only applies in January and August for employers with turnover
  ≤ 40 MSEK. Surface that nuance instead of saying just "12:e".

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

* fix(skatteverket): server-side kvittens reconciliation cron

Round 2 of compliance review on https://github.com/erp-mafia/gnubok/pull/391.

- /api/extensions/skatteverket/agi/kvittenser/cron
  Walks every agi_declarations row in 'pending_signature' status, fetches
  kvittenser via the matching token, and on a hit promotes the row to
  'submitted' + stamps salary_runs.agi_submitted_at. Authoritative source
  for the audit trail per BFNAR 2013:2 kap 8 / BFL 5 kap 5§ — the
  AGIPanel client-side timers from the previous round remain as the
  fast-path UX, but no longer carry the audit-trail responsibility on
  their own. Per-row errors are skipped, not abort-the-run. 50s budget.
  Scheduled every 2 hours in vercel.json.

- AGIStatus union now includes 'pending_signature'
  Without this update, downstream code reading the union would have
  rejected the new status as unknown. The migration extending the DB
  CHECK constraint shipped in the previous commit; this brings the type
  layer into sync.

- Stale comment update in AGIPanel.tsx
  Referred to status='exported' from before the rename. Now reads
  'pending_signature', matching the actual handler behavior.

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

* fix(skatteverket): close audit-trail gaps from compliance round 3

- Cron now writes submitted_by from the token-owning auth.users row
  Previously left NULL with a "system actor" comment. The token row was
  created when the operator authenticated with BankID, and the kvittens'
  signeradAv refers to the same person — so writing the user_id from
  skatteverket_tokens captures actor traceability without inventing a
  system identity. Closes the BFL 5 kap 6§ / BFNAR 2013:2 kap 8 gap on
  cron-reconciled rows.

- /agi/spara monotonicity guard
  Adds .in('status', ['generated', 'exported']) to the row update so a
  delayed /agi/spara call after the cron (or interactive /agi/kvittenser)
  has already promoted the row to 'submitted'/'accepted' won't silently
  regress it back to 'pending_signature'. behandlingshistorik must
  advance only.

- DONE_REJECTED / DONE_FAILED → status='rejected'
  /agi/kontrollresultat handler now flips the matching agi_declarations
  row to 'rejected' on a terminal SKV failure, using the same
  cached-submission-state lookup pattern /agi/spara already uses.
  Without this the row sat at 'generated' indefinitely even though SKV
  considered the underlag failed. Same monotonicity guard prevents
  regressing a successfully-filed row.

- Deadline criterion: lönesumma, not omsättning
  AGIPanel pendingText. SFL 26 kap's relaxed-deadline criterion (17:e
  in Jan/Aug) is the employer's total taxable wages, not turnover.
  Internal reference (.claude/skills/swedish-payroll/references/agi-filing.md)
  used the colloquial "turnover"; statutory wording is "lönesumma".

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

* fix(skatteverket): close round-4 audit-trail and UX gaps

- agi_submitted_at: NULL when signeradTid absent
  Both /agi/kvittenser handler and the kvittens cron previously
  fell back to new Date().toISOString() if SKV's kvittens lacked
  signeradTid. Substituting wall-clock now() falsifies the filing
  moment in behandlingshistorik (BFNAR 2013:2 kap 8 / BFL 5 kap 6§).
  Now leaves the column NULL and logs a warning. Status flip to
  'submitted' still happens — the audit gap was timing only.

- Proactive missing-agd-scope banner
  SkatteverketConnectPanel and AGIPanel now warn when the stored
  token lacks the agd scope. Tokens issued before the agd rollout
  would otherwise 403 with invalid_scope at submission time, often
  too close to the AGI deadline. SkatteverketConnectPanel mirrors
  the existing "skattekonto saknas" pattern; AGIPanel surfaces a
  banner in the connected state and links to /settings/skatteverket.

- Granskningsunderlag isError keys on tillstand only
  Previous check mixed HTTP 409 with the INCORRECT_DATA tillstand
  string. A future SKV addition like RECEIVING returned with HTTP
  200 would have slipped through as awaiting_signing. Now keys
  solely on tillstand: only LOCKED_FOR_SIGNING / UNLOCKED are
  treated as signable; everything else (INCORRECT_DATA, RECEIVING,
  CALCULATING, SIGNING) routes to underlag_rejected.

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

* fix(skatteverket): close round-5 audit-trail and recovery gaps

- agi_submitted_at: stamp with reconciliation time when signeradTid absent
  Round 4 left the column NULL on missing signeradTid to avoid falsifying
  the signing moment. Round 5 pointed out that NULL hides that the filing
  *occurred* — also a behandlingshistorik integrity violation. Resolution:
  presence of uuidKvittens proves SKV signed and accepted the AGI, so we
  stamp with signeradTid || now() and warn-log when fallback is used.
  Both /agi/kvittenser handler and the kvittens cron.

- Persist signeradAv + full kvittens in agi_declarations.response_data
  submitted_by is the auth.users UUID we have on hand (the polling /
  reconciling user). The legally load-bearing signer identity is
  kvittens.signeradAv (a personnummer) — which the token user_id does
  NOT necessarily match (e.g. bookkeeper vs deklarationsombud). The
  existing response_data jsonb column now holds the full kvittens record,
  preserving signeradAv for the audit trail (BFL 5 kap 6§ / BFNAR 2013:2
  kap 8) without a schema change. Cron path also marks reconciledBy='cron'.

- /agi/spara monotonicity: allow recovery from 'rejected'
  Previously .in('status', ['generated', 'exported']) excluded rejected
  rows, so a successful re-submission after a prior rejection couldn't
  promote the row to pending_signature — it silently stayed rejected.
  The xml-route reuses the same agi_declarations row when re-generating
  XML, so this is the realistic recovery path. Added 'rejected' to the
  allowed-from list. 'submitted'/'accepted' still blocked (no regression
  from filed states).

- Fix misleading agi-client.ts comment
  Claimed users could "fix the errors in Mina Sidor" after a
  DONE_REJECTED save. Mina Sidor doesn't expose in-place editing; the
  correct recovery is to regenerate XML and resubmit. Updated the
  agiSparaUnderlag JSDoc to describe the actual flow.

- Deadline copy: "vars sammanlagda lönesumma understiger 40 MSEK"
  Reads more cleanly than "≤ 40 MSEK" and matches the phrasing the
  compliance reviewer suggested.

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

* fix(skatteverket): tighten /agi/spara guard and clarify deadline copy (round 6)

- Drop 'exported' from /agi/spara allowed-from states
  Audit confirmed no code path writes status='exported' today; the value
  is preserved in the schema (and union) for the legacy manual-download
  path that no longer has a writer. Allowing the spara handler to flip
  an 'exported' row to 'pending_signature' would conflate two distinct
  filing attempts on a single row, weakening the chain of custody (BFL
  5 kap 6§). Tightened to .in(['generated', 'rejected']) — same recovery
  path for re-submission after rejection, no path for the dormant state.

- Deadline copy: explicit "per år" qualifier
  The 40 MSEK threshold is annual lönesumma, not per-payment. Adding
  "per år" closes the (admittedly thin) misread the compliance reviewer
  flagged.

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-05 17:08:29 +02:00
Jakob WennbergandClaude Opus 4.7 f3fd4c0822 feat(salary, skatteverket): per-day absence + AGI Frånvarouppgift + skattekonto + hardening (#388)
* feat(salary): per-day absence tracking with calendar UX

Replace aggregated-day absence counts with per-day records so payroll
calculations can correctly enforce Swedish legal rules that depend on
actual dates: karensavdrag once per sjuklöneperiod, återinsjuknande
within 5 calendar days, allmänt högriskskydd cap of 10 karensavdrag per
rolling 12 months, day-8 läkarintyg flag, day-15 transition to
Försäkringskassan.

Adds:
- salary_absence_days table (RLS, dedup unique on employee+date+type)
- /api/salary/employees/[id]/absence CRUD route
- deriveAbsenceLineItems helper that walks per-day records into
  sjuklöneperioder and emits correctly-classified line items, with the
  existing absence-calculator formulas reused for VAB / parental
- Per-employee pay-spec detail page with month-grid AbsenceCalendar
- Calculate route now derives line items from the calendar before
  running the salary engine, replacing the prior sumQuantity model
- Salary run GET surfaces the formatted Skatteverket arbetsgivare ID
  so downstream UI can build extension URLs without a second round-trip
- GET /salary/runs/[id]/employees/[employeeId] for the detail page

Tests: 15 new unit tests covering segment merge, återinsjuknande
within 5 days, högriskskydd cap, FK transition flag, läkarintyg flag,
VAB/parental semesterlönegrundande ceilings.

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

* feat(skatteverket): harden API client + add NEXT_PUBLIC_SKATTEVERKET_ENABLED feature flag

Three hardening fixes from the prior audit, plus a runtime extension
toggle for phased rollout.

api-client.ts:
- Map 429 to a new SkatteverketAuthError code RATE_LIMITED with a
  Swedish user message. The 4 req/sec local rate limiter normally
  prevents this, but the per-consumer gateway quota can still hit.
- Extend the error union with TOKEN_CORRUPTED for the token-store fix
  below.

token-store.ts:
- Surface decryption failures instead of silently returning null. A
  rotated key or tampered ciphertext used to look like "not connected";
  callers now get TOKEN_CORRUPTED with a clear "anslut igen med BankID"
  message and a structured log line for ops.

Extension dispatcher (app/api/extensions/ext/[...path]/route.ts):
- Per-extension feature flag table. When NEXT_PUBLIC_SKATTEVERKET_ENABLED
  is not exactly "true", the dispatcher returns 503 with code
  EXTENSION_DISABLED, letting ops disable a single integration mid-
  rollout without redeploying or removing it from extensions.config.json.
  UI panels (SkatteverketPanel, AGIPanel) detect the 503 and render an
  empty state.

Tests: 7 api-client cases (401/403/403-Behörighet/429/5xx/200/auth-error
codes) + 2 token-store cases (no-row → null, corrupted → TOKEN_CORRUPTED).

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

* feat(salary): emit AGI Frånvarouppgift per SKV 4785, add AGIPanel for one-click submission

AGI XML upgrade:
- Emit <gem:Franvarouppgift> top-level blocks for VAB and parental
  leave events sourced from salary_absence_days, per SKV 4785 +
  technical doc. Element order matches the spec example file.
  TILLFALLIG_FORALDRAPENNING for VAB / FORALDRAPENNING for parental,
  with FranvaroTimmarTFP (FK825) or FranvaroTimmarFP (FK827) for
  hours. Stable 1-based specifikationsnummer per (employee, period),
  date-sorted. Skipped entirely for periods before 202501.
- Sick days are NOT emitted (they go to Försäkringskassan).
- FK499 TotalSjuklonekostnad now derived from sick_day2_14.quantity
  × dailyRate × 0.80 instead of Math.abs(amount). The line-item
  amount is the net deduction (lostPay − sjuklon), not the cost,
  so the prior formula understated by a factor of four.

AGI submission UI:
- New AGIPanel mirroring SkatteverketPanel's validate → draft → lock
  → BankID-sign → poll-submitted flow. Detects 503 EXTENSION_DISABLED
  and renders a clear empty state. Replaces the bare "Skicka till
  Skatteverket" button on /salary/runs/[id], keeping the AGI XML
  download as a sibling for archival / manual upload fallback.
- Salary run rows now link to the per-employee detail page added in
  the previous commit.

Tests: 14 new agi-xml cases covering element order, type↔hour-field
mapping, specifikationsnummer ordering, fractional-hour formatting,
range clamping (0.01-24.00), period guard at 202501 boundary,
placement after Blankett blocks, multi-employee date ordering,
required-fields invariant, omission when no events.

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

* feat(skatteverket): skattekonto integration — read-only saldo + transactions, daily sync, per-row bokför

Adds read-only Skattekonto v2.1 access via the existing BankID OAuth
flow (extends the OAuth scope with `skattekonto`). Daily background
sync pulls saldo + transactions, dedupes on (company_id, dedup_key),
and surfaces the data in a /skattekonto dashboard plus a settings
panel for connection management.

Backend:
- skattekonto-client.ts: GET /skattekonton/{omfragad}/saldo and
  /transaktioner. Felkod 1–5 mapped to Swedish messages via dedicated
  SkatteverketSkattekontoError.
- skattekonto-sync.ts: parallel saldo + transaktioner fetch, UPSERT
  on (company_id, dedup_key) so kommande rows graduate to tidigare
  in place. Dedup key uses transaktionsidentitet when available, else
  sha256 of (date|amount|text). Caches saldo snapshot in extension_data.
  Emits skattekonto.synced / balance.changed (sign flip) /
  transaction.upcoming (first appearance) / connection.expired.
- skattekonto-booking.ts: keyword→counter-account rules with AB/EF
  differentiation (2510 vs 2012 for preliminärskatt; 2731/2710/2650
  for arbetsgivaravgifter/avdragen skatt/moms; 8423/8313 for
  kostnads-/intäktsränta). Creates a draft journal entry against
  BAS 1630, leaves it for the user to review and commit. Throws
  NO_COUNTER_ACCOUNT instead of guessing when no rule matches.
- Daily cron at 0 4 * * * (Swedish 06:00). Double-gated by
  CRON_SECRET and NEXT_PUBLIC_SKATTEVERKET_ENABLED. Per-company
  cooldown of 1 hour, time budget 50s, distinct `expired` status
  for token-exhaustion separate from generic errors.

Database:
- skattekonto_transactions: company-scoped with RLS, unique
  (company_id, dedup_key), indexed on (company_id, date DESC) and
  (company_id, status). journal_entry_id FK with ON DELETE SET NULL
  so a row can be re-bokförd after entry deletion.

Frontend:
- /skattekonto/page.tsx: dashboard with saldo card, transactions list
  (booked + upcoming), per-row "Bokför" action.
- /settings/skatteverket: connection panel showing scope/expiry.
- Extension toggle in SettingsSidebar (gated by ENABLED_EXTENSION_IDS).

Tests: 9 booking-rule cases (counter-account guessing, AB/EF
divergence, no-match throw) + 7 mapper cases (dedup key stability,
sign convention, kommande→tidigare graduation).

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

* fix: address PR review findings

Build:
- Fix Next.js build failure: Zod refuses .partial() on a refined schema.
  Replace AbsenceRangeQuerySchema.partial().extend(...) in the absence
  DELETE handler with a fresh z.object that defines its own optional
  fields.

Greptile findings (PR #388):
- skattekonto_transactions UPDATE policy was missing WITH CHECK; without
  it a user could mutate company_id to one they don't belong to. Edit
  the original migration for fresh applies + add a follow-up migration
  that drops/recreates the policy with both clauses (already applied
  to prod via Supabase MCP).
- FK499 TotalSjuklonekostnad now reads sjuklonRate from
  run.calculation_params (snapshot taken at calc time) instead of a
  hardcoded 0.80, so an operator override (e.g. CBA-specific rate) is
  honored. Falls back to 0.80 for older runs without the snapshot.
- Rename NEXT_PUBLIC_SKATTEVERKET_ENABLED → SKATTEVERKET_ENABLED so the
  flag is server-side only. NEXT_PUBLIC_* vars are inlined into the
  client bundle at build time, which would create split-brain (server
  503 vs client still rendering enabled flow) on a flag flip without
  redeploy. UI panels detect 503 by response code, not by reading the
  env directly, so no client-visible change is needed.
- Add pg-real RLS smoke tests for both new tables (salary_absence_days
  and skattekonto_transactions): tenant SELECT isolation, UPDATE WITH
  CHECK enforcement, unique-constraint enforcement, cross-tenant dedup
  key allowed.

Swedish compliance review:
- Document the högriskskydd cap interpretation in
  derive-absence-line-items.ts. We count *sjuklöneperioder* in the
  rolling 12-month window, matching the law's plain reading
  ("från och med den 11:e sjukperioden ... görs inget karensavdrag").
  An alternative reading counts only periods that actually had karens
  deducted; that requires persisting per-period karens-deduction state,
  which gnubok doesn't yet do. The period-count reading can over-
  suppress, never under-suppress, so it's the safer default.

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

* fix(test): inline skattekonto fixtures so core-only CI runs without dev_docs

dev_docs/ is gitignored, so the skattekonto-mappers test failed in CI
when it tried to readFileSync from dev_docs/skattekonto(2.1.0)/examples/.
Inline the saldoResponse + transaktionerResponse fixtures verbatim
from the spec; the test still verifies our mappers + dedup-key logic
against the same shape.

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-04 19:01:21 +02:00
MattssonandClaude Opus 4.7 11621bb79f Feat/skv integration full (#284)
* feat: add script to import Skatteverket monthly tax tables as fallback TypeScript module

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

* feat: gate salary module behind dev-only flag

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

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

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

* fix: bump migration timestamp to avoid collision with logos_bucket

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:03:23 +02:00
Mattssonandgreptile-apps[bot] bb0db7a588 Salary module improvements (#250)
* feat: implement salary module with personnummer encryption, salary entries, tax tables, and AGI tracking

- Added personnummer encryption and decryption functions for secure storage.
- Created salary entries handling for journal entries including gross salary, tax withholding, and employer contributions.
- Implemented tax table lookup functionality for calculating tax amounts based on monthly income.
- Developed SQL migration for salary module including tables for payroll configuration, tax rates, employees, salary runs, salary run employees, salary line items, and AGI declarations.
- Established row-level security policies for all new tables to ensure company-scoped access.

* feat: add salary calculation modules for 2026

- Implemented engångsskatt calculation for one-time payments with tax brackets.
- Added löneväxling functionality for salary sacrifice to pension, including employer savings and warnings.
- Created pain.001 generator for salary batch payments in compliance with Swedish banking standards.
- Developed PDF template for payslips, including detailed breakdowns and employer costs.
- Generated seed data for Swedish tax tables for 2026, including SQL insert statements.
- Implemented traktamente calculations for per diem and mileage allowances, adhering to Skatteverket regulations.
- Added seed script for populating tax tables in the database.

* feat: Update meal reduction percentages in traktamente calculation

fix: Remove obsolete seed script for 2026 tax tables

feat: Extend SalaryRunStatus type to include 'corrected' status

feat: Implement KU10 XML generation endpoint for annual employee income statements

feat: Add endpoint for creating corrections to booked salary runs

feat: Implement endpoint for sending payslip PDFs to employees

feat: Create KU10 XML generator for annual reporting

feat: Add salary transaction matcher for auto-linking bank transactions to salary entries

chore: Add database migration for salary correction support

* feat: replace select elements with custom Select component for employment and salary types

* feat: enhance salary calculations with pension entry and avgifter category support

* feat: enhance employee management with salary type, tax status, and validation improvements

* feat: Implement AGI submission flow to Skatteverket

- Added AGI submission route to handle the submission process.
- Created AGI client for interacting with Skatteverket's API.
- Introduced AGI mappers to convert salary run data into the required AGI JSON payload format.
- Enhanced API client to support custom base URLs for Skatteverket API requests.
- Added types for AGI submission payload and validation results.
- Implemented tests for AGI mappers to ensure correct payload structure and data handling.

* feat: enhance salary module with Skatteverket integration and update dashboard navigation

* Update app/api/salary/runs/[id]/agi/submit/route.ts

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

* Update app/api/salary/runs/[id]/approve/route.ts

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

* feat: integrate write permission check and remove Skatteverket extension

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-15 20:55:29 +02:00
Jakob WennbergandClaude Opus 4.6 d8e0a22495 feat: counterparty templates, Skatteverket extension, complete VAT form (#117)
* feat: separate AR/AP/accounting into distinct nav groups (#92)

Split the flat "Finans" sidebar group into three visually distinct
sections — Försäljning (AR), Inköp (AP), and Redovisning — so users
coming from Fortnox immediately find customer invoicing and supplier
invoices as top-level concepts.

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

* feat: journal entry detail view, correction chain, and account name display

- Add journal entry detail page at /bookkeeping/[id] with full entry view
- Add correction chain API and component showing storno relationships
- Add JournalEntryStatusBadge component for entry status display
- Show debit/credit account names in template picker and review dialogs
- Expand client-side BAS account name mapping with additional accounts
- Show account codes on transaction inbox suggestion buttons

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

* fix: address review feedback — N+1 query, duplicate name, nav dedup

- Batch reverse-lookup into single query per BFS iteration (was N+1)
- Differentiate account 2393 from 2893 in display names
- Extract shared loop for desktop/mobile nav group rendering

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

* feat: counterparty templates, Skatteverket extension, VAT form completeness, and UI cleanup

- Add counterparty-based categorization templates (learned from user approvals
  and auto-ingestion) with fuzzy matching in the mapping engine
- Add Skatteverket extension for direct VAT declaration submission via API
- Complete VAT declaration form with all 30 SKV 4700 boxes (ruta 08, 35-42, 50, 60-62)
- Fix ruta 49 formula to include import VAT (ruta 60+61+62)
- Simplify dashboard UI: remove redundant icons from stat cards, customer cards,
  invoice list, supplier invoices; use Badge variants consistently
- Add SkatteverketPanel component to reports page
- Add categorization_templates and skatteverket_tokens migrations
- Update tests and helpers for new types

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

* fix: address PR review feedback — VAT detection, migration timestamps, dedup

- Fix detectVatTreatment to derive actual rate (12%/6%) from VAT line
  description instead of hardcoding standard_25
- Rename skatteverket_tokens migration to 20260324120001 to avoid
  duplicate timestamp with categorization_templates (fixes Supabase
  deployment failure)
- Make refreshAccessToken accept previousRefreshCount param to enforce
  refresh limit contract at the type level
- Fix rate limiter TOCTOU by claiming slot before await
- Extract formatRedovisare/formatRedovisningsperiod to shared
  lib/skatteverket/format.ts — eliminates duplication between
  mappers.ts and SkatteverketPanel.tsx

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:41:12 +01:00