* 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>
210 lines
12 KiB
TypeScript
210 lines
12 KiB
TypeScript
export const COOKBOOK_YEAR_END_MD = `# Cookbook — year-end closing (bokslut)
|
||
|
||
> Lock a Swedish fiscal year, run the year-end procedures, set opening balances for the new year. Built around BFL 5 kap and 7 kap requirements (verifikationskedja, balanskontinuitet, 7-year retention).
|
||
|
||
This is the operational companion to the [Fiscal-periods reference](/docs/api/reference/fiscal-periods). Year-end is the single most consequential lifecycle event in a Swedish bookkeeping system — closing is irreversible per BFL 5 kap 8 §. Treat the steps below as a checklist, not a script to copy-paste.
|
||
|
||
## What you'll need
|
||
|
||
- A test API key with \`bookkeeping:write\`, \`bookkeeping:read\`, and \`reports:read\` scopes.
|
||
- All transactions for the year posted (no drafts).
|
||
- All VAT declarations for the year filed (12 monthly, 4 quarterly, or 1 annual — see the [VAT cookbook](/docs/api/cookbook/file-vat-declaration)).
|
||
- All AGI declarations filed and kontrolluppgift (KU) generated.
|
||
- The bokslut date — usually 31 december for calendar-year companies (kalenderår), or the last day of the räkenskapsår for off-calendar (brutet räkenskapsår).
|
||
|
||
## 1. Pre-flight: continuity check (IB/UB per BFL 5 kap)
|
||
|
||
Before locking anything, verify the period's continuity. BFL 5 kap requires that the closing balance (UB) of year N equals the opening balance (IB) of year N+1 on every BAS 1xxx, 2xxx account.
|
||
|
||
\`\`\`bash
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/reports/continuity-check?from=2025-01-01&to=2025-12-31" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
Response:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"checks": [
|
||
{ "account": "1930", "year_end_ub": 156432.00, "next_year_ib": 156432.00, "match": true },
|
||
{ "account": "1510", "year_end_ub": 47100.00, "next_year_ib": 47100.00, "match": true },
|
||
{ "account": "2440", "year_end_ub": -8200.00, "next_year_ib": -8200.00, "match": true },
|
||
...
|
||
],
|
||
"ib_ub_continuity_holds": true,
|
||
"discrepancy_count": 0
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
\`ib_ub_continuity_holds: false\` is a BFL violation — investigate before proceeding. A discrepancy on \`1930\` (bank) usually means a missed reconciliation; on \`2611-2641\` (moms) means a VAT declaration disagrees with the GL.
|
||
|
||
## 2. Pre-flight: voucher gaps (BFNAR 2013:2)
|
||
|
||
BFNAR 2013:2 kap 6–7 §§ requires explanations for missing voucher numbers. Run the check:
|
||
|
||
\`\`\`bash
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/compliance/check?type=voucher_gaps&period=2025" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
For every gap returned, file an explanation via \`POST /voucher-gap-explanations\` before the year is closed. Skatteverket can ask for these years later under the 7-year retention rule.
|
||
|
||
## 3. Pre-flight: missing documents on posted entries
|
||
|
||
For aktiebolag, BFL 7 kap requires every verifikation to have its underlag (receipt, faktura, kontrakt) attached. The check:
|
||
|
||
\`\`\`bash
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/compliance/check?type=unmatched_documents&period=2025" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
Attach missing documents via \`POST /journal-entries/{id}/documents\` before locking. After locking, the document-immutability trigger prevents detaching but allows attaching (first-link is treated as completing the audit trail, not modifying it).
|
||
|
||
## 4. Lock the period
|
||
|
||
\`POST /fiscal-periods/{id}/lock\` blocks all writes to the period while leaving it reversible. Use this when the year's books are "done" but you may still need to add a year-end accrual entry under supervision.
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://gnubok.app/api/v1/companies/$COMPANY_ID/fiscal-periods/$PERIOD_ID/lock" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
Locked periods can be unlocked via \`PATCH\` with a clear reason that lands in the audit log. After year-end procedures are complete (step 6), close instead — closing is irreversible.
|
||
|
||
## 5. Run year-end procedures
|
||
|
||
\`POST /fiscal-periods/{id}/year-end\` is the engine-touching step. It:
|
||
|
||
1. Posts the resultatdisposition — closes every 3xxx, 7xxx, 8xxx account into \`8910\` (årets resultat), then transfers to \`2099\` (årets resultat in equity).
|
||
2. Posts the periodiseringsfond adjustment if \`company_settings.use_periodiseringsfond\` is true.
|
||
3. Posts överavskrivningar för fastigheter och inventarier if the depreciation differential exists.
|
||
4. Computes bolagsskatten on the taxable result (currently 20.6% for 2026) and posts the \`8811\` (skatt på årets resultat) ↔ \`2512\` (beräknad skatt) entry.
|
||
5. Generates the opening-balance journal for year N+1 in a single atomic batch — every IB entry on the new period referencing the UB of the closing period.
|
||
|
||
This is an async operation:
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://gnubok.app/api/v1/companies/$COMPANY_ID/fiscal-periods/$PERIOD_ID/year-end" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"next_period_id": "fp_...",
|
||
"result_disposition": {
|
||
"to_periodiseringsfond": 120000,
|
||
"to_balanserat_resultat": 380000
|
||
}
|
||
}'
|
||
\`\`\`
|
||
|
||
Response is a 202 with an operation handle (year-end can take minutes for large books):
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"operation_id": "op_...",
|
||
"status": "queued",
|
||
"poll_url": "/api/v1/operations/op_...",
|
||
"webhook_event": "operation.completed"
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
Poll the operation; on \`succeeded\` the result block lists every voucher posted:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"operation_id": "op_...",
|
||
"status": "succeeded",
|
||
"result": {
|
||
"year_end_voucher_numbers": ["A-2025-9001", "A-2025-9002", "A-2025-9003"],
|
||
"opening_balance_voucher_number": "A-2026-0001",
|
||
"årets_resultat_amount": 580000.00,
|
||
"bolagsskatt_amount": 119480.00,
|
||
"periodiseringsfond_set_aside": 120000.00
|
||
}
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
## 6. Verify opening balances on the new year
|
||
|
||
After year-end runs, the new period (\`next_period_id\`) has IB on every balance-sheet account matching the prior period's UB. Verify:
|
||
|
||
\`\`\`bash
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/reports/trial-balance?period=2026" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
The \`opening_balance\` column on every 1xxx/2xxx row should equal the \`closing_balance\` on the same row for 2025. \`3xxx-8xxx\` accounts have zero opening balance — the year-end procedure cleared them into \`2099\`.
|
||
|
||
If opening balances are wrong (rare; the engine validates before posting), use \`POST /fiscal-periods/{id}/opening-balances\` with explicit values — but this is a backstop, not a routine path. The year-end procedure should produce correct IB without manual intervention.
|
||
|
||
## 7. Close the period (irreversible)
|
||
|
||
After the declaration, the auditor's review (if applicable), and any year-end accruals are settled, close the period. **BFL 5 kap 8 §: closing is irreversible.** No code path can re-open a closed period.
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://gnubok.app/api/v1/companies/$COMPANY_ID/fiscal-periods/$PERIOD_ID/close" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{ "confirmation_phrase": "close period 2025 irrevocably" }'
|
||
\`\`\`
|
||
|
||
The \`confirmation_phrase\` is a forced typed acknowledgment. The request fails with \`VALIDATION_ERROR\` unless the literal phrase matches.
|
||
|
||
## 8. Generate the årsredovisning (aktiebolag only)
|
||
|
||
For an AB, the annual report (årsredovisning) is filed with Bolagsverket within 7 months of the fiscal-year end. v1 produces the K2/K3-formatted source data; you typeset it externally and submit via Bolagsverket Mina Sidor.
|
||
|
||
\`\`\`bash
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/reports/annual-report?year=2025" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
The response carries the resultaträkning, balansräkning, kassaflödesanalys (K3 only), the noter pre-populated from the GL, and the förvaltningsberättelse template. The signing flow (every styrelseledamot must sign) is outside the API surface.
|
||
|
||
## 9. Generate INK2 / NE for the tax declaration
|
||
|
||
The tax declaration (INK2 for AB, NE-bilaga for enskild firma) is due in March/May depending on entity type and fiscal-year shape. The endpoints:
|
||
|
||
\`\`\`bash
|
||
# Aktiebolag — INK2
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/reports/ink2?year=2025" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
|
||
# Enskild firma — NE-bilaga
|
||
curl "https://gnubok.app/api/v1/companies/$COMPANY_ID/reports/ne-bilaga?year=2025" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..."
|
||
\`\`\`
|
||
|
||
These produce **two files** — \`INFO.SRU\` (metadata header) plus \`BLANKETTER.SRU\` (the declaration body) — uploaded together as a single submission to Skatteverket. **The SRU format is plain text encoded in ISO 8859-1 (NOT XML)** — a tagged record-line shape per Skatteverket's SRU specification. A single-file upload is rejected by Skatteverket's validation. This is a separate artefact from Bolagsverket's digital årsredovisning filing, which uses **iXBRL** (an XML-based standard). SRU goes to Skatteverket for INK2/INK2R/INK2S declarations; iXBRL goes to Bolagsverket for the public annual report. Don't conflate them.
|
||
|
||
> **Note:** \`/reports/ink2\` and \`/reports/ne-bilaga\` are queued as deferred endpoints — see the API changelog for current availability. Until they ship, generate the inputs via \`/reports/trial-balance?year=...\` and feed your tax-software of choice.
|
||
|
||
## Brutet räkenskapsår (off-calendar year)
|
||
|
||
For a company on a non-calendar fiscal year (e.g. 2024-07-01 → 2025-06-30), the entire flow is identical — substitute the actual period dates everywhere. The IB/UB continuity check, the year-end procedure, and the lock/close lifecycle all operate on the period regardless of its alignment with the calendar.
|
||
|
||
The one exception: the VAT declaration cadence is monthly/kvartalsvis/årlig regardless of the räkenskapsår shape, so a brutet räkenskapsår company files moms on calendar months while closing books on its own fiscal calendar.
|
||
|
||
## Common pitfalls
|
||
|
||
- **Don't year-end before the last month's moms is declared.** The year-end procedure expects every moms-account balance to reconcile. A pending declaration leaves dangling balances on 2611-2641.
|
||
- **Periodiseringsfond reserve cap.** Per IL 30 kap 5 § and 30 kap 6 a §, AB can set aside max 25% of the **taxable profit AFTER schablonintäkt has been added back and BEFORE the periodiseringsfond deduction itself**. The schablonintäkt rate is **(SLR + 1%) × outstanding prior-year periodiseringsfonder balance**, where SLR is Skatteverket's statslåneränta as published on 30 November of the preceding income year — for 2026 SLR is 2.55%, so the rate is **3.55%**. The engine reads the canonical rate from \`tax_rates\` and surfaces both the schablonintäkt amount and the resulting cap on the year-end result block; pass \`to_periodiseringsfond\` as the desired set-aside amount and the engine returns \`VALIDATION_ERROR\` with the maximum allowed value if it exceeds the cap. Note: under BFL/BFNAR 2016:10 kap 13 (materiellt samband for AB), periodiseringsfond is BOOKED as an obeskattad reserv on accounts 2110–2139, not just declared on INK2 — the engine posts the booking automatically as part of the year-end procedure.
|
||
- **Don't unlock a period after the AB's annual report is filed.** The signed annual report is a public document at Bolagsverket; unlocking and changing the books afterwards creates a discrepancy with the filed report (which is itself an audit finding). Use storno (\`POST /journal-entries/{id}/reverse\`) to correct in the current open period instead.
|
||
- **Year-end is async.** The operation can take minutes; don't block your request loop on it. Subscribe to \`operation.completed\` or poll \`GET /operations/{id}\` with reasonable backoff (every 5–10s).
|
||
- **The closing-period confirmation phrase is locale-sensitive.** It must match exactly. If you localise the prompt to Swedish ("stäng period 2025 oåterkalleligt"), document the exact string your UI requires — the API requires the English version above.
|
||
|
||
## Next steps
|
||
|
||
- **[VAT declaration cookbook](/docs/api/cookbook/file-vat-declaration)** — covers each monthly cycle within the year.
|
||
- **[Payroll cookbook](/docs/api/cookbook/run-payroll-and-agi)** — kontrolluppgift season (jan of year N+1) follows naturally after year-end.
|
||
- **[Fiscal-periods reference](/docs/api/reference/fiscal-periods)** — every parameter, every state transition.
|
||
`
|