* 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>
222 lines
14 KiB
TypeScript
222 lines
14 KiB
TypeScript
export const COOKBOOK_PAYROLL_AGI_MD = `# Cookbook: run payroll and generate the AGI XML
|
||
|
||
> Drive a Swedish salary run from draft to booked, then generate the arbetsgivardeklaration på individnivå (AGI) XML for manual submission to Skatteverket. Five-step lifecycle, every state transition idempotent and dry-runnable (generate-agi is idempotent but not dry-runnable).
|
||
|
||
This is the operational companion to the [Salary-runs reference](/docs/api/reference/salary-runs). The whole lifecycle is API-callable end-to-end: create the run, attach employees (\`POST /salary-runs/{id}/employees\`), add manual payslip lines if needed, calculate, approve, mark paid, book, generate AGI. Per-employee results are readable via \`GET /salary-runs/{id}/employees\` (list) and \`GET /salary-runs/{id}/employees/{employeeId}\` (payslip detail incl. line items and the step-by-step calculation breakdown); the rendered payslip PDF is at \`GET /salary-runs/{id}/payslips/{employeeId}/pdf\`.
|
||
|
||
## What you'll need
|
||
|
||
- A test API key with \`payroll:read\` AND \`payroll:write\` scopes. \`payroll:write\` is required for every state transition; \`payroll:read\` covers the read paths plus the elevated-scope gate on the webhook \`salary_run.*\` subscription.
|
||
- At least one employee on file with the payroll fields set (\`monthly_salary\` or \`hourly_rate\`, \`tax_table_number\`, \`tax_column\`, \`f_skatt_status\`). \`GET /employees\` masks personnummer to \`ÅÅÅÅMMDDXXXX\`; \`GET /employees/{id}\` returns the full value (deliberate drill-in, GDPR Art.5(1)(c)).
|
||
- An open fiscal period covering the salary date.
|
||
|
||
## 1. Create a salary run (draft)
|
||
|
||
\`POST /salary-runs\` opens an **empty** run in \`draft\` status: it takes only the period + payment metadata (\`period_year\`, \`period_month\`, \`payment_date\`, optional \`voucher_series\` and \`notes\`). Attach each employee with \`POST /salary-runs/{id}/employees\` (body \`{ "employee_id": "..." }\`, plus \`hours_worked\` for hourly staff) before you \`:calculate\`. The attach snapshots the employee's pay config onto the run and seeds the base salary line; one-off components (bonus, deduction, reimbursement) go through \`POST /salary-runs/{id}/employees/{employeeId}/lines\` while the run is still a draft. Line edits never recompute tax: always \`:calculate\` afterwards.
|
||
|
||
\`\`\`bash
|
||
curl "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"period_year": 2026,
|
||
"period_month": 5,
|
||
"payment_date": "2026-05-25",
|
||
"voucher_series": "L"
|
||
}'
|
||
\`\`\`
|
||
|
||
Response:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"id": "run_...",
|
||
"status": "draft",
|
||
"period_year": 2026,
|
||
"period_month": 5,
|
||
"payment_date": "2026-05-25",
|
||
"voucher_series": "L",
|
||
"total_gross": 0,
|
||
"total_tax": 0,
|
||
"total_net": 0,
|
||
"total_avgifter": 0,
|
||
"total_employer_cost": 0
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
Totals are 0 until you calculate.
|
||
|
||
## 2. Calculate (math + draft → review)
|
||
|
||
\`POST /salary-runs/{id}/calculate\` runs the full Swedish tax engine: skattetabell lookup per employee, sociala avgifter at the current rate (31.42% for 2026), age-adjusted reductions per Prop. 2025/26:66 (the youth-reduction band is **18-22 years old at the start of 2026**: i.e. employees **born 2003-2007** for the 2026 income year, NOT a blanket "under-25"; the elder reduction applies at **67+ from 2026**, not 66+), förmånsbeskattning, semesterlöneskuld, OB-tillägg, traktamente.
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs/$SR_ID/calculate" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
Response transitions \`draft → review\`:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"id": "run_...",
|
||
"status": "review",
|
||
"period_year": 2026,
|
||
"period_month": 5,
|
||
"total_gross": 80000.00,
|
||
"total_tax": 24300.00,
|
||
"total_net": 55700.00,
|
||
"total_avgifter": 25136.00,
|
||
"total_employer_cost": 105136.00,
|
||
"warnings": []
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
The \`warnings\` array surfaces non-blocking issues (Skatteverket tax-table fallback, läkarintyg day-8 / Försäkringskassan day-15 transitions, F-skatt \`not_verified\` employees); the run still advances to \`review\`. Per-employee figures are not returned inline: read them from the run detail or the salary-journal report.
|
||
|
||
The \`review\` status is a soft hold: the math is done but no journal entries are posted yet. Treat this as the human-review step.
|
||
|
||
## 3. Approve (review → approved)
|
||
|
||
\`POST /salary-runs/{id}/approve\` validates and locks the run for payment. \`PATCH\` on a salary run is **draft-only** (\`SALARY_RUN_PATCH_NOT_DRAFT\`): once a run leaves \`draft\` it can no longer be edited, and there is no \`:unapprove\`. Correcting a booked run is done via the forthcoming \`:correct\` verb (storno-then-rebook).
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs/$SR_ID/approve" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
Response shows \`status: 'approved'\` plus a \`warnings\` array. The endpoint validates every employee on the run and returns **all** problems at once (not just the first):
|
||
- Bank details present: \`clearing_number\` AND \`bank_account_number\` (required for the transfer)
|
||
- \`calculation_breakdown\` populated (proves \`:calculate\` has run)
|
||
|
||
Missing email is a **non-blocking warning** (lönebesked can't be sent automatically) and does not fail approval. Validation failures return \`SALARY_RUN_APPROVE_VALIDATION_FAILED\` with the full \`issues\` list (and any \`warnings\`) in \`details\`. A non-\`review\` run returns \`SALARY_RUN_APPROVE_NOT_REVIEW\`.
|
||
|
||
## 4. Mark paid (approved → paid)
|
||
|
||
After the bank transfer settles (or you mark it on the same day for cash-method shops), tell Accounted:
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs/$SR_ID/mark-paid" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
This step records the payment event but does NOT post the journal entry yet: that's step 5. The split is deliberate: the \`mark-paid\` step gives integrators a hook to confirm the bank-side leg landed before locking the GL side. It is a **bodyless POST**: \`paid_at\` is stamped server-side to the current UTC timestamp (the API does not accept a body-supplied date, to keep the BFL audit trail clean).
|
||
|
||
## 5. Book (paid → booked)
|
||
|
||
\`POST /salary-runs/{id}/book\` is the engine-touching step. It posts **2-4 verifikationer** atomically — always the salary and avgifter entries, plus a semesterlöneskuld-accrual entry and/or a löneväxling-pension entry when those apply:
|
||
|
||
- Verifikation 1: Bruttolön: debit → 7210 / 7220 / 7240 (Löner tjänstemän / företagsledare / styrelsearvoden, by \`employment_type\`), credit → 2710 (Personalskatt, avdragen skatt) + 1930 (utbetalning)
|
||
- Verifikation 2: Arbetsgivaravgifter: debit → 7510 (Lagstadgade sociala avgifter), credit → 2731 (Avräkning sociala avgifter: payable to Skatteverket, cleared when arbetsgivardeklarationen is paid)
|
||
- Verifikation 3 (if semesterlöneskuld): debit → 7290 (Förändring semesterlöneskuld) + 7519 (sociala avgifter på semester), credit → 2920 (Upplupna semesterlöner) + 2940 (Upplupna sociala avgifter)
|
||
- Verifikation 4 (if löneväxling): debit → 7410 (Pensionsförsäkringspremier) + 7533 (Särskild löneskatt på pensionskostnader), credit → 2740 (Skuld pensionsförsäkringar) + 2514 (Beräknad särskild löneskatt); pension = löneväxling × 1.058
|
||
|
||
The 2731 series is the **employer-contributions-payable** liability per BAS 2026: not to be confused with 2615 (utgående moms vid import, unrelated to payroll). The arbetsgivardeklaration cycle posts the payable on book day and clears it via 1930 when the bank transfer to Skatteverket settles.
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs/$SR_ID/book" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
Response:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"id": "run_...",
|
||
"status": "booked",
|
||
"booked_at": "2026-05-26T09:15:00Z",
|
||
"booked_by": "user_...",
|
||
"salary_entry_id": "je_salary...",
|
||
"avgifter_entry_id": "je_avg...",
|
||
"vacation_entry_id": "je_vac...",
|
||
"pension_entry_id": null,
|
||
"entry_ids": ["je_salary...", "je_avg...", "je_vac..."]
|
||
},
|
||
"meta": {
|
||
"request_id": "req_...",
|
||
"api_version": "2026-05-12",
|
||
"audit": {
|
||
"voucher_number": "L2026-0023",
|
||
"voucher_url": "/api/v1/companies/.../journal-entries/je_salary...",
|
||
"immutable_at": "2026-05-26T09:15:00Z"
|
||
}
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
If \`book\` fails partway (e.g. period locked while waiting for the bank-side confirmation), the route is strict-mode v1: no partial commits. The state stays at \`paid\` and the response carries the \`PERIOD_LOCKED\` error code with the offending period.
|
||
|
||
## 6. Generate the AGI XML
|
||
|
||
\`POST /salary-runs/{id}/generate-agi\` produces the arbetsgivardeklaration på individnivå XML for the period. Skatteverket requires AGI monthly; the XML is embedded in the JSON response: no separate file endpoint.
|
||
|
||
\`\`\`bash
|
||
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/salary-runs/$SR_ID/generate-agi" \\
|
||
-H "Authorization: Bearer gnubok_sk_test_..." \\
|
||
-H "Idempotency-Key: $(uuidgen)"
|
||
\`\`\`
|
||
|
||
Response:
|
||
|
||
\`\`\`json
|
||
{
|
||
"data": {
|
||
"agi_declaration_id": "agi_...",
|
||
"period_year": 2026,
|
||
"period_month": 5,
|
||
"employee_count": 2,
|
||
"is_correction": false,
|
||
"totals": {
|
||
"totalTax": 24300.00,
|
||
"totalAvgifterBasis": 80000.00,
|
||
"totalAvgifterAmount": 25136.00,
|
||
"totalSjuklonekostnad": 0,
|
||
"avgifterByCategory": { "standard": { "basis": 80000.00, "amount": 25136.00 } }
|
||
},
|
||
"xml": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><Skatteverket omrade=\\"Arbetsgivardeklaration\\">…</Skatteverket>",
|
||
"xml_filename": "AGI_5566778899_202605.xml"
|
||
}
|
||
}
|
||
\`\`\`
|
||
|
||
Save the XML to disk and upload it to **Skatteverket Mina Sidor → Tjänster → Arbetsgivardeklaration**. Mina Sidor accepts the file directly; no manual transcription needed. (Direct API submission requires BankID and goes through the \`skatteverket\` extension, not the public REST API.)
|
||
|
||
Generating the XML stamps \`agi_generated_at\` on the salary run and emits an \`agi.generated\` event. There is no public endpoint to store a Skatteverket confirmation number back on the run: track the submission reference in your own system.
|
||
|
||
## State machine summary
|
||
|
||
\`\`\`
|
||
draft ──calculate──► review ──approve──► approved ──mark-paid──► paid ──book──► booked ──generate-agi──► (AGI XML)
|
||
\`\`\`
|
||
|
||
Each transition is idempotent on \`Idempotency-Key\`. Retrying a transition that has already completed returns the same response with \`Idempotent-Replayed: true\`. Failed transitions don't advance the state: fix and retry.
|
||
|
||
## Förmånsbeskattning
|
||
|
||
When an employee has bilförmån / fri kost / friskvård, the förmånsvärde is configured on the employee's benefit records: it is **not** passed on the run-creation request (\`POST /salary-runs\` takes only period + payment metadata). When you \`:calculate\`, the engine adds the förmånsvärde to bruttolön for the avgifts-basis (2731) and produces a separate förmåner line on the AGI. \`bilförmån_värde\` follows Skatteverkets schablon for 2026; supply the figure directly: the API does not compute it from car make / model / year.
|
||
|
||
## Common pitfalls
|
||
|
||
- **\`PATCH\`/\`DELETE\` are draft-only.** Both require \`draft\` status (\`SALARY_RUN_PATCH_NOT_DRAFT\` / \`SALARY_RUN_DELETE_NOT_DRAFT\`). There is no revert-to-draft and no \`:unapprove\`; a booked run is corrected via the forthcoming \`:correct\` verb (storno-then-rebook), not by editing or deleting.
|
||
- **AGI period vs run period.** The AGI declaration covers \`(period_year, period_month)\`: the same period as the run, not the payment date. A run paid on 2026-06-02 for May still files as the May AGI.
|
||
- **F-skatt verification is the integrator's job.** The API trusts \`employee.f_skatt_status\` (\`a_skatt\` | \`f_skatt\` | \`fa_skatt\` | \`not_verified\`) to be in sync with the employee's live Skatteverket registration. A wrong status produces a non-compliant AGI; check the F-skattsedel before payroll runs. \`not_verified\` employees are surfaced as a non-blocking warning on \`:calculate\` (30% skatteavdrag and full avgifter are applied until verified).
|
||
- **Sociala avgifter age reduction.** Per Prop. 2025/26:66, employees who are **18-22 years old at the start of the 2026 income year (born 2003-2007)** AND employees who **have turned 67 at the start of the income year (1 January 2026)** get reduced satser. The "at the start of" boundary matters: a 66-year-old whose 67th birthday falls in February 2026 does NOT qualify for the elder reduction in 2026. The engine derives age from the employee's personnummer (the leading birthdate digits: there is no separate \`birthdate\` field) and applies the correct sats automatically: don't override unless you've consulted [Skatteverkets table](https://www.skatteverket.se/foretagochorganisationer/skatter/arbetsgivareochinkomstuppgifter/arbetsgivaravgifteroch_skatteavdrag.4.18e1b10334ebe8bc80003392.html). The old "under 26" rule from 2024 does NOT apply for 2026 and later.
|
||
- **Bruttolöneavdrag vs nettolöneavdrag order.** Bruttolöneavdrag reduces both lön och avgifter; nettolöneavdrag only affects the employee's payout. Pass either explicitly in the run; don't mix them.
|
||
|
||
## Next steps
|
||
|
||
- **[Set up webhooks](/docs/api/cookbook/webhooks)**: subscribe to \`salary_run.booked\` and \`agi.generated\` events to drive downstream payroll integrations.
|
||
- **[Year-end closing](/docs/api/cookbook/year-end-closing)**: payroll's annual cap is the kontrolluppgift season (january of the following year).
|
||
- **[Salary-runs reference](/docs/api/reference/salary-runs)**: every parameter, every error code.
|
||
`
|