c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b68c082ef5 |
feat(bank-sync): close the F2 report: gap backfill, consent and paused chip states, agent-triggered sync (#2165)
* fix(bank-sync): cron backfills the gap since the last successful sync The daily incremental sync always asked the bank for the last 7 days. Any pause longer than that (a lapsed subscription paid again, a consent renewed after expiry, an outage) silently lost the days in between: the connection came back, looked healthy, and the missing transactions never arrived. The lookback now widens to cover the gap since last_synced_at plus one day of overlap, capped at the 90-day PSD2 limit, and a gap of a month or more asks for strategy=longest like the manual sync route does. Dedup via external_id makes the overlap harmless. First syncs keep their 90-day path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * feat(bank-sync): chip warns seven days before a bank consent expires The transactions-page chip only reacted once a connection was already dead (expired/error) or had gone stale. A consent that is about to end looked healthy until the morning it stopped syncing. New "expiring" state when a live connection's consent_expires is within seven days, the same threshold as the consent-expiry email in the sync cron. Precedence: attention, expiring, stale, healthy. getChipState moves to lib/transactions/bank-sync-chip-state.ts so the precedence is unit-tested; the component keeps the rendering only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * feat(bank-sync): chip says paused when the subscription lapsed The daily cron filters connections by the bank_sync capability, so a company whose trial or subscription ended keeps status=active rows with a frozen last_synced_at. The chip read that as "stale, check the connection", which sends the user to re-authorise a connection that is perfectly alive. 56 of 191 active connections on prod were in this state on 2026-09-01. New "paused" state, ranked above everything else, when the company lacks bank_sync: hosted points at billing, self-host at the connector key, the same split BankSyncNowButton already makes. getChipState takes an options object so the clock stays out of render (react-hooks/purity). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * feat(api): agent-triggerable bank sync in v1 and MCP Closes the first wish in the F2 report: an integration could read bank data but never refresh it. New POST /api/v1/companies/{id}/bank-connections/ {connectionId}/sync and MCP gnubok_sync_bank, both on a shared runner (extensions/general/enable-banking/lib/trigger-sync.ts). Cost is bounded structurally, not by policy: the window is never caller-controlled (the cron's gap-aware 7 to 90 day lookback), a connection synced within 15 minutes answers BANK_SYNC_COOLDOWN with next_allowed_at (429 + Retry-After on v1; synced=false in-band on MCP so the agent reads on instead of retrying), and a failing connection is throttled per process by attempt time. A dead session is flipped to expired with a remediation that hands the user the connect link: no API call revives a consent. Gated on bank_sync like gnubok_connect_bank; scope transactions:write. Registry, scope map, load-routes, spec snapshot and the generated accounted-api skill updated; five BANK_SYNC_* / BANK_SESSION_EXPIRED codes added to the structured-error registry. The web Synka-nu route is left as is (see DECISIONS.md). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * test(bank-sync): use the options object in the remaining chip-state calls Four multi-line calls still passed the clock positionally after getChipState moved to an options object; tsc flagged them (vitest did not, the extra argument was ignored at runtime). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * fix(api): address skeptic findings on the agent-triggered bank sync Three refutations from the pre-publish skeptic pass: 1. Core imported the extension. The v1 sync route pulled the runner straight from @/extensions, which the core-build gate rejects and which left a live bank endpoint on zero-extension builds. The route now resolves it through the registry's services channel against a contract in lib/bank-sync/trigger-sync-contract.ts (same pattern as the Skatteverket read service) and answers EXTENSION_DISABLED when the extension is absent. 2. The idempotency cache stored the handler-level 429. A same-key retry after Retry-After, which is the documented retry, replayed the stale cooldown as a 400 for the cache's 24-hour TTL. withApiV1 no longer caches 429 responses; regression test added. The endpoint's pitfall no longer claims Idempotency-Key is mandatory (it was never enforced). 3. Two cron tests read the clock twice and failed whenever a millisecond passed between the reads. They now pin the clock with fake timers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdcDV7CngLkWUvfKSsxFhS * fix(bank-sync): durable cooldown lease and review wording Resolves the PR #2165 review findings in one pass. Superagent P1: the attempt throttle was a process-local Map, so two agent calls on different serverless instances (or a retry after a cold start on a failing connection) could each bill an Enable Banking call, contradicting the one-sync-per-15-minutes promise. New bank_connections.sync_lease_until (migration 20260902150000), claimed with one conditional UPDATE before the bank is called; Postgres row locking makes exactly one claimer win, the rest answer BANK_SYNC_COOLDOWN. The lease stays for the full window on success and failure. Tests cover the claim order, a failed attempt seen from a second instance, a lost race, and an expired lease. CodeRabbit: the =1 plural branch now reads "in 1 day" / "om 1 dag" (daysUntilConsentExpiry rounds a partial day up, so "tomorrow" could be today); the cooldown pitfall on the v1 endpoint, the MCP description and the in-band cooldown instruction now say a cooldown can follow a failed attempt and tell the agent to compare last_synced_at before deciding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0125TMQQBjBBZG9YxP7wQWub * fix(bank-sync): lease claim as a literal filter for the schema guard CI's no-phantom-columns guard counts runtime-built query expressions and its ceiling is exact; the templated `.or('sync_lease_until.is.null,...')` claim added one. The column now defaults to epoch (NOT NULL), so "never claimed" is just "expired long ago" and the atomic claim is a single literal `.lte('sync_lease_until', now)` the guard can check. Migration is unshipped (same PR), so it is edited in place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0125TMQQBjBBZG9YxP7wQWub * fix(bank-sync): runner verifies company membership before the lease Superagent (round 3): the MCP path reached the shared runner without a membership check of its own. Both callers do enforce it upstream (withApiV1's company resolution and resolveMcpCompanyContext in the MCP dispatcher), but the runner writes transactions and bills a bank call, so it now checks company_members itself, before the cooldown and the lease claim, and answers NOT_FOUND for a non-member. The viewer check that was buried inside the sync block moves up with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0125TMQQBjBBZG9YxP7wQWub --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b56da5d6c5 |
feat(api): expose bank-connection freshness in MCP and v1 REST (#2124)
* feat(api): expose bank-connection freshness in MCP and v1 REST
gnubok_connect_bank now returns last_synced_at, consent_expires and
error_message per connection, and its instructions tell the agent to
flag stale or expiring connections. New read-only endpoint
GET /api/v1/companies/{companyId}/bank-connections exposes the same
fields to API-key integrations (scope companies:read).
Background: a user's PSD2 feed died silently in July; bookkeeping
looked complete while three weeks stale, and nothing on the API/MCP
surface could reveal it. Sync stays cron-driven; an agent-triggerable
sync was considered and deferred (see DECISIONS.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USJHnxsindrs9X6zqQDLix
* fix(api): address skeptic findings on bank-connection freshness
- Map the bank-connections group into skills/accounted-api (apiskill:check
crashed on the unmapped group; regenerated skill files included).
- Gate the v1 route on the bank_sync capability, mirroring the MCP twin:
a lapsed entitlement now answers with a capability error instead of
status=active with a frozen last_synced_at.
- Reword MCP instructions + v1 pitfalls: null last_synced_at right after
connecting is normal, staleness threshold aligned to the UI's 36 hours,
and re-authorisation is only advised for expired/error/consent-out, not
for stale-but-active connections (lapsed subscription or deselected
accounts are the usual causes there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USJHnxsindrs9X6zqQDLix
* fix(mcp): keep gnubok_connect_bank schema under the tools/list token ceiling
The enriched outputSchema plus the worked examples that landed on main
(#2100) pushed the projected tools/list payload 20 tokens over the
61.6K context-budget ceiling. Drop the per-property descriptions from
the new freshness fields; the instructions string (runtime output, not
catalog payload) already explains them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USJHnxsindrs9X6zqQDLix
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cd40127f0e |
feat(bank): expose bank-reported balance (booked + available) in UI, reconciliation, MCP and v1 API (#2118)
* feat(bank): expose bank-reported balance (booked + available) in UI, reconciliation, MCP and v1 API
The PSD2 sync has fetched the bank's reported balance for years but the
data was stranded (F7): the Bank-page source picker read a cash_accounts
column no sync ever updated (frozen at connect time), reconciliation
hard-coded external_balance to null for bank accounts, and neither MCP
nor the v1 API exposed any balance at all, so the only path to a current
bank balance was logging into the bank.
- getAccountBalance now returns booked + available from the same
quota-limited BALANCES response (previously all but one type discarded)
- every sync (manual + cron) mirrors balance, available_balance and
balance_updated_at into cash_accounts, fixing the stale picker
- new cash_accounts.available_balance column (additive migration)
- reconciliation bank kind: external_balance = bank-reported balance,
plus bank_reported_* fields and fetch timestamp in the bank block;
difference math stays movement-based and untouched
- reconciliation view shows "Saldo enligt banken ... hamtat {date}"
- MCP gnubok_list_cash_accounts returns the three balance fields; the
cash_today prompt now reports the bank's figure instead of teaching
agents to answer with the bookkept 19xx balance
- new GET /api/v1/companies/{companyId}/cash-accounts endpoint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): keep external_balance null for bank sign-offs; never fabricate a zero balance; guard the mirror against stale writers
Post-review fixes from the skeptic pass + CodeRabbit on PR #2118:
- external_balance stays null for the bank reconciliation kind: sign-off
persists it into account_reconciliations and bokslutsbilagor computes
closing - external from that row, so a today-balance stored on a
balansdag sign-off printed a phantom warning-red differens in the
year-end appendix. The bank-reported figure lives only in the
timestamped bank_reported_* pair in the bank block, and only when its
fetch timestamp exists (a balance of unknown age is suppressed).
- AccountOverview no longer falls back to today's date when the balance
timestamp is missing; the line is omitted instead.
- getAccountBalance returns null on an empty BALANCES response instead
of fabricating amount 0 with a fresh timestamp; sync keeps the
previous stored value.
- updateBalancesFromSync only writes over an older-or-missing
balance_updated_at, so an older sync run finishing later cannot move
the mirrored balance backwards.
- The inline initial backfill (picker save) now mirrors fetched
balances into cash_accounts too (accounts_data is deliberately not
re-written there).
- cash_today MCP prompt mentions the gnubok_call_tool bridge for hosts
that only see the default catalog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): express the stale-writer guard as two literal predicates for the schema guard
The .or() with a template literal pushed the no-phantom-columns
unresolvable-expression count over its ceiling. Same semantics, two
updates: one for rows with an older timestamp, one for rows with none.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): rank interimBooked (ITBD) as a booked balance type before the generic fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
dc07ca8872 |
feat(transactions): steer private marking in locked periods to ignore, with v1 and MCP ignore verbs (#1661) (#2031)
Decision (option a): a private marking stays a real booking (eget uttag/insattning), so it remains blocked in a locked or closed period; the legal escape for rows that are not affarshandelser is ignore. Private + locked now returns TX_CATEGORIZE_PRIVATE_PERIOD_LOCKED with remediation naming the ignore paths instead of a bare PERIOD_LOCKED, on all four categorize surfaces and the bulk driver. New v1 POST/DELETE /transactions/{id}/ignore (isTransactionBooked-based 409, idempotent) and a staged MCP gnubok_ignore_transaction (+ accounted_ alias, search visibility to respect the tools/list payload ceiling) with operation_type ignore_transaction; the CHECK pair 20260831070000/070001 rebuilds the constraint from main's newest list plus the new value. Dashboard toast gains an Ignorera i stallet action. Closes #1661
|
||
|
|
9f8fa1b692 |
feat(invoices): draft invoice delete on v1 and MCP with staged approval (#2036)
* feat(invoices): draft invoice delete on v1 and MCP with staged approval
Draft customer-invoice deletion was web-only. This makes the same
semantics available on the v1 API-key surface and as an MCP write tool:
unnumbered drafts are hard deleted (no F-series number was consumed, so
no gap arises), numbered drafts are makulerade (status 'cancelled',
number retained so the F-series stays gap-free per ML 17 kap 24 and
BFNAR 2013:2). Non-drafts are refused; posted invoices can only be
reversed via a credit note.
- extract the web DELETE logic into lib/invoices/delete-draft-invoice.ts
with an explicit userId param (service-role clients null auth.uid());
the cookie route behavior is unchanged
- add DELETE /api/v1/companies/{companyId}/invoices/{id}: 409
INVOICE_DELETE_NOT_DRAFT for non-drafts (status override; the cookie
route keeps its 400), 404 generic NOT_FOUND, dry-run preview of the
outcome, mandatory Idempotency-Key; scope invoices:write
- fix the stale v1 PATCH pitfall that claimed a DELETE handler existed
- new MCP tool gnubok_delete_draft_invoice: staged operation requiring
approval, risk 'high' (both outcomes irreversible, never
auto-committed), catalogVisibility 'search' (tools/list budget at zero
headroom)
- delete_draft_invoice commit executor delegating to the shared service,
plus pending_operations CHECK constraint migration pair
(20260830100000/100001), risk tier, scope map, Granskning vocabulary
and sv/en labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98
* fix(migrations): renumber delete_draft_invoice pair after 20260830101500 on main
Merging origin/main brought 20260830101500_seed_agent_atom_bodies; the
constraint pair must sort after every version already on main so it
never applies out of order at merge time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98
* docs(api-skill): regenerate accounted-api skill for the new invoices.delete endpoint
apiskill:check failed on CI: registering DELETE /invoices/{id} makes the
generated skills/accounted-api docs stale. Output of npm run
apiskill:generate, no hand edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98
* fix(invoices): pin staged delete outcome and align v1 risk metadata
Skeptic findings on PR #2036:
- Outcome pin: gnubok_delete_draft_invoice stages
expected_invoice_number alongside invoice_id; the executor passes it to
deleteDraftInvoice, which refuses with INVOICE_CANCEL_RACE when the
draft's number changed since staging. An unnumbered draft finalized
between staging and approval is now auto-rejected with a message naming
the new number, instead of silently switching from the approved hard
delete to a makulering. Ops staged without the pin keep legacy
semantics; single-phase callers (web, v1) are unaffected.
- v1 invoices.delete registerEndpoint risk raised medium -> high to match
the delete_draft_invoice pending-op tier (both outcomes irreversible);
generated accounted-api docs regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98
* fix(migrations): renumber delete_draft_invoice pair after skattekonto collision
Merging origin/main brought PR #2039's 20260830130000/130001 pair, which
collides with this branch's versions AND re-creates the same
pending_operations CHECK wholesale. Renumber to 20260830150000/150001 and
rebuild the value list as a strict superset (skattekonto list plus
delete_draft_invoice) so applying last revokes nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e8aa0670ca |
feat(salary): agent path to set this month's per-run salary (#2015)
* feat(salary): agent path to set this month's per-run salary
Agents could not do variable owner pay: the only per-run edit tool,
gnubok_update_payslip_line, edits the display-only Grundlon line that
every recalculation rebuilds from salary_run_employees.monthly_salary,
so the fixed employee salary silently won (user-reported).
- lib/salary/run-employees.ts: setRunEmployeeSalary() shared service
(draft gate, roundOre, 0 = nollkorning, display-line refresh); the
cookie route PATCH now delegates to it (behavior unchanged)
- MCP: gnubok_set_run_salary staged tool (search catalog: tools/list
budget at zero headroom), op type set_run_salary (medium risk),
commitSetRunSalary executor, payroll:write scope, payroll_month
loadout + payroll-monthly skill step; update_payslip_line description
now warns that recalc rebuilds base salary lines
- v1 REST: PATCH /salary-runs/{id}/employees/{employeeId} accepting
monthly_salary (draft only, dry-run, idempotency key)
- Migration pair (NOT VALID + VALIDATE) adds set_run_salary to the
pending_operations op-type CHECK; base list verified against prod live
- Tests: service, staged tool, executor, cookie route, v1 route; spec
snapshot updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MP37pE3zk667nP6S766iJG
* fix(salary): harden set_run_salary per skeptic + CI findings
- Clear calculation_breakdown when the per-run salary changes so the
existing book preflights force a recalculation: a run can no longer
be booked with gross/tax derived from the old salary (skeptic R1)
- Enforce SALARY_OVERRIDE_MAX (10 MSEK) in the shared service and the
v1 body schema: closes the unbounded/1e307-overflow path that wrote
Infinity -> NULL -> 500 (skeptic R2)
- Promote gnubok_set_run_salary to the default catalog: a search-only
WRITE is uncallable on Claude.ai (update_customer lesson) while three
surfaces pointed agents at it; payload ceiling bumped 63.8K -> 64.4K
with a ledger entry, read-demotion left as its own change (skeptic R3)
- Granskning label type_set_run_salary in vocabulary.ts + sv/en (R4)
- Display-line refresh is fire-and-forget again (write already
committed; matches pre-refactor route behavior) and DB error details
carry the SQLSTATE code for Swedish error mapping
- v1 risk metadata aligned to 'medium'; NOT_DRAFT message now covers
salary edits, not just roster changes
- npm run apiskill:generate committed (CI apiskill:check failure)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MP37pE3zk667nP6S766iJG
* chore(migrations): rename set_run_salary pair past main's newest versions
origin/main gained 20260828120000 and 20260828154800 after this branch
staged 20260828110000/1; out-of-order versions are skipped at merge, so
the pair moves to 20260828160000/1 (byte-identical SQL, reference in the
VALIDATE header updated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MP37pE3zk667nP6S766iJG
* chore: retrigger Supabase preview after migration-version repair
The preview branch tracked 20260828110000/1 before the rename to
20260828160000/1; the orphan rows are deleted from the preview branch's
schema_migrations (preview only, prod never saw those versions) and this
empty commit re-runs the tasks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MP37pE3zk667nP6S766iJG
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
85e039035d |
feat(reports): custom date ranges on report endpoints in REST v1 and MCP, plus PDF export via API (#1909)
* feat(reports): custom date ranges on report endpoints in REST v1 and MCP, plus PDF export via API
Requested by a v1/MCP user: the web UI can produce resultat- and
balansrapport for a custom period with PDF export, but REST v1 and the
MCP tools only served whole fiscal years and silently ignored
from_date/to_date.
- v1 income-statement: optional from_date/to_date (validated against the
fiscal period via the same parseReportDateRange the dashboard uses)
- v1 balance-sheet: same, plus as_of as the natural alias for to_date
(mutually exclusive with it)
- Unknown query params on these report routes now return
VALIDATION_ERROR with the unknown and allowed names instead of being
silently dropped (scoped to these routes, not a global v1 change)
- MCP gnubok_get_income_statement: from_date/to_date;
gnubok_get_balance_sheet: as_of_date; both validate format, in-period
and ordering, and reject unknown args (tools/list payload bench held
under the ceiling by trimming the same tools' descriptions)
- New v1 PDF endpoints reports/{income-statement,balance-sheet}/pdf,
byte-equivalent to the dashboard export: the K2/K3 grouping and the
balance gate moved to lib/reports/financial-statement-pdf.ts, shared
by both surfaces
- Both JSON endpoints echo the effective range in data.period
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reports): range semantics, empty-date validation, and review findings on PR #1909
Consolidated resolution of the skeptic refutations, CI failures, and
CodeRabbit findings:
- Ranged income statement summed closing balances, so from_date after
period start returned year-to-date figures mislabeled as the range
(July revenue reported as Jan-Jul on JSON, PDF, and MCP). The trial
balance rolls pre-range P&L activity into opening columns, so
generateIncomeStatement now builds from period movements whenever
fromDate is set, matching the resultatrapport convention. Full-period
behavior is unchanged; generator-level regression tests added.
- from_date dropped from the v1 balance-sheet routes (JSON + PDF): a
balansraking is a cumulative position, not a flow over a window
(ÅRL 3 kap); matches the MCP tool's as_of_date-only surface.
- Empty date values (from_date=) now fail validation instead of
silently producing a full-period report with an empty period echo
(null-check instead of truthiness in parseReportDateRange).
- dry_run, read by the withApiV1 wrapper on every request, is tolerated
by the strict param check instead of being rejected as unknown.
- Unbalanced balansrakning on the v1 PDF route returns 400 (caller-data
condition), matching the dashboard export, instead of 500.
- skills/accounted-api regenerated (apiskill:check gate).
- Removed the ISO_DATE_RE import that collided with the pre-existing
local declaration in the MCP server (TS2440 on core build).
- CodeRabbit: 401 tests for both PDF endpoints; event bus cleared in
the new MCP test's beforeEach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
31e0cd6e05 |
feat(onboarding): company setup from the conversation and POST /api/v1/companies (#1814 PR 3) (#1864)
* feat(onboarding): company setup from the conversation and POST /api/v1/companies Third PR of agent-first onboarding (#1814). Once connected, the agent can now set up a company end to end without the web wizard, and partner platforms can provision companies over REST. - create_company_for_user: service-role-only SECURITY DEFINER twin of create_company_with_owner taking the owner explicitly (service clients have no auth.uid()). pg-real test covers creation, role gating, unknown owner and foreign team. - lib/company/create-company.ts: the wizard's creation sequence (org number, TIC snapshot, BAS chart, settings, first fiscal period, tax deadlines, rollback) extracted into createCompanyCore; the Server Action delegates to it, behaviour unchanged. - lib/company/onboarding-input.ts: one Zod schema + planner for the agent/API paths; a VAT-registered company without moms_period is refused (a missing period silently yields zero VAT deadlines). - MCP: gnubok_create_company (two-phase: preview, then confirm=true; companies:write, company-independent), gnubok_connect_bank and gnubok_connect_skatteverket (status + the browser link, gated on bank_sync / skatteverket, search-only in the catalog), the "onboarding" skill, and initialize instructions pointing at it. - Consent page pre-ticks companies:write for an account with no company yet, so the setup does not dead-end on insufficient scope after signup. - POST /api/v1/companies (companies:write, dry-run aware) on the same core; scope map, registry, spec snapshot and the generated API skill updated. - tools/list payload ceiling raised 59.95K -> 60.4K for the one new default-catalog tool (documented in the guard). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(onboarding): explicit f_skatt, org number when VAT-registered, EF first year ends 31 Dec Review findings on #1864 (Swedish compliance review): - f_skatt is required, never defaulted to approved (SE-R-005 risk). - org_number is required when vat_registered: the invoice momsregistreringsnummer derives from it (ML 17 kap 24 §). - An enskild firma's first fiscal year must end on 31 December and its start month is forced to 1 even with first_fiscal_year set, mirroring the wizard's own rule text (BFL 3 kap. 1 §). - POST /api/v1/companies no longer claims Idempotency-Key support (the wrapper only honours it on company-scoped routes). - pg-real: createCompanyCore's chart seed runs under the real service_role, which the unit tests could not prove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * test(pg): starter chart has 41 accounts, assert non-empty The service_role chart-seed proof passed the part that mattered (no 42501 from seed_chart_of_accounts) and failed on a wrong row-count guess: the seeded chart is a curated starter set, not the full BAS list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(migrations): move create_company_for_user to 20260825120000 main gained 20260824170000_bulk_book_transactions_service_actor.sql with the same version while this branch was open; two files on one version abort every Supabase branch apply and the prod auto-apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * chore(api): refresh spec snapshot and generated skill after rebasing onto main Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(mcp): flat create_company result, refuse localhost connect links, test hygiene CodeRabbit on #1864: the confirmed-create result was wrapped in the { data, next } envelope while its outputSchema promised top-level fields; it now returns the fields with next as a sibling. The two connect-link tools refuse to build a link when NEXT_PUBLIC_APP_URL is unset instead of handing a remote user a localhost URL. Tests clear mocks and the event bus in beforeEach. Not changed: the rollback already survives user_preferences.active_company_id (that FK is ON DELETE SET NULL since 20260331010000), and v1 error details stay in the surface's English developer convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d88df74b85 |
feat(reconciliation): residual booking + junction-aware bridge (#1862)
When the worksheet selection (N bank rows vs one verifikat) misses by a few kronor, 'Bokför mellanskillnaden som Bankavgift / Räntekostnad / Ränteintäkt / Öresavrundning och koppla' books the remainder on 6570 / 8410 / 8310 / 3740 against the bank account, links the rows to the main verifikat and anchors the residual verifikat through transaction_voucher_links. Bank accounts only (Skatteverket posts ränta and avgifter as rows of their own), capped at 5 000 kr, direction-checked against the kind; links are made first and undone if the booking is refused. Dashboard + v1 doors (transactions:write, Idempotency-Key, dry run), API skill regenerated. The bridge now treats transaction_voucher_links as links on both sides: migration 20260824190000 re-creates get_unlinked_gl_lines and get_account_gl_lines_for_matching to count junction-linked verifikat as matched (pg-real test), and the TS engine + items do the same for the transactions. This also stops bulk-booked samlingsverifikat from polluting the open buckets. 'Koppla bort' drops the junction rows too. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f40795896f |
feat(reconciliation): sign-off, period picker, Hem row and the three doors for it (#1835)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): sign-off, period picker, Hem row and the three doors for it "Markera som avstämd t.o.m. <datum>" as an append-only attestation: account_reconciliations (who signed which account through which date, with the numbers as they stood; reopen stamps instead of deletes; RLS members write as themselves, viewers read). Policy in one place (lib/reconciliation/signoff.ts): refused with an unexplained difference unless forced with a note, refused past today or past the skattekonto snapshot, refused at or before an active sign-off; reopen is the undo. Every status read now carries the latest active sign-off and the rail shows "avstämt t.o.m.". Three doors: dashboard routes (GET/POST .../signoff, POST .../reopen), v1 (same, scope reconciliation:signoff, Idempotency-Key, dry-run, registry + regenerated API skill), MCP gnubok_reconcile_signoff (search catalog, stages reconciliation_signoff after a policy dry run; executor + risk tier + op-type CHECK migration pair). Events reconciliation.signed_off / reconciliation.reopened, and the four reconciliation events join the public webhook set (additive; API version unchanged, changelog section added). Page: räkenskapsår + range picker in the header (own preset memory, opens on this month) scoping the bridge, the items and the default sign-off date; sign-off dialog with the forced-with-note path; reopen on hover. Hem: worklist category reconciliation_due ("Konton att stämma av"), zero until the company has signed anything off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): classify reconciliation:signoff as a tenant write for the MCP role guard gnubok_reconcile_signoff carries the deliberately separate reconciliation:signoff scope; the central viewer guard keys on the :write/:approve/:manage suffixes, so a viewer could reach the tool (RLS would still refuse the row, but the guard is the intended layer). Add :signoff to the classifier; the strictness test that caught it now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(providers): serve local rate-limiter waiters in arrival order Two callers that both found the in-memory bucket empty each set their own timeout; the timeouts expired at the same instant from different timer lists and which woke first was platform-dependent. hydrateInvoices relies on "started first, requested first" to serve open invoices before paid ones, so lib/providers/__tests__/hydrate-invoices.test.ts flipped on CI (twice on #1817) while holding locally. A promise queue makes the local waiters FIFO without changing the rate; the Upstash path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 14a7599bf2c6fa7f97de6ffab3dc4cf4d0e1827d) --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3a62c5419e |
feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools (#1833)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
60920ec794 |
feat(skatteverket): expose filed VAT declarations and decisions via the v1 API (#1773)
* feat(skatteverket): expose filed VAT declarations and decisions via the v1 API Add GET /api/v1/companies/:companyId/skatteverket/vat-declarations, returning a period's momsdeklaration as Skatteverket has it on file: the submitted declaration (SKV /inlamnat) and Skatteverket's beslut (SKV /beslutat), either individually via ?state= or both. - Auth: compliance:read scope; member-visibility read model per #1673 (resolveReadAuth: caller's token, any member's active token, or system credentials with a verified ombud grant). - Architecture: core reaches the Skatteverket extension through the registry-resolved services channel (contract in lib/skatteverket/declaration-status.ts), so core never imports from @/extensions/. - New structured error SKATTEVERKET_API_ERROR (502) for upstream SKV failures; 404 from SKV maps to submitted/decided = null with HTTP 200. - 19 new tests (route: auth, validation, extension-disabled, happy path; extension service: auth resolution, state filtering, SKV error mapping). Fixes #1663 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): address review findings on the vat-declarations read API Consolidated fixes for PR #1773 review round: - apiskill sync (core-build Checks): map the new skatteverket endpoint group into the periods.md reference and regenerate skills/accounted-api (124 -> 125 operations). - CodeRabbit: parse the SKV 2xx body before writing the audit row, so an unreadable body is audited as skv_error and returns the structured SKATTEVERKET_API_ERROR 502 instead of escaping as an internal 500; regression test added. - Compliance swarm (ISO A.8.12 / SOC2 CC6.1): stop forwarding the raw upstream SKV response body to API consumers; the caller now gets the status code and a generic Swedish message, the body is logged server-side only. - Compliance swarm (GDPR Art.30): add the moms.declaration_status_read processing activity to .compliance/ropa.yaml (live read, no payload persisted, audit-log metadata only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86c6af6976 |
feat(api): v1 REST company-settings write endpoint (PATCH) (#1405)
* feat(api): v1 REST company-settings write endpoint (PATCH)
Adds PATCH /api/v1/companies/{companyId}/settings, closing the gap where
the v1 REST surface had no company-settings write (only the staged MCP
tool gnubok_update_company_settings could change them).
- Field set is identical to the MCP tool: payment details (bank account,
bankgiro, plusgiro, swish, iban, bic), invoice contact details (email,
phone, website), contact_person (aliased onto default_our_reference,
exactly as the MCP tool maps it), and invoice_email_texts.
- Validation reuses the shared UpdateCompanySettingsParamsSchema (Luhn
bankgiro/plusgiro, invoice email placeholder whitelist), so REST and
MCP can never drift apart on the Swedish-domain rules.
- Writes directly with an explicit .eq('company_id', ...) filter,
following the v1 customers PATCH precedent: no staged operation, since
REST callers are already gated by the companies:write scope.
- Dry-runnable, mandatory Idempotency-Key, registered in the endpoint
catalogue, scope map, and load-routes; spec snapshot updated.
- The companies:write scope description now mentions the REST endpoint.
No GET endpoint yet (possible follow-up); reads stay on the MCP tool.
Fixes #1348
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(v1): harden company-settings PATCH contract, align risk tier
Adversarial-review follow-up for the settings PATCH endpoint (#1348):
- Declare risk: 'medium' in registerEndpoint, matching the
update_company_settings tier in lib/pending-operations/risk-tiers.ts
(payment settings control where customers send money on future
invoices). The spec snapshot does not pin the risk field, so no
snapshot regeneration is needed.
- Pin the partial-PATCH contract: every column the caller did not
supply must arrive as undefined in the update payload, never null.
A future ?? null on the literal 13-column payload would silently
clear every unsupplied column; the new test fails on exactly that
regression (verified by mutation).
- Cover the body-parsing branches: invalid JSON and non-object JSON
bodies (bare array, string, number, null) each return 400 with the
handler's respective message and never reach the update call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b6332e9ff4 |
Fix/skv connection flow (#1015)
* feat(salary): one-click AGI submission with filing state machine and success feedback The AGI panel required users to know that "Ladda ner AGI-fil" was the generate step, then click submit, signing link, and kvittens manually. A nollkorning filing stalled on "AGI-XML saknas" pointing at a UI path that does not exist. - New primary button "Lamna in till Skatteverket" chains the existing endpoints client-side: generate XML if missing, POST underlag, poll kontrollresultat, create signing link, open Mina Sidor in a tab opened synchronously at click (popup-blocker safe). Inline stepper shows each step; the four old buttons become collapsed advanced/recovery actions, auto-expanded in stale-draft and rejected states. XML download stays visible and free for manual filing. - deriveAgiFilingState() + useAgiSubmission() lift the per-period submission record to the run page: the progress rail and salary hero now render the real state machine (generated, underlag inskickat, vantar pa BankID-signatur, inlamnad med kvittensnummer) instead of telling users to "lamna in" an already-submitted declaration. - Success card with kvittensnummer and signature metadata once signed, plus a toast when a poll flips the state while the page is open. - AGI kvittens cron every 15 min instead of every 2 h so filings signed on another device get stamped and emailed promptly. - Advanced submit also auto-generates, and the stale "Lon -> AGI -> Generera" error text now points at the real buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): instant OAuth callback feedback and dead-attempt cleanup The bank redirect landed on a blank page for the several seconds the callback spent exchanging the PSD2 session and mirroring accounts, and every failed connect attempt left a status='error' row that rendered forever as an "Atgard kravs" card next to a successful retry, showing duplicate connections to the same bank. - Stream a branded "Slutfor bankanslutningen" progress page from the callback: the shell flushes before the session exchange starts and a script/meta redirect follows when the work completes, with a 30s slow-work escape hatch. Fast outcomes (denial, bad params, unknown state) keep their plain redirects. - Delete never-activated connection rows (no session_id, no accounts_data) on denial or exchange failure, and sweep leftovers for the same bank on the next connect. Established connections keep their "Atgard krävs" card via the accounts_data guard; FKs are ON DELETE SET NULL so deletion has no dependents. - Show "Banken ar ansluten: hamtar dina konton" while the settings panel loads after the callback instead of an anonymous spinner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): reject re-send of issued invoices and gate bookkeeping on the sent flip A direct POST to /api/invoices/[id]/send against an already-issued invoice re-emailed the customer and posted a second revenue verifikat (createInvoiceJournalEntry has no dedup), overwriting journal_entry_id and orphaning the first entry. Only the UI hid the button; the v1 route and the MCP commit executor already rejected non-drafts. - Non-draft invoices now return 409 INVOICE_ALREADY_SENT. - The draft to sent status flip is an optimistic lock (status guard plus row-count check); journal entry, accrual schedules, PDF archival and the invoice.sent event only run for the request that won the flip. - On a flip failure the journal entry is deferred: the row stays draft and a retry re-runs the pipeline, ending with exactly one verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): payment links, failure visibility and sandbox guard for recurring auto-send - sendInvoiceFromSchedule now auto-creates an online payment link via applyPaymentLinkToInvoice before rendering and passes the payment link QR to the PDF: parity with the dashboard and v1 send routes, which recurring invoices silently lacked. - The recurring cron persists last_run_warning both when a claimed run throws (hourly retries stay visible on the schedule) and when a stale schedule is rolled forward, so a deterministic failure can no longer skip a month silently. - Auto-send is blocked for sandbox companies at the email chokepoint (freeze-and-retain: the invoice is still generated as a draft), covering both the cron and the run-now route with one guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): close the Fortnox payroll API gaps (phases 1-4) Payroll now runs end-to-end through the open API, including onboarding a client from another payroll system, with every write staged for approval. - v1: per-employee payslips (list/detail/PDF), payslip line writes, run roster attach/remove, absence ranges (per-day storage), jamkning fields, cutover opening balances (single + atomic bulk PUT), vacation balance + vacation-year-close. PUT added to the wrapper's idempotency/ test-key set (test keys could otherwise write through PUT). - MCP: 10 new tools (get_employee/get_payslip/list_absence/ get_vacation_balance reads + staged update_payslip_line, register_absence, create_employee, update_employee, set_employee_opening_balances, close_vacation_year), executors, risk tiers, op-type CHECK expansions. create_employee encrypts personnummer at staging: pending_operations never holds plaintext. - Scope-map audit retrofit: 11 formerly unmapped tools now scoped; BREAKING for keys that relied on the 4 default-allow writes. - Cutover: employee_opening_balances (derived lock trigger, self-unlocks on run correction), engine YTD/karens/liability integration, Ingaende saldon section in the employee editor. - Arbetsschema-lite: employees.hours_per_week/workdays_per_week drive the hourly/daily divisors; legacy 173/21 preserved exactly at defaults so existing pay math is byte-identical. - Vacation ledger + semesterberedning/arsavslut: recomputed per-year day balances (synced on book/correct, non-fatal), year-close with the min-20 floor, 5-year sparade-dagar expiry to forced payout, and a 2920/2940 drift adjustment via the bookkeeping engine; Semester dashboard card with preview-then-confirm dialog. - Fix: Zod 4 defaults leak through .partial(), which made every sparse employee PATCH fail validation and reset defaulted columns. Migrations 20260713100000/101000/110000/121000/122000 (applied to staging with version rows; prod via merge). vacation_ledger renamed from 20260713120000 to avoid colliding with vat_declaration_totals_rpc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: cut dashboard page-load latency (region, round trips, caching, VAT RPC) The dominant cost was infrastructure: Vercel functions ran in iad1 (Washington D.C.) while Supabase (DB + auth) lives in eu-north-1 (Stockholm), so every request paid 4-5 transatlantic round trips of auth + company resolution before doing any real work (measured 530-1900ms for single-query GETs in prod logs). Pin functions to arn1 and cut the redundant work on top: - vercel.json: functions to arn1, same city as the database - getActiveCompanyId: preference + first-membership queries run in parallel; the fallback result doubles as validation in the common single-company case (one round trip instead of two sequential) - withRouteContext: Server-Timing header and authMs/companyMs/handlerMs in the op-completed log, so latency is attributable per phase - dashboard layout: nav badge counts off the critical path; DashboardNav loads them client-side via the new use-worklist-badges SWR hook with debounced realtime revalidation - swr (new dependency, approved): global provider; useCompanySettings shares one cache entry across consumers and renders from cache on back-navigation instead of re-showing skeletons - /pending: realtime refetch debounced; bulk operations previously fired 4 requests per row-change event - VAT declaration: new get_vat_declaration_totals RPC returns per-account totals, settlement-shape detection (#984) and source_type counts in ONE round trip instead of paging every entry+line through PostgREST. Account lists stay TS-side parameters so ACCOUNT_RUTA remains the single source of truth. Shape-exclusion coverage moved to tests/pg/vat-declaration-totals-rpc.pg.test.ts; DDL already applied to staging. - bundle: CommandPalette lazy-mounts on first Ctrl/Cmd+K, AgentChat dynamic-imports the markdown parser, @vercel/speed-insights (new dependency, approved) added for real-user timings The /salary fetch-waterfall fix from the same effort already landed inside 2084a756. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): settle öre-rounded payments from the mark-paid flow An invoice with öresavrundning shows a rounded "Att betala" on the PDF; the customer pays that amount (up to 50 öre off the stored öre total) and the invoice-page mark-paid flow rejected it with MATCH_AMOUNT_EXCEEDS_REMAINING: a dead end, while the bank-transaction match flow already absorbed the residual to 3740. - PaymentBookingDialog now proposes the rounded bank leg plus the 3740 residual line (credit when rounded up, debit when rounded down), resolved via getDisplayTotal from the per-invoice override and company_settings.ore_rounding. - settleInvoicePayment and the v1 mark-paid route absorb the sub-krona residual, gated by planInvoicePaymentForLines: absorption applies ONLY when the caller lines carry the exact residual on 3740; otherwise the strict plan applies (sub-krona partials stay partial, no-3740 overshoots keep the 400), so the GL can never diverge from the AR sub-ledger. - planInvoicePayment absorb-band boundary tightened to >= 1 kr: an exactly-1-kr overshoot used to slip past both the guard and the absorb branch and silently over-record paid_amount (pre-existing on the bank-match path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): resolve all 7 PR compliance findings - ASVS V3.3: per-request CSP nonce on the enable-banking finalize page (mirrors the mcp-oauth consent page); inline scripts are nonce-bound - ASVS V16: decouple callback finalize work from the response stream (eager promise + next/server after()) so a client disconnect cannot drop session persistence or the consent_granted audit emit - ISO 27001 A.8.15: failed audit-event emits log through the structured logger with a stable message for log-based alerting - ASVS V2.3: recurring-invoice cron and run-now routes resolve isSandboxCompany themselves and pass an explicit suppressAutoSend flag (defence in depth around the email chokepoint, freeze-and-retain kept) - ISO 27001 A.8.11: stagePendingOperation rejects plaintext personnummer-bearing keys in params/preview_data (key-based guard; EF org numbers make value-matching unsafe) - ASVS V4.5: employee PATCH body is truly sparse; cleared number fields are omitted instead of resetting DB values to hardcoded fallbacks - ASVS V8.2.1: route-level tests pin the v1 cross-company deny (404 by convention, not 403) on the payslip PDF endpoint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: implement vacation-year basis change validation and error handling - Added tests to block vacation-year basis changes when open balances exist. - Implemented error handling for open-balances guard query failures in the settings route. - Enhanced absence route to reject reversed date ranges with a validation error. - Updated absence handling to use atomic upserts instead of delete+insert for better performance and reliability. - Refactored salary calculation logic to correctly handle age-based avgifter rates according to Skatteverket's rules. - Improved error messaging for vacation year closure adjustments. - Adjusted employee opening balances handling to preserve audit information during upserts. * feat(settings): add validation to block vacation-year basis change with open balances feat(absence): reject reversed date ranges in absence queries fix(absence): update absence handling to use atomic upserts instead of delete+insert fix(employee): improve validation for jamkning dates in employee updates fix(opening-balances): ensure created_by field is preserved during upserts test(absence): enhance tests for absence range and date validations test(calculation): add tests for age-based avgifter rates and edge cases test(semesterberedning): validate vacation year closure adjustments and error handling test(employee-opening-balances): update tests to reflect changes in salary_run_employees schema * fix(migrations): implement NOT VALID constraints for pending_operations and add validation migration --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cdac1808c9 |
feat(api): ROT/RUT, articles, and project lifecycle on the v1 API (#904)
* feat(api): ROT/RUT + articles + dimensions on the v1 invoice surface (#895) - v1 invoice POST now routes through buildInvoiceWriteData, the same builder as the dashboard: ROT/RUT deduction lines (server-side compute, personnummer encryption), article_id + revenue_account linkage, accruals, and line_type no longer get silently dropped on the wire. - v1 invoice PATCH accepts default_dimensions so integrations can tag a draft with a project/cost centre after creation. - New PATCH/DELETE /dimensions/:id/values/:valueId: rename, archive, set end_date on project codes; delete unreferenced values (409 with an archive hint when the BFL retention trigger blocks). - New GET /articles: read-only artikelregister list (incl. housework_type) so callers can resolve article_id before composing invoice lines. - Invoice GET/POST projections now expose deduction fields and full item columns; dry-run previews never echo the encrypted personnummer. Closes #895 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): address review on #904 - Extract shared v1 invoice projections to lib/api/v1/invoice-columns.ts so create/detail/patch responses can't drift; PATCH now returns deduction_total + deduction_personnummer_last4 like GET/POST. - Narrow the v1 create customer fetch back to the three fields the builder reads instead of select('*'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8bb49c07a2 |
feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry (#858)
* feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry
Phase 2 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false (default) see zero change.
API:
- Dashboard CRUD: GET /api/dimensions (lazy-seeds system dims 1/6 via the
ensure_company_dimensions RPC), PATCH /api/dimensions/[id] (is_system
rename blocked), POST/PATCH/DELETE values (code immutable after creation;
strict Fortnox code format ^[A-Za-z0-9ÅÄÖåäö_+\-]{1,20}$ at the API layer;
retention-trigger deletes surface the Swedish "arkivera istället" message
as 409 DIMENSION_VALUE_REFERENCED).
- POST /api/dimensions/import-existing — scans journal_entry_lines.dimensions
for unregistered codes and mints inactive placeholder registry rows.
- v1 public API: GET dimensions + POST values (Idempotency-Key, dry-run),
registered in the OpenAPI spec (102→104 endpoints).
- dimensions_enabled boolean on company_settings (new migration,
UI-visibility only, never correctness-bearing) exposed through the
existing settings read/update path.
SIE export (lib/reports/sie-export.ts):
- Reads the new dimensions/dimension_values registry; legacy
cost_centers/projects tables now have zero readers (drop migration next).
- Fixes the latent Visma-rejection bug: #OBJEKT now declared for INACTIVE
values referenced by lines.
- Generic-N: #DIM/#UNDERDIM loop sorted by sie_dim_no; #TRANS object lists
serialize from the line JSONB map (sorted, '01'→'1' collapse); orphan
codes/dims synthesize declarations from the SIE reserved-number seed —
every referenced (dim, code) pair is guaranteed declared.
UI:
- /dimensions register (Register-recipe): tabs per dimension, search,
sortable table, value dialog (code immutable on edit, projekt dates on
dim 6), archive-not-delete affordances.
- DimensionCombobox shipped (mounts in the tagging PR).
- Settings toggle "Aktivera kostnadsställen & projekt" — toggle-on runs the
import-existing scan and links to the register.
- Nav row in redovisning, rendered only when dimensions_enabled (same
mechanism as pays_salaries).
- dimensions.* i18n namespace (51 keys, sv/en parity).
- Sandbox seed: demo dims + values, revenue line tagged {"1":"BUTIK","6":"P001"}.
Verified: 6328/6328 unit tests, guard + coverage gate green, tsc parity with
main (210=210), production build passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): PR2 review round — atomic archived-create, UNDERDIM ordering, import robustness, date semantics
- POST values accepts is_active so "create as archived" is atomic; the UI's
fragile create-then-PATCH fallback is deleted (PR Agent finding 1).
- DimensionCombobox blur revert reads the committed value/values through refs
so a selection landing inside the 150ms window always wins (finding 2).
- import-existing sanitizes candidate codes like the PR1 backfill and upserts
with ignoreDuplicates — one bad/duplicate code can no longer abort the
batch; created counted from returned rows (finding 3).
- SIE export emits all root #DIM before any #UNDERDIM so a parent always
precedes a lower-numbered child (SIE4 declaration order — Swedish review);
synthesized placeholder declarations now log one structured warning
(BFNAR 2013:2 behandlingshistorik) + defence-in-depth comment.
- Value dates rejected (400 DIMENSION_VALUE_DATES_NOT_ALLOWED) when the
parent dimension is flow-period (resets_annually=true); explicit null
still clears (Swedish review).
- Sandbox seed logs seeded dimension codes; GET /api/dimensions documents
the deliberate absence of dimensions_enabled gating (UI-visibility flag,
not a security boundary — compliance-swarm V8.2.1 rejected by design).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5b4cefe8ab |
feat(api): v1 endpoints to stamp invoice inbox items as consumed (#767)
* feat(api): v1 endpoints to stamp invoice inbox items as consumed
Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link
(best-effort stamp on the originating invoice_inbox_items row) and a new dedicated
POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping
independently of the document link — both use documents:write scope and require
Idempotency-Key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
* fix(api): wrap stamp response in dataEnvelope and register route in load-routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
---------
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
b94ed3bec2 |
feat(api): cookbooks + webhook audit_log + secret rotation (PR-500 carry-overs) (#501)
* docs(api): ship 4 cookbook recipes (close docs polish backlog) Promotes the four placeholder cookbook entries to full narrative recipes matching the Stripe-grade quality bar set by quickstart + webhooks. Closes the docs follow-up bucket from the PR-500 description's deferred list. Recipes: - ingest-bank-transactions: bank-file upload (CSV / CAMT.053 auto-detect) → async poll → list uncategorised → suggest-categories → categorize (single + batch) → match-invoice / match-supplier-invoice. Multicurrency notes covering Riksbanken FX lookup and the kontantmetoden partial- payment guard. - file-vat-declaration: GET /reports/vat-declaration → rutor 05–62 walkthrough → GL reconciliation block → 2026-04-01 livsmedel 12% → 6% transition explicitly covered (delivery_date supply-date rule) → voucher- gap pre-flight → period lock workflow → manual Skatteverket Mina Sidor submission with confirmation-reference capture → EU / reverse-charge / import handling. - run-payroll-and-agi: draft → calculate → approve → mark-paid → book → generate-agi state machine. Per-step idempotency, strict-mode book failure semantics, förmånsbeskattning + bilförmån + bruttolöneavdrag vs nettolöneavdrag ordering. AGI XML download for manual Mina Sidor upload (direct API submission requires BankID via the Skatteverket extension, not the public REST surface). - year-end-closing: IB/UB continuity check per BFL 5 kap → voucher-gap pre-flight → missing-documents pre-flight → lock (reversible) → year- end async operation (resultatdisposition + periodiseringsfond + överavskrivningar + bolagsskatt + opening-balance batch) → close (irreversible per BFL 5 kap 8 §, typed-phrase confirmation) → årsredovisning + INK2/NE generation. Brutet räkenskapsår variant documented. Each cookbook follows the same shape as the existing quickstart and webhooks recipes — concrete curl commands, response samples, common pitfalls, next-steps cross-links. Lengths are deliberately uneven: the year-end recipe is longest because the consequences of getting it wrong are most severe (BFL violations, irreversible close). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api): V16 audit_log entries for webhook lifecycle + secret rotation endpoint Two intertwined changes that together close the "real audit attribution gap in actively-used routes" item from the PR description. 1. POST /api/v1/companies/{companyId}/webhooks/{id}/rotate-secret New endpoint that issues a fresh HMAC signing secret and invalidates the previous one immediately. Returns the new secret EXACTLY ONCE in the response, mirroring the create-time contract. Required scope: webhooks:manage. Idempotency-Key mandatory. Rotation is instant — no grace period. Documented workflow: stage the new secret on the receiver side (separate config slot, not yet active) → POST /rotate-secret → activate the new secret on the receiver → POST /webhooks/{id}/test to verify. A "previous_secret" column with TTL-based grace window (Stripe-style) is the natural follow-up; the instant-rotation shape ships first because it closes the "secret leaked, need to rotate now" use case with minimum new surface. The route is wired into load-routes.ts and lib/auth/scopes.ts. Spec snapshot updated. 2. V16 audit_log entries on every webhook lifecycle mutation The audit_log column shape (user_id, company_id, action, table_name, record_id, actor_id, old_state, new_state, description) is exactly what V16 / Art.32(1)(b) / A.8.24 audit-trail requirements call for. Wired entries on: - POST /webhooks (create) — action INSERT, new_state captures the row WITHOUT the secret (signing material must not land in the audit trail; only secret-event metadata). - PATCH /webhooks/:id (update) — action UPDATE, before/after pair so reviewers can reconstruct exactly what changed. - DELETE /webhooks/:id (delete) — action DELETE, old_state snapshot so the row's prior state survives the delete. - POST /webhooks/:id/rotate-secret — action SECURITY_EVENT, new_state carries the event marker only (no secret value). - dispatcher.disableWebhook (auto-disable on HTTP 410 / redirect / url_unsafe) — action SECURITY_EVENT, before/after capturing the disable cause for SIEM correlation. actor_id is set to ctx.apiKeyId on caller-driven entries so the audit row points back to the specific API key that triggered the change (PR-500 round-1 CC6.3 finding: actor attribution via created_by_api_key_id alone leaves a gap if a key is deleted — keeping the actor_id in audit_log closes that). 4 new integration tests cover the rotate-secret happy path, 404, 401 unauthorized, and Idempotency-Key required. The existing webhook integration tests continue to pass because the audit_log inserts fall through to the default mock response (no-op) without disturbing the per-table queues. 39 integration tests pass on the webhook surface (+4 vs round-2). Total: 3588 unit tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-501 review round 1 — correctness + Swedish compliance Round 1 of review fixes. Two real correctness bugs Greptile caught, two audit-trail gaps, and four Swedish-compliance errors in the cookbook prose. Compliance Swarm has 17 findings (0 blocking); the 4 architectural items (secret-at-rest encryption, dedicated rotate scope, rate-limit on rotation, URL redaction) remain deferred with rationale. Greptile (3 / 3 — all addressed): 1. rotate-secret silent 0-row UPDATE — fixed by adding `.select('id').maybeSingle()` to the UPDATE and returning NOT_FOUND when no row was touched. Closes the TOCTOU window between the existence check and the secret update; a concurrent DELETE no longer hands the caller a freshly-generated secret that no webhook in the database matches. 2. DELETE handler audit_log silently skipped when prior snapshot is null — fixed by writing the audit row UNCONDITIONALLY with `old_state: prior ?? null` and a degraded description when the snapshot is unavailable. A successful DELETE now always produces exactly one audit row (CC6.3 attribution contract). 3. Typo "bookslut" → "bokslut" in year-end-closing.ts. Compliance Swarm code-quality items addressed: 4. PATCH new_state now derived from the DB-confirmed returned `data` with an explicit field allowlist, not from the request-body-derived `update` object (A.8.11 / V16.1.1). Closes the gap where a future trigger that rejects a field would leave the audit trail out of sync with the actual stored state. 5. All four route-side audit_log inserts (create, update, delete, rotate-secret) now capture the insert error and emit a structured warning via ctx.log; mirrors the dispatcher pattern (CC7.2). 6. Dispatcher null-user_id path now emits a structured warning instead of silently skipping the audit_log entry — SIEM can alert on the gap (CC7.2 / V16.1.1 / A.8.15). Swedish compliance (cookbook content fixes — all real errors): 7. VAT cookbook ruta 06 label corrected: "Övrig försäljning (ej skattepliktig)" → "Momspliktig försäljning som inte ingår i ruta 05" (Skatteverket's verbatim label). The old label conflated exempt vs zero-rated supplies and would cause integrators to omit export / EU zero-rated sales from box 06. 8. Livsmedel rate-change framing rewritten: leads with the supply-date rule (ML 1 kap 3 §) as the decisive date, not invoice_date. The old opening sentence ("invoices created with invoice_date >= 2026-04-01 book to 2631") was wrong on its face — a copy-paste reader would mis-book pre-cutover deliveries invoiced in April at the new 6% rate. 9. Reverse-charge EU 2645 note adds the blandad-verksamhet caveat: "Net zero impact on cash flow" only holds when full avdragsrätt applies; partial avdragsrätt requires proportional restriction per HFD 2023 ref. 45. 10. Payroll cookbook age bounds corrected: "under-25 / over-66" → "18-22 years old (born 2003-2007) / 67+ from 2026", per Prop. 2025/26:66. The old bounds would cause integrators to apply the reduced rate (20.81%) to 23-24-year-olds who must pay 31.42%, producing non-compliant AGI files. 11. Payroll cookbook BAS 2615 corrected to 2731 (Avräkning sociala avgifter). 2615 is "Utgående moms vid import" in BAS 2026 — using it for the payroll liability would misclassify a payroll payable as an import-VAT payable and break moms reconciliation. 12. Year-end cookbook periodiseringsfond cap base corrected: IL 30 kap 5 § cap is on taxable profit BEFORE the periodiseringsfond deduction itself (and after schablonintäkt is added back). Note on materiellt samband (BFNAR 2016:10 kap 13) added — the reservation is BOOKED on 2110-2139, not declaration-only. Deferred to follow-ups (architectural / out of scope for round 1): - Secret-at-rest encryption (CC6.1 / Art.5(1)(f)): PR-1 architectural carryover, applies to existing webhooks.secret column too. - Dedicated `webhooks:rotate` scope (CC6.3 informational): introduces friction without closing a real gap when the only caller-driven action gated by `webhooks:manage` is the rotation itself. - Per-route rate-limit on :rotate-secret (Art.32 abuse case): part of the wider per-route rate-limit pass already on the deferred list. - webhook_url redaction in audit_log (Art.5(1)(c)): URLs are admin- supplied configuration values with no expected sensitive params; truncation would degrade audit value for legitimate review. 23 webhook integration tests pass locally (no regressions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-501 review round 2 — atomic mutations + audit completeness + cookbook compliance Round 2 of review fixes. Compliance Swarm flagged refinements to the round-1 fixes; Swedish-compliance had a fresh batch of cookbook items (including a self-contradiction in payroll pitfalls I missed last round). All addressed. Code changes — atomicity + audit completeness: 1. rotate-secret collapsed to a single UPDATE … RETURNING (V8.2.1). The preflight existence-check SELECT was redundant after round 1 added .select().maybeSingle() on the UPDATE — the same null-row signal indicates non-existence, but in one round trip with no TOCTOU window. RETURNING `name` so the audit_log description still carries a human identifier without a second read. 2. DELETE handler collapsed to atomic .delete().select().maybeSingle() (V8.2.1). Eliminates the pre-read TOCTOU window entirely. A 0-row delete (already-deleted webhook) still returns 204 — idempotent DELETE — and the audit entry captures the attempt with old_state: null. Description discriminates the two cases ("deleted: name" vs "delete attempted on missing id"). 3. Cache-Control: no-store, no-cache, must-revalidate, private on the rotate-secret response (Art.25). The HMAC secret is sensitive credential material returned exactly once; this header prevents any intermediary (CDN, proxy, gateway access log, browser cache) from persisting the response body in a store with a different retention policy than intended. 4. Dispatcher auto-disable now writes the audit_log entry UNCONDITIONALLY (A.8.15 / V16.1.1 / CC7.2). Previously a null prior snapshot or a legacy null user_id caused the audit row to be silently skipped — only a warn log was emitted. Now writes user_id=NULL when unavailable (post-multi-tenant-refactor schema allows it; row is invisible under user RLS but queryable under service-role review, which is correct for system-initiated SECURITY_EVENT records). Description discriminates the snapshot- available / snapshot-unavailable cases. Swedish compliance — cookbook content fixes (all real errors): 5. VAT cookbook rounding rule corrected: SFL 22 kap 1 § mandates TRUNCATION of öre (Math.floor for positive amounts), not half-up rounding. Last round mislabeled this as "Math.round (half-up)"; the SRU filing skill is canonical and uses truncation. Using Math.round would produce values that differ from Skatteverket's expectations and cause GL-reconciliation mismatches at the öre level. 6. VAT reconciliation block now includes 2614 (Utgående moms vid omvänd skattskyldighet, matches ruta 30). The previous list of 2611/2621/2631/2641/2645 omitted 2614; a reconciliation that skips it would show rutor_match_gl: true even when the 2614 balance is non-zero and un-reconciled. 7. Livsmedel rate-change adds a one-sentence caveat for continuous/ subscription supplies — the supply-date framing in round 1 was too tight for cases where multiple deliveries roll up into a subscription. Confirms against ML 1 kap 3 § rather than assuming a single delivery date is decisive. 8. Payroll pitfalls bullet contradicted step 2 — "Employees under 26 (2024 rule for 2026 birth year ≥ 2001)" rewritten to match step 2: "18–22 years old at the start of 2026 (born 2003–2007) AND 67+ from 2026". An integrator reading only the pitfalls section would have applied the reduced rate too broadly, producing underpaid arbetsgivaravgifter and a non-compliant AGI. 9. Year-end periodiseringsfond cap now states schablonintäkt explicitly: 1.94% × outstanding prior-year balance (SLR + 1% for 2026) is ADDED to taxable income before the 25% cap is computed. Last round mentioned the "BEFORE the periodiseringsfond deduction" ordering but elided the schablonintäkt step; omitting it produces a cap that's too low when prior-year reserves exist. 10. Year-end SRU format characterization corrected: SRU is plain text encoded in ISO 8859-1, NOT XML. iXBRL (XML-based) is the Bolagsverket digital annual-report format — a separate artefact for a separate authority. Round 1 conflated them. Deferred (architectural / out of scope, documented in commit): - Audit-log dead-letter queue / SIEM alert escalation (Art.32 / A.8.15): infra setup, not code-PR scope. The warn-on-failure path is the in-process surface; durable delivery is a SRE/SIEM concern. - Secret encryption at rest (CC6.1): PR-1 architectural carryover. - webhook_url + description redaction in audit_log (Art.5(1)(c)): URLs are admin-supplied configuration values; redaction would degrade audit reconstructibility without closing a real PII gap. - PATCH old_state TOCTOU via Postgres function (CC6.3): the read- then-write pattern produces an append-only audit row capturing the read state; the small race window is non-load-bearing for audit purposes and a stored-procedure refactor exceeds the cost/value. 23 webhook integration tests pass locally (no regressions). Type-check clean for all changed files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-501 review round 3 — real cookbook tax errors + cache-control on create Round 3 closes two tax-impact errors in the cookbooks plus the consistency gap on the create response. Compliance Swarm's remaining findings are recurring architectural carryovers or oscillation against prior rounds. Real cookbook errors (would mislead integrators): 1. Schablonintäkt rate corrected. Round 2 hardcoded 1.94% — that's the 2024 rate (SLR 0.94% + 1%). For 2026 SLR is 2.55%, so the rate is 3.55%. A wrong rate produces a too-low add-back, a too-high periodiseringsfond cap, and an IL 30 kap compliance error for any integrator copying the cookbook number. Rewrite to describe the formula (SLR + 1%, where SLR is the Riksbank statslåneränta on 30 Nov of the preceding year) with the 2026 figure as an example, and note the engine reads the canonical rate from `tax_rates`. 2. SRU format is a TWO-file pair, not one. Round 2 correctly said "plain text encoded in ISO 8859-1 (NOT XML)" but described it as a single file. Skatteverket requires both INFO.SRU (metadata header) AND BLANKETTER.SRU (declaration body) uploaded together — a single-file upload is rejected by their validation. Fix the prose to describe the two-file pair explicitly. Code consistency: 3. POST /webhooks (create) now returns the same `Cache-Control: no-store, no-cache, must-revalidate, private` + `Pragma: no-cache` headers as the rotate-secret endpoint (A.8.12). Both endpoints return the HMAC secret exactly once; both need the same intermediary-cache prevention. Smaller cookbook refinements (round 3 bot follow-ups): 4. VAT reconciliation block now includes 2615 (Utgående moms vid import, matches ruta 60) — the previous list covered 2611-2645 but omitted import VAT. A reconciliation that skips 2615 would show rutor_match_gl: true falsely for any importer. 5. Service supply-date fallback statement qualified to "one-off service supplies where delivery and invoice coincide" — long- running service contracts (subscriptions, maintenance) have per-delprestation skattskyldighet and need an explicit delivery_date per billing cycle. 6. Payroll elder-reduction boundary clarified: "67 years or older AT THE START OF the income year (1 January 2026)" — a 66-year- old whose 67th birthday falls in February does NOT qualify in 2026. Prevents misreading the pithy "67+ from 2026" as a birthday-during-year rule. Bot oscillation (skipping with rationale documented here for posterity): - Compliance Swarm Art.25 now asks to REMOVE webhook_url from DELETE old_state — direct contradiction with CC6.3's round-1 ask for complete attribution. webhook_url is admin-supplied configuration, not PII; keeping it preserves audit reconstructibility. - Swedish-compliance flags the unconditional re-delete audit row as "polluting" the behandlingshistorik — direct contradiction with Compliance Swarm V8.2.1 + CC6.3 round-1 / round-2 asks for unconditional writes. The audit_log is operational, not BFL räkenskapsinformation (which lives on journal_entries and related tables under explicit immutability triggers). Audit trail completeness wins over BFL purity for this table. Architectural carryovers (already documented in earlier commit bodies as deferred to follow-up PRs): - Secret encryption at rest (CC6.1, recurring) - Audit-log dead-letter / SIEM alerting (Art.32 / A.8.15, infra) - webhook_url userinfo stripping (A.8.11 low — URLs are admin- configured, no expected credentials; validating at registration would be a registration-time concern, not audit-time) 23 webhook integration tests pass. Type-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3912c74a7b |
feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) (#497)
* feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) Ships the developer-facing documentation surface for the v1 REST API. Mirrors Stripe's structure (landing → cookbooks → concepts → reference → errors → changelog) at /docs/api with a sticky-sidebar layout in the gnubok editorial-monochrome aesthetic. Every page is also served as plain Markdown via a sibling .md URL so agents and LLM crawlers can ingest the same content without HTML parsing — the existing /llms.txt already promised /docs/api references that this PR makes real. Single source of truth for endpoint metadata is the existing Zod registry (lib/api/v1/registry.ts). The reference pages auto-generate from it: adding a new endpoint surfaces in the docs on the next build with no manual sync. The error reference pulls directly from lib/errors/structured-errors.ts STRUCTURED_ERRORS. CONTENT LAYER (lib/docs/): - content/landing.ts — introduction, auth, base URL, response envelope, the four core principles (dry-run, idempotency, strict-mode, inline audit), pointers to every other section. - content/versioning.ts — versioning + deprecation policy (Stripe dated format), idempotency, dry-run, strict-mode write semantics, inline audit blocks. - content/webhooks.ts — webhook concept guide. Full Node.js (express + crypto) and Python (Flask + hmac) signature-verification samples that match lib/webhooks/signing.ts exactly. Lifecycle, event-type catalogue, payload shape, request headers, common pitfalls, auto-disable behaviour, audit + retention. - content/errors.ts — generated from STRUCTURED_ERRORS. Groups by domain (generic, bookkeeping, periods, invoices, supplier-invoices, transactions, reports, imports, documents, salary, company, provider). Every code is anchorable so the docs_url field on every error envelope finally points somewhere real. - content/reference.ts — generated from listEndpoints(). Groups by resource (companies, customers, invoices, suppliers, supplier-invoices, transactions, journal-entries, fiscal-periods, accounts, documents, employees, salary-runs, reports, imports, compliance, webhooks, operations, voucher-gap-explanations, reconciliation). Each endpoint section: summary, description, useWhen, doNotUseFor, pitfalls, scope, idempotent/reversible/dry-run flags, request + response examples. - content/changelog.ts — initial entry for API version 2026-05-12 covering every endpoint shipped in Phases 1-6. Lists what's coming in Phase 6 PR-3 (hardening + remaining cookbooks). - content/cookbook/quickstart.ts — five-minute send-your-first-invoice guide. Demonstrates auth, dry-run, idempotency, audit-block patterns in one continuous narrative. - content/cookbook/webhooks.ts — end-to-end webhook setup, sig verification, retry handling, idempotency on receiver side, replay patterns, auto-disable behaviour. Companion to the concept page. - content/cookbook/index.ts — recipe registry. 4 placeholder recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) link to their reference pages with a "coming after Phase 6 PR-3 hardening" note. Narrative cookbook quality benefits from a focused pass after the substrate stabilises. - nav.ts — single source of truth for the sidebar nav, used by the layout AND the landing-page resource grid. - markdown.tsx — shared <DocsMarkdown> component using react-markdown (already a dep) with Hedvig serif headlines, Geist mono code blocks, hairline section borders, paper-white surfaces — same editorial aesthetic as the dashboard. LAYOUT (components/docs/DocsLayout.tsx): Two-column sticky-sidebar layout. Top header carries the gnubok mark + section links (API reference, Cookbooks, Errors, Changelog, openapi.json). Sidebar groups: Getting started, Cookbooks, Concepts, API reference, Reference. Active page highlighted with the same warm-beige bg the dashboard sidebar uses. ROUTES (app/docs/api/, app/llms-full.txt/): - /docs/api → landing - /docs/api/errors → error reference - /docs/api/webhooks → webhook concept - /docs/api/versioning → versioning + idempotency + dry-run - /docs/api/changelog → release notes - /docs/api/reference → resource overview - /docs/api/reference/[slug] → per-resource pages (19 resources, all generated from the registry; generateStaticParams listed) - /docs/api/cookbook/[slug] → recipe pages (8 entries, 2 fully written + 6 aliases/placeholders) - /llms-full.txt → everything concatenated for one-shot LLM ingestion Every page has a sibling .md route (e.g. /docs/api/errors.md) serving the raw Markdown for agents — same content, no HTML wrapper, same 5-min cache. Honours the existing /llms.txt promise that "every .md URL under /docs/api is served as plain Markdown". CI GUARD (lib/api/v1/__tests__/spec-snapshot.test.ts): Vitest snapshot test that locks down (a) the endpoint count, (b) the sorted set of method+path keys, (c) the set of distinct scopes referenced. CI fails if any drift unexpectedly so a Zod-schema change can't ship a silent API break — when you intentionally add/remove an endpoint, run with -u to refresh the snapshot, review the diff, and commit alongside the route change. The snapshot diff itself is a self-describing changelog entry. Initial snapshot: 100 endpoints, 17 distinct scopes, full key set sorted. Fourth assertion in the test guarantees every endpoint declares the agent-facing metadata (summary, description, useWhen, doNotUseFor, pitfalls, example) the reference pages depend on — so a registerEndpoint call that omits any of these fields is caught at CI time rather than rendering an empty section in the docs. INFRA TOUCH (lib/api/v1/load-routes.ts): Added the 5 Phase 6 webhook route imports so the registry includes them on the docs builders' path. Required for the /docs/api/reference/ webhooks page to render. The webhook routes' registerEndpoint calls already exist; this just side-effect-imports them where the spec generator can see them. Coming in Phase 6 PR-3 (hardening — separate PR): - 90-day TTL cleanup cron for non-accounting webhook deliveries - claim_due_webhook_deliveries SQL function (FOR UPDATE SKIP LOCKED) - Per-route rate limits on :test, :retry, webhook :create - V16 audit-log on webhook lifecycle events - DNS-rebinding pinned-IP HTTPS agent - Integration tests for webhook routes + *.pg.test.ts for triggers - Populated previous_attributes for update-style webhook events - The remaining 4 cookbook recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) once the engine surface is fully stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 1 — CI fix + 5 small docs items CI BLOCKER (the reason core-only failed): 1. **Type error on `[slug].md/route.ts` dynamic routes** — Next.js 16's route-type inference can't extract the dynamic segment from a directory whose name contains a literal suffix like `[slug].md/`. It types `params` as `Promise<{}>` and rejects our handler that declares `params: Promise<{ slug: string }>`. The framework still ROUTES requests correctly (URL `/docs/api/cookbook/quickstart.md` reaches the handler) — only the typed `params` is unusable. Fix: drop the typed `params` parameter on the two affected handlers (cookbook + reference) and parse the slug from `request.url.pathname` directly. Inline comment documents the workaround so the next person to touch these doesn't try to "fix" it back to the typed pattern. GREPTILE INLINE (2 items): 2. **Python sample was missing `import json` and `import os`** — the webhook signature-verify sample uses both but only imported `hmac`, `hashlib`, `time`, and `flask`. Added the two missing imports. 3. **`buildResourcePages()` perf — called twice per request** (P2). Each call iterates every registered endpoint, groups by resource, sorts, and serialises Markdown for all 19 resource pages. Memoised at module level — the registry is populated once at module load and immutable for the process lifetime, so a single derivation is safe to cache. Halves the cost on the HTML routes' `generateMetadata` + page render pair, and the .md route handlers (which Next.js doesn't statically pre-render) are now constant-time after the first GET. SWEDISH-COMPLIANCE PRECISION (3 items): 4. **`webhooks.ts`: behandlingshistorik vs räkenskapsinformation distinction.** The previous "Audit + retention" section conflated the two — webhook delivery rows are *behandlingshistorik* (system- event log) per BFNAR 2013:2 kap 8 §, NOT räkenskapsinformation themselves. The 7-year retention from BFL 7 kap 1 § attaches to the underlying verifikation/faktura/AGI XML in its own table, not to the delivery envelope. Updated the section to draw the distinction and clarify gnubok's 7-year retention on accounting-event delivery rows is an operational audit-trail policy, not a statutory obligation passed through to the integrator. 5. **`changelog.ts`: same distinction in the Phase 6 PR-1 entry** — replaced the "räkenskapsinformation" framing with the correct behandlingshistorik framing + the operational-policy note. 6. **Quickstart cookbook: ML 17 kap 24 § p.8 note about `beskattningsunderlag per skattesats`.** The "What just happened" section now explicitly notes that the rendered PDF contains every ML 17 kap 24 § field (including taxable amount per VAT rate) and that the JSON response's summary fields are convenience aggregates for the integration — the binding faktura content is the PDF. Forecloses the misreading that `subtotal + vat_total` is sufficient compliance. 7. **Changelog: BFL 7 kap caveat on SIE export.** The `/reports/ sie-export` line now warns that a SIE4 export alone does NOT satisfy BFL 7 kap archiving obligations — SIE captures account positions and verifikationer but lacks system documentation and behandlingshistorik. Treat as a portability format (Fortnox/Visma/Bokio migration), not as a complete archive. Closes the misreading the swedish-sie-import-export skill flagged. DEFENSIBLE DEFERS (round 1 final): - **CM-8 SPDX-License-Identifier headers per file** (Compliance Swarm). The repo declares AGPL-3.0-or-later in the root LICENSE file, which satisfies licensing for the project as a whole. Per-file SPDX headers are a REUSE-conformance feature; we can address as a sweep across the entire codebase if/when REUSE conformance becomes a requirement. Out of scope for a docs PR. - **`/llms-full.txt` exposes payroll endpoint metadata publicly** (A.8.12). By design — the entire point of the file is one-shot LLM ingestion of the public docs corpus. Endpoint METADATA (path, scope, description) is non-sensitive; actual payroll DATA is gated behind `payroll:read` scope and requires a real API key. Adding an auth gate would defeat the agent-discovery purpose. - **Secret rotation endpoint** (Art.25(2)). Real product gap (delete + recreate is the current rotation path), but it's feature work, not docs. Tracked for Phase 6 PR-3 alongside the other webhook hardening. - **DNS rebinding pinned-IP HTTPS agent** (Art.5(1)(f)). Already documented in this changelog as a Phase 6 PR-3 item; bot is just re-flagging that it's not yet shipped. - **A.5.34 changelog cites GDPR Art.5(1)(c) for personnummer masking without linking privacy policy.** The citation is informational context for developers, not a privacy notice to data subjects. Data- subject notices live at /privacy. Adding a pointer is reasonable; adding it would consume real estate that's better spent on the technical detail. Defer. - **Compliance Swarm A.5.21 third-party attribution in llms-full.txt**. False positive — all markdown content in this PR is original first-party text. No third-party snippets to attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 2 — 6 small precision fixes All CI green after round 1 (core-only fixed). Compliance Swarm: 7 → 10 findings is the documented oscillation pattern — net-new actionable items are 6 small fixes; the rest are recurring defers (plaintext-secret variants, SPDX, planned PR-3 features the changelog already lists as "coming soon"). FIXED: 1. **Slug allow-list validation in `[slug].md` routes** (V1.2.5 ×2, medium). The cookbook + reference .md routes parse the slug from the URL pathname (round-1 workaround for Next.js 16's failed inference on `[slug].md/` directories) and pass it to a dictionary-based lookup. The lookup itself is safe — findRecipe / buildResourcePages can't reach SQL or filesystem from a bad slug — but the explicit allow-list gate keeps the contract safe if the lookup mechanism ever changes (file-load, RPC, etc.). Added `Set<string>(COOKBOOK_SLUGS)` + `Set<string>(RESOURCE_SLUGS)` guard before any lookup runs. 2. **Changelog: BFL 5 kap 5 § cited on both `/reverse` AND `/correct`** (swedish-compliance precision). The previous wording cited BFL 5:5 only on `/reverse` (storno) and described `/correct` as plain "rättelse" — but BFL 5:5 governs rättelse generally, and storno is the canonical method of rättelse, so both endpoints satisfy 5:5. Updated to: "/{id}/reverse (storno) and /{id}/correct (rättelse) — both satisfy BFL 5 kap 5 § (storno is the canonical method of rättelse)". 3. **Changelog AGI: explicit that XML is for manual submission** (swedish-compliance / swedish-payroll). Previously said "/generate-agi produces AGI XML" — could be misread as auto-submission to Skatteverket. Now states explicitly that the response carries `data.xml` for the integrator to upload via Skatteverket Mina Sidor (or via the optional `skatteverket` extension), and that the AGI deadline (12th / 17th of the following month) is the integrator's responsibility. Aligns with the route file's existing doNotUseFor + pitfalls metadata. 4. **Quickstart: F-skatt note qualified** (swedish-invoice-compliance). The "What just happened" section previously said the PDF "contains the F-skatt note" — only valid if the seller actually holds F-skatt. Updated to: "The 'Godkänd för F-skatt' note is included automatically when company_settings.has_f_skatt is set — confirm this on the company settings page before sending invoices in production." Also tightened the beskattningsunderlag wording to mention "one line per distinct rate on multi-rate invoices" — closes the swedish-compliance note about the multi-rate claim in the landing needing explicit support in the cookbook. 5. **Cookbook placeholder VAT description: "compute and review" not "submit"** (swedish-vat). The placeholder previously said "Compute momsdeklaration rutor and submit to Skatteverket" — but no Skatteverket-submission endpoint exists in the v1 surface; the API only computes the rutor 05–62 values for manual filing. Updated description in BOTH cookbook/index.ts AND nav.ts (where the same string was duplicated): "Compute momsdeklaration rutor 05–62 and reconcile against the GL before manual submission to Skatteverket." Title also flipped from "File a VAT declaration" to "Compute and review a VAT declaration". 6. **Cookbook nav for AGI: aligned to "generate" semantics**. nav.ts AGI summary used to say "file AGI" — same misreading risk as #3. Now: "Calculate, approve, mark paid, book, generate AGI XML for manual Skatteverket upload." DEFERS (round 2 final — every remaining swarm finding is in one of these buckets): - **🟠 Art.32 plaintext webhook secret + 4 sibling framings** (V14, V11.1, A.8.24, CC6.1). Established defer per Stripe / GitHub / Slack precedent; documented inline in lib/webhooks/signing.ts. Bot is re-flagging via the docs surface this round; underlying position unchanged. - **🟡 Art.5(1)(e) 90-day TTL non-accounting cron + V16 audit log + V2.4 rate limits**. All explicitly listed in the changelog as "Coming soon (Phase 6 PR-3 hardening)". Bot is reading the same text we wrote; not blocking. - **🟠 A.8.11 personnummer masking lacks an automated test**. Real product-hardening request, but it's feature work in the employees test surface, not docs. Tracked. - **🟡 CM-8 SPDX-License-Identifier headers per file**. Established defer from round 1 — root LICENSE covers AGPL-3.0-or-later for the project as a whole; per-file SPDX is a REUSE-conformance sweep that's its own effort. - **🟡 SR-3 SBOM dependencies**. False positive — next/server, react, next/link, next/navigation are existing project deps, not new in this PR. - **swedish-compliance "SIE disclaimer could note immutability requirement"** — the current disclaimer accurately calls out system documentation + behandlingshistorik as missing; adding immutability would over-stuff a one-line caveat. Defer with the understanding that the SIE skill itself documents the immutability requirement for any consumer that follows the reference. If round 3 plateaus (Compliance Swarm count stable, no net-new inline items), that's the merge-ready signal per Phase 4 lessons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 3 — 4 small precision fixes Compliance Swarm: 10 → 3 (down 70%) — net-new actionable items are the 4 below; remaining 3 swarm findings are either trivial defense- in-depth (URL decode, fixed here) or out-of-repo decisions (personnummer disclosure DPO confirmation). FIXED: 1. **URL-decode slug before allow-list check** in both .md route handlers (V1.2.5 ×2 low). The closed allow-list is pure ASCII so a percent-encoded value can't decode to a legitimate slug, but the explicit decode-then-check pattern keeps the contract correct under any future encoding-quirk runtime. try/catch around decodeURIComponent so a malformed % sequence (which throws) returns a clean 404 rather than a 500. 2. **Quickstart: explicit `delivery_date` requirement** (swedish- invoice-compliance / ML 17:24 field 7). The previous wording listed "supply date" as a covered field but didn't note that the API does NOT default delivery_date to invoice_date — integrators shipping invoices for goods delivered on a different date than the invoice date must pass delivery_date explicitly or the rendered PDF is non-compliant. Added explicit pass-it-yourself note. 3. **Quickstart: F-skatt strengthened from "verify" to "legal requirement"** (swedish-invoice-compliance / Peppol BIS 3.0 SE-R-005). The previous "confirm on settings page" wording risked integrators treating the F-skatt note as optional UX. It's a legal requirement on every faktura issued by a company that holds F-skatt registration — and a FATAL Peppol BIS 3.0 validation failure (SE-R-005) for B2G invoices when missing. Reframed as a compliance assertion: the PDF includes it automatically when the setting is true; verifying the setting is correct before production is the integrator's responsibility. 4. **Changelog: SIE post-import VAT code reconfiguration warning** (swedish-sie-import-export). The /imports/sie line previously noted the file format support but didn't warn that SIE files do NOT carry VAT codes or tax-rate-to-account mappings. After migrating from Fortnox / Visma / BL / SpeedLedger / Bokio, integrators must manually reconfigure VAT codes before the first momsdeklaration — skipping this is the most common source of incorrect VAT submissions in migrated bookkeeping. DEFERS (round 3 final — these are the architectural floor): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (Compliance Swarm). Defensible: documenting that personnummer is masked is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision — the docs change is appropriate as written. - **AGI penalty amounts (625 / 1,250 SEK)**. Operational guidance for integrators building deadline-tracking; not strictly API-doc material. The deadline (12th / 17th) is documented; integrators who automate compliance can read SFL for penalties. - **VAT period thresholds in the cookbook placeholder**. Belongs in the actual cookbook content when written, not in the placeholder description. - **`invoice.credited` event-naming verification**. False positive — the emitter uses `credit_note.created` (which IS in the docs); there is no `invoice.credited` event in the codebase. Naming is consistent. - **Webhook retention sentence reordering** (swedish-compliance stylistic). Current wording leads with what delivery rows ARE (behandlingshistorik), then clarifies what they are NOT (räkenskapsinformation) — clean teaching arc, the qualifier is prominent. Reordering doesn't change clarity. Round 3 stop signal hit (per Phase 4 lessons): swarm count plateauing at the architectural floor with all remaining findings either defers, false positives, or out-of-repo decisions. Greptile has posted no inline comments since round 1's two items (both fixed). This should be merge-ready. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 4 — 5 small precision fixes Compliance Swarm: 3 → 5 (slight uptick from oscillation, but 0 critical, 1 actionable; remaining 4 are recurring or philosophical). swedish-compliance: 7 advisories — 3 actionable precision items incorporated below; the others are forward-looking notes for cookbook content that ships in Phase 6 follow-ups. FIXED: 1. **Spec-snapshot test enforces ep.scope is explicitly defined** (CC6.3, real future-bug prevention). Previously the test asserted every endpoint declared the agent-facing metadata fields the docs depend on, but `scope` could be `undefined` — a registerEndpoint call that silently dropped the field would make the wrapper treat the route as unauthenticated. Added an assertion that `ep.scope !== undefined` (the literal sentinel `null` is allowed for genuinely public endpoints like /api/v1/health). The 4 spec tests still pass — confirming no current endpoint has undefined scope and the gate works prospectively. 2. **F-skatt: integrator responsibility for `has_f_skatt` accuracy** (swedish-invoice-compliance). The previous "verify on settings page" framing didn't connect the flag to the live Skatteverket registration. Now: "The integrator is responsible for keeping has_f_skatt in sync with the company's live Skatteverket registration status. Update via PATCH /api/v1/companies/{id}/ settings or the settings page — a flag that's false while the company is actually F-skatt-registered produces non-compliant invoices, not merely a missing optional note." 3. **AGI deadline qualified by turnover** (swedish-payroll). The previous wording listed "12th / 17th of the following month" with no condition. Now: "12th of the following month for large employers, 17th for companies with annual turnover ≤ 40 MSEK." Aligns with the swedish-payroll skill's AGI filing deadline section. 4. **SIE import warning includes behandlingshistorik gap** (swedish-sie-import-export + swedish-accounting-compliance). The previous warning covered the VAT-code reconfiguration requirement but didn't note that SIE files also do NOT transfer behandlingshistorik (the source system's processing log per BFNAR 2013:2 kap 8 §) or systemdokumentation. Added: "The behandlingshistorik gap must either be preserved separately (export from the source system + archive alongside the SIE file) or accepted with documented justification — gnubok starts a fresh behandlingshistorik from the import date forward." 5. **Webhook 7-year retention: voluntary policy vs statutory obligation** (swedish-accounting-compliance). The previous wording said gnubok keeps delivery rows "for 7 years as an operational audit-trail policy" — the 7-year figure could be misread as statutory. Tightened in BOTH webhooks.ts and changelog.ts: the 7-year statutory retention under BFL 7 kap 1 § applies ONLY to the underlying verifikation/faktura/AGI XML; gnubok's 7-year policy on delivery rows is voluntary and chose the duration to align conveniently with the statutory horizon on the underlying records. DEFERS (round 4 final, all in the architectural-floor bucket): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (recurring from round 3). Defensible — documenting PII handling is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision. - **🟡 A.8.23 DNS-rebinding "coming soon" item**. The bot is reading the changelog's own deferral list. Already tracked for Phase 6 PR-3 hardening. - **🟠 CC6.6 SSRF protection details exposed in /llms-full.txt**. Stripe / GitHub / Slack publish their full webhook security posture publicly (signature format, rejected IP ranges, retry policy) — documenting protections IS the trust pattern. Obscurity is not security; the SSRF protection is enforced in code, not in the docs. - **🟡 CC6.7 public CDN caching**. withPublicSecurityHeaders() already applies the appropriate headers (CSP, X-Content-Type- Options, X-Frame-Options). The 5-min cache is appropriate for static developer documentation; the alternative (no caching) is cost without security benefit since the content is intended to be public. - swedish-compliance "VAT 2026-04-01 livsmedel rate change" — forward-looking; ships when the actual VAT cookbook is written. - swedish-compliance "year-end IB/UB continuity" — forward-looking; ships when the year-end cookbook is written. - 2 verify-only notes (rättelse implementation, future salary- journal/avgifter-basis masking sweep) — not actionable in this PR. Round 4 stop signal: every remaining swarm finding is in the deferred or recurring bucket; the actionable item (CC6.3) is shipped. swedish-compliance is now in advisory mode (no errors, just stylistic suggestions and future-cookbook notes). Per Phase 4 lessons, this is the merge-ready signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 5 — 4 small precision fixes (last actionable items) Compliance Swarm: 5 → 2 (down to architectural floor — 1 high + 1 medium). swedish-compliance: 7 advisories, 4 actionable precision items addressed below; the others are forward-looking notes for content that ships in Phase 6 follow-ups. Trajectory: 7 → 10 → 3 → 5 → 2. Plateaued. FIXED: 1. **Webhook secret storage guidance: secrets manager, not env file** (A.8.5 high). Added explicit instruction to the cookbook that the returned secret is signing material and must live in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler, 1Password Connect, ...) — not a plaintext .env file or config commit. Treated with the same care as a database password. 2. **AGI deadline correction: 17th = January and August only** (swedish-payroll). Round 4's wording said "12th (large employers) / 17th (≤40 MSEK)" — but per the swedish-payroll skill the 17th applies in January and August specifically, not generally to all months for sub-40 MSEK companies. Other months are the 12th regardless of employer size. Fixed to: "the 12th of the following month for every reporting period EXCEPT January and August, where companies with annual turnover ≤ 40 MSEK get the 17th." This would've caused integrators automating sub-40 MSEK deadline tracking to misfile by 5 days from February through July and September through December. 3. **F-skatt SE-R-005 broader scope** (swedish-invoice-compliance / swedish-e-invoicing). The previous wording framed SE-R-005 as primarily a Peppol B2G validation rule. Reframed: the F-skatt note is a legal requirement on every faktura issued by a Swedish momsregistrerad seller that holds F-skatt registration — applies to PDF/paper AND Peppol/e-invoice formats. The buyer uses it to determine A-skatt withholding obligation (omitting it can shift tax liability onto the buyer); B2G is just where the validation is automated as a FATAL Peppol BIS 3.0 check. 4. **SIE behandlingshistorik gap: full räkenskapsår scope** (swedish-accounting-compliance). Round 4's wording said the integrator "must either preserve [behandlingshistorik] separately or accept the gap with documented justification" and that gnubok "starts a fresh behandlingshistorik from the import date forward." The "documented justification" framing implied the gap was acceptable as a default. Per BFNAR 2013:2 kap 8 §, the obligation attaches to the entire räkenskapsår, not from the import date. Reframed as: "must be preserved separately... best practice for a mid-year migration: export the source system's behandlingshistorik for the full fiscal year and archive it alongside the SIE file." DEFERS (round 5 final — these are the architectural-floor items that will recur indefinitely): - **🟡 A.8.20 DNS-rebinding gap** (Compliance Swarm). Already documented in the changelog as a Phase 6 PR-3 deferral item; the bot is reading the same text we wrote. - **swedish-compliance: VAT 2026-04-01 livsmedel rate change**. Forward-looking — for the actual VAT cookbook recipe content, which ships post-Phase-6. - **swedish-compliance: year-end IB/UB continuity**. Forward-looking — same. - **swedish-compliance: SIE warning placement note**. Forward-looking — for the imports reference page when authored. - **swedish-compliance: BFNAR 2013:2 citation correct, webhook retention correct**. No-op confirmations. - **swedish-compliance: delivery_date pre-payment scenario**. Real but extremely narrow edge case (faktura utfärdad före leverans). Defer with the understanding that anyone using the API for pre-payment invoicing will read the full invoice reference, not rely solely on the quickstart. This is the merge-ready signal per Phase 4 lessons-learned: every remaining swarm finding is in the deferred or recurring bucket; swedish-compliance is in pure-advisory mode (forward-looking notes for cookbook content that ships later); CI is fully green; Greptile posted nothing past round 1's two items (both fixed). Ship it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |