* 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>
128 lines
7.8 KiB
TypeScript
128 lines
7.8 KiB
TypeScript
import type { Skill } from './types'
|
||
|
||
const body = `# Monthly Payroll: Accounted
|
||
|
||
Salary run + AGI filing for one calendar month.
|
||
|
||
## When to use
|
||
|
||
- "Run payroll for [month]"
|
||
- "Lönekörning [månad]"
|
||
- "Generate AGI"
|
||
- Once per month, **before payment_date**
|
||
|
||
## Statutory deadlines
|
||
|
||
- **AGI (arbetsgivardeklaration):** 12th of the **next month** (17th in January and August). E.g. payroll for March → file AGI by 12 April.
|
||
- **Skatt + sociala avgifter payment:** same deadline as AGI.
|
||
- Skattekontot must be in funds by deadline (SFL 62 kap. 3 §).
|
||
|
||
## Workflow
|
||
|
||
### Step 1: Verify employees are set up
|
||
|
||
\`gnubok_list_employees\` returns all active employees. For each, the system needs:
|
||
|
||
- **Personnummer** (last 4 stored, full encrypted)
|
||
- **monthly_salary** (or hourly_rate + estimated hours)
|
||
- **employment_degree** (1-100 %)
|
||
- **tax_table_number** + **tax_column** (skattetabell + kolumn from Skatteverket)
|
||
- **employment_type** (\`tjänsteman\`, \`arbetare\`, etc.): drives BAS account choice (7210 vs 7010)
|
||
|
||
If anything is missing, the user fixes it in the web UI before running payroll.
|
||
|
||
### Step 2: Create the salary run
|
||
|
||
\`gnubok_create_salary_run({ period_year, period_month, payment_date })\`
|
||
|
||
- Creates a \`salary_runs\` row with status \`draft\`
|
||
- Adds **all active employees** with their base salary line (item_type \`monthly_salary\` or \`hourly_salary\`)
|
||
- Returns the run ID + employee count
|
||
- Idempotent on \`(company_id, period_year, period_month)\`: re-calling errors with "Salary run already exists for this period"
|
||
|
||
### Step 3: Set this month's salary (if it differs from the fixed pay)
|
||
|
||
\`gnubok_set_run_salary({ salary_run_id, employee_id, monthly_salary })\`: sets THIS run's base salary for one employee (draft only; 0 = nollkörning). This is the per-run value the engine reads at calculation: the employee's fixed salary is untouched. Common for owners taking salary by need and capacity. Do NOT edit the \`monthly_salary\` payslip line instead: recalculation rebuilds base salary lines from the per-run value.
|
||
|
||
### Step 3b: Add OB-tillägg, traktamente, förmåner (if any)
|
||
|
||
Variable lines (overtime, weekend supplement, milage, traktamente, förmåner) are added in the web UI per-employee. There's no MCP tool yet for these: guide the user there.
|
||
|
||
### Step 4: Calculate
|
||
|
||
\`gnubok_calculate_salary_run({ salary_run_id })\`. Computes per employee:
|
||
|
||
- **Bruttolön** (gross): sum of taxable salary lines
|
||
- **Skatteavdrag**: tax-table lookup (skattetabell + kolumn → table column for the gross level)
|
||
- **Nettolön** (net): bruttolön − skatteavdrag
|
||
- **Sociala avgifter (arbetsgivaravgifter)**: 31.42 % of bruttolön (standard 2025). Reduced rates apply to specific age groups: always check the current statutory rates before relying on these:
|
||
- **Born 1937 or earlier**: **0 %**: no avgifter at all (oldest cohort, never paid into the modern pension system). Easy to miss; the BAS journal entries for 7510/2731 simply don't apply for these employees.
|
||
- **Age 66+ on 1 January of the income year (67+ from income year 2026)**: 10.21 % (only ålderspensionsavgift). The threshold rises with the riktålder; verify the cohort year for the current run rather than hard-coding a birth year.
|
||
- **växa-stöd / temporary youth reduction**: ages 19-23, salary ≤ 25 000 SEK/month, capped duration. The exact rate and window vary year-over-year (e.g. 20.81 % during 1 Apr 2026: 30 Sep 2027 per Prop. 2025/26:34): confirm against Skatteverket's current published table before applying.
|
||
- **Semesterlöneskuld** (vacation accrual): 12 % of bruttolön (default). Booked monthly to 2920.
|
||
- **Förmåner** (benefits): employer-paid taxable amounts (bilförmån, kostförmån, etc.): added to skattegrundande lön but not to nettolön payment.
|
||
|
||
Errors at this stage usually mean missing tax-table data: fall back to \`getDefaultTaxColumn(personnummer, year)\` heuristics or prompt user.
|
||
|
||
### Step 5: Review
|
||
|
||
\`gnubok_get_salary_run({ salary_run_id })\`: full breakdown including \`calculation_breakdown\` showing step-by-step formulas. The user reviews per-employee in web UI.
|
||
|
||
\`gnubok_get_salary_journal({ year })\`: annual rollup for sanity check.
|
||
|
||
### Step 6: Approve & book
|
||
|
||
\`gnubok_book_salary_run({ salary_run_id })\` stages the booking (high-risk: commit needs \`confirmed=true\` on approval). On commit it walks the run \`review\` → \`approved\` → \`paid\` → \`booked\`; the staged approval is the authorization act. Alternatively the user clicks through the same statuses in the web UI. Booking creates the JE:
|
||
|
||
- Debit **7210** (lön tjänstemän) or **7010** (lön arbetare): bruttolön
|
||
- Debit **7510** (sociala avgifter): \`avgift_base × applicable_rate\`: **per employee**, using the rate from Step 4 (default 31.42 %, or a reduced rate when applicable: 10.21 % for 66+, växa-stöd, etc.)
|
||
- Credit **2710** (källskatt): skatteavdrag
|
||
- Credit **2731** (avräkning lagstadgade sociala avgifter): same amount as the 7510 debit (the avgift cost is the same number as the avgift liability). 2731 is what the engine books and what the skattekonto AGI draw debits; crediting the group account 2730 instead splits the liability across two accounts (issue #1870)
|
||
- Credit **2920** (semesterlöneskuld): 12 % × bruttolön (debit 7290 to balance)
|
||
- Credit **1930** (bank): nettolön (when paid)
|
||
|
||
When a run mixes full-rate and reduced-rate employees, the 7510/2731 lines are summed across all employees: the *total* avgift line equals \`Σ(per-employee avgift_base × per-employee rate)\`, **not** \`Σ bruttolön × 31.42 %\`. \`gnubok_calculate_salary_run\` already does this aggregation.
|
||
|
||
### Step 7: Generate AGI
|
||
|
||
\`gnubok_generate_agi({ salary_run_id })\`. Run must be in \`review\`/\`approved\`/\`paid\`/\`booked\` status (past draft).
|
||
|
||
Returns \`{ message, period, employee_count, download_url }\`. The XML conforms to Skatteverket's AGI format and is stored 7 years per BFL. Download from \`/api/salary/runs/{id}/agi/xml\` and upload to Skatteverket e-tjänst.
|
||
|
||
## Critical rules
|
||
|
||
- **Skatteavdrag is mandatory.** Never pay gross. Skatteverket charges 100% penalty for missing avdrag.
|
||
- **Sociala avgifter are 31.42 % even if salary is in EUR.** Convert to SEK at payment date for the avgift base.
|
||
- **Semesterlöneskuld** must be reserved monthly, not at year-end. 2920 grows by 12 % of every month's bruttolön.
|
||
- **Förmånsbeskattning** (benefit tax) is required even if not in cash. Bilförmån, kostförmån, sjukvårdsförsäkring all count.
|
||
- **Karensavdrag** (sick day deduction): first day of sickness is generally without pay; 80 % from day 2. Specific rules: fall through to \`swedish-payroll\` reference if unsure.
|
||
|
||
## Common errors
|
||
|
||
- **Run already exists**: idempotency on (company, year, month). Find the existing run with \`gnubok_get_salary_run\`.
|
||
- **Tax table column wrong**: defaults to column 1 if not set, which is too high for most employees. Fix on employee record.
|
||
- **AGI before booking**: works (status check is past-draft, not booked), but you should book first so the JE matches what AGI reports.
|
||
|
||
## Tools
|
||
|
||
- \`gnubok_list_employees\`: verify setup
|
||
- \`gnubok_create_salary_run\`: stage new monthly run
|
||
- \`gnubok_calculate_salary_run\`: compute tax + avgifter + accrual
|
||
- \`gnubok_get_salary_run\`: review breakdown
|
||
- \`gnubok_book_salary_run\`: stage booking (statuses + verifikat)
|
||
- \`gnubok_get_salary_journal\`: annual rollup
|
||
- \`gnubok_generate_agi\`: produce AGI XML for filing
|
||
`
|
||
|
||
export const payrollMonthlySkill: Skill = {
|
||
slug: 'payroll-monthly',
|
||
name: 'Monthly Payroll',
|
||
summary: 'Monthly salary run + AGI: employee setup, calculation, sociala avgifter, semesterlöneskuld, booking, AGI XML.',
|
||
tags: ['monthly', 'payroll', 'agi', 'compliance'],
|
||
body,
|
||
tier: 'workflow',
|
||
// Only relevant when the company actually has employees. EF without payroll
|
||
// (most sole traders) shouldn't see this in the discovery list.
|
||
applicability: { entity_type: 'both', requires: ['employees'] },
|
||
}
|