Files
accounted/extensions/general/mcp-server/skills/payroll-monthly.ts
T
MattssonandClaude Fable 5 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>
2026-08-28 18:14:31 +02:00

128 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'] },
}