fix(payroll): declare AGI for the payout month, not the run's period month (#2191) (#2228)

Arbetsgivardeklarationen is filed for the calendar month the pay went
out (kontantprincipen), so a run for August paid on 25 September belongs
to redovisningsperiod 202609. The generator, the submit route, the run
page and the run header all took run.period_year/period_month instead,
and three PATCH paths refused any payment date outside that month, which
made lön i efterskott impossible to set up at all.

- lib/salary/agi/reporting-period.ts: one dependency-free helper
  (agiReportingPeriod) derives the period from payment_date, falling
  back to the run period only when the date is missing.
- generate-declaration.ts: XML Redovisningsperiod, the agi_declarations
  lookup/insert and the sanity warnings key on the payout month. New
  AGI_PERIOD_CONFLICT (409) refuses to overwrite another live run's
  declaration for the same payout month; corrections still replace.
- submit route, run page (AGI panel, submission hook, tax-payment fetch,
  XML filename) and RunHeader use the helper; the header says "AGI
  redovisas för 2026-09 (utbetalningsmånaden)" whenever the two differ.
- The in-period payment-date guard is lifted in the dashboard PATCH,
  lib/salary/update-run.ts (MCP staged tool + pending-ops executor) and
  the v1 PATCH, plus the RunHeader min/max; its only stated reason was
  the period-keyed AGI. Generated API skill reference updated.

Existing agi_declarations rows keep their stored period: a declaration
already filed under the earned month is a correction with Skatteverket,
not a re-key. Rule verified against Skatteverket's guidance on
redovisningsperiod (kontantprincipen).

Closes #2191


Claude-Session: https://claude.ai/code/session_01QPQLwHNEiQfiCNLSMzXMiQ

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 17:19:19 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 601e521584
commit cb39cded81
21 changed files with 387 additions and 134 deletions
@@ -285,16 +285,22 @@ describe('gnubok_update_salary_run', () => {
expect(result.preview.new_payment_date).toBe('2026-04-07')
})
it('rejects a payment_date outside the run period month (kontantprincipen)', async () => {
it('stages a payment_date in the month after the period (lön i efterskott, #2191)', async () => {
// The AGI redovisningsperiod follows payment_date, so a March run paid
// in April is declared for April; no period guard refuses it.
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({ data: RUN_ROW }) // draft gate passes; period is 2026-03
enqueue({ data: null }) // resolvePeriodStatusForDate: company_settings
enqueue({ data: null }) // resolvePeriodStatusForDate: fiscal_periods
enqueue({ data: { id: 'op-1' }, error: null }) // pending_operations insert
await expect(
updateSalaryRun.execute(
{ salary_run_id: 'run-1', payment_date: '2026-04-05' },
'company-1', 'user-1', supabase as never, { type: 'user' },
),
).rejects.toThrow(/SALARY_RUN_PAYMENT_DATE_OUTSIDE_PERIOD/)
const result = (await updateSalaryRun.execute(
{ salary_run_id: 'run-1', payment_date: '2026-04-05' },
'company-1', 'user-1', supabase as never, { type: 'user' },
)) as { staged: boolean; preview: Record<string, unknown> }
expect(result.staged).toBe(true)
expect(result.preview.new_payment_date).toBe('2026-04-05')
})
it('throws SALARY_RUN_NOT_FOUND for a foreign or unknown run', async () => {
+1 -1
View File
@@ -15438,7 +15438,7 @@ export const tools: McpTool[] = [
additionalProperties: false,
properties: {
salary_run_id: { type: 'string', description: 'UUID of the salary run (must be draft)' },
payment_date: { type: 'string', description: 'New payment date (YYYY-MM-DD); the date the salary verifikat will be booked on. Must stay within the run\'s period month (AGI is declared per payment month); supplying it clears the run\'s calculation.' },
payment_date: { type: 'string', description: 'New payment date (YYYY-MM-DD); the date the salary verifikat will be booked on. May fall outside the run\'s period month (lön i efterskott): the AGI is declared for the payment month. Supplying it clears the run\'s calculation.' },
voucher_series: { type: 'string', description: 'Voucher series letter (single A-Z)' },
notes: { type: ['string', 'null'], description: 'Free-text note on the run (max 2000 chars); null clears it' },
},