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>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
/**
|
|
* The AGI redovisningsperiod of a salary run.
|
|
*
|
|
* Arbetsgivardeklarationen is filed for the calendar month in which the pay
|
|
* was PAID OUT (kontantprincipen, SFL 26 kap.), not the month the work was
|
|
* done. A run for August paid on 25 September is declared in September.
|
|
*
|
|
* salary_runs.period_year/period_month is the earned month (what the payslip
|
|
* says and what the run list groups by); payment_date is the day the money
|
|
* left the account and is therefore what decides the AGI period. The two
|
|
* coincide for lön i förskott (paid inside the earned month) and differ by a
|
|
* month for lön i efterskott (hourly pay settled the month after).
|
|
*
|
|
* Dependency-free on purpose: it is read by the generator, the submit route,
|
|
* the run page and the run header, so it must be safe in client bundles.
|
|
*/
|
|
|
|
export interface AgiReportingPeriod {
|
|
periodYear: number
|
|
periodMonth: number
|
|
}
|
|
|
|
type RunPeriodSource = {
|
|
payment_date?: string | null
|
|
period_year: number
|
|
period_month: number
|
|
}
|
|
|
|
const ISO_YEAR_MONTH_RE = /^(\d{4})-(\d{2})/
|
|
|
|
/**
|
|
* Payout month of the run, falling back to the earned month only when
|
|
* payment_date is missing or unparseable (legacy or half-created rows).
|
|
*/
|
|
export function agiReportingPeriod(run: RunPeriodSource): AgiReportingPeriod {
|
|
const match = typeof run.payment_date === 'string' ? ISO_YEAR_MONTH_RE.exec(run.payment_date) : null
|
|
if (match) {
|
|
const year = Number(match[1])
|
|
const month = Number(match[2])
|
|
if (Number.isInteger(year) && month >= 1 && month <= 12) {
|
|
return { periodYear: year, periodMonth: month }
|
|
}
|
|
}
|
|
return { periodYear: run.period_year, periodMonth: run.period_month }
|
|
}
|
|
|
|
/** True when the AGI period is not the earned month (lön i efterskott or förskott). */
|
|
export function agiPeriodDiffersFromRunPeriod(run: RunPeriodSource): boolean {
|
|
const period = agiReportingPeriod(run)
|
|
return period.periodYear !== run.period_year || period.periodMonth !== run.period_month
|
|
}
|
|
|
|
/** "202609": the compact form Skatteverket's AGI endpoints and settings keys use. */
|
|
export function formatAgiPeriodCompact(period: AgiReportingPeriod): string {
|
|
return `${period.periodYear}${String(period.periodMonth).padStart(2, '0')}`
|
|
}
|
|
|
|
/** "2026-09": the dashed form the tax-payment routes and user-facing copy use. */
|
|
export function formatAgiPeriodDashed(period: AgiReportingPeriod): string {
|
|
return `${period.periodYear}-${String(period.periodMonth).padStart(2, '0')}`
|
|
}
|