fix(vat): momsdeklaration defaults respect the configured cadence and persist manual changes (#1998)

* fix(vat): momsdeklaration defaults respect the configured cadence and persist manual changes

The period picker re-seeded from scratch on every visit: an arsmoms user
whose moms_period was never set landed on a silently guessed quarterly
declaration (companies without a company_settings row bypassed every
gate), and a manually chosen cadence evaporated on the next visit.

- Gate the view when no company_settings row exists, matching the
  existing "registered but no period" gate: a declaration for the wrong
  period type is a compliance hazard, not a convenience.
- Persist the manually chosen cadence per company (localStorage,
  FyPicker pattern) and restore it while moms_period is unchanged; the
  concrete period still re-seeds to the most recently ended one, and a
  changed setting discards the stored cadence.
- Extract the seeding decision into lib/vat/period-selection.ts with
  unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pQn9kC742B9R7Ggi8wdn9

* fix(vat): drop cadence persistence; the moms_period re-seed is the control

Skeptic review refuted the persistence half of the previous commit twice:
the render-phase localStorage restore diverged from SSR (hydration error
on every visit once a cadence was stored), and restoring a manually
chosen cadence that deviates from moms_period kept the filing pipeline
open on the wrong period type across visits, with no downstream path
validating period type against the setting.

The redovisningsperiod has exactly one lawful value per company, so the
mount-time re-seed from company_settings.moms_period is the self-healing
control, not a bug. The settings-row gate and the extracted, tested
seeding resolver stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pQn9kC742B9R7Ggi8wdn9

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-27 19:09:47 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 4f6ecad549
commit fca57dc470
4 changed files with 147 additions and 17 deletions
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest'
import { resolveInitialVatPeriodSelection } from '../period-selection'
describe('resolveInitialVatPeriodSelection', () => {
const today = new Date(2026, 7, 27) // 2026-08-27
it('seeds yearly cadence from a yearly setting', () => {
expect(
resolveInitialVatPeriodSelection({ momsPeriod: 'yearly', over40m: false, today }),
).toEqual({ periodType: 'yearly', year: 2026, period: 1 })
})
it('seeds the most recently ended quarter for a quarterly setting', () => {
expect(
resolveInitialVatPeriodSelection({ momsPeriod: 'quarterly', over40m: false, today }),
).toEqual({ periodType: 'quarterly', year: 2026, period: 2 })
})
it('rolls a quarterly seed back across the year boundary in Q1', () => {
expect(
resolveInitialVatPeriodSelection({
momsPeriod: 'quarterly',
over40m: false,
today: new Date(2026, 1, 10),
}),
).toEqual({ periodType: 'quarterly', year: 2025, period: 4 })
})
it('seeds monthly filers from the deadline-aware default', () => {
// 2026-08-06: June's declaration is due 17 Aug, so June is the open one.
expect(
resolveInitialVatPeriodSelection({
momsPeriod: 'monthly',
over40m: false,
today: new Date(2026, 7, 6),
}),
).toEqual({ periodType: 'monthly', year: 2026, period: 6 })
})
it('honors the over-40M monthly rule (always the most recently ended month)', () => {
expect(
resolveInitialVatPeriodSelection({
momsPeriod: 'monthly',
over40m: true,
today: new Date(2026, 7, 6),
}),
).toEqual({ periodType: 'monthly', year: 2026, period: 7 })
})
it('falls back to quarterly when moms_period is unset (state is gated, never rendered)', () => {
expect(
resolveInitialVatPeriodSelection({ momsPeriod: null, over40m: false, today }),
).toEqual({ periodType: 'quarterly', year: 2026, period: 2 })
})
})
+51
View File
@@ -0,0 +1,51 @@
/**
* Initial period selection for the VAT declaration view.
*
* The view seeds its cadence (month/quarter/räkenskapsår) from the company's
* configured redovisningsperiod (company_settings.moms_period) and its
* concrete period from the most recently ended one (period-defaults.ts), the
* only one that can actually be filed.
*
* The cadence is deliberately NOT persisted across visits: a company's
* redovisningsperiod is fixed by its Skatteverket registration (SFL 26 kap),
* so the setting has exactly one lawful value and the mount-time re-seed is
* the control that self-heals a temporary in-session detour (e.g. a
* helårsmoms user peeking at quarterly figures). Restoring such a detour
* would keep the filing pipeline open on the wrong period type. The yearly
* cadence's räkenskapsår pick is persisted by FyPicker, which is fine: every
* fiscal year is a legitimate target.
*/
import { mostRecentEndedVatPeriod } from './period-defaults'
import type { MomsPeriod, VatPeriodType } from '@/types'
export interface VatPeriodSelection {
periodType: VatPeriodType
year: number
/** 1-12 for monthly, 1-4 for quarterly, always 1 for yearly. */
period: number
}
/**
* Decide the view's initial period from the company's moms_period setting:
* the setting's cadence, seeded to the most recently ended period in it.
*/
export function resolveInitialVatPeriodSelection(opts: {
momsPeriod: MomsPeriod | null
over40m: boolean
today?: Date
}): VatPeriodSelection {
const { momsPeriod, over40m } = opts
const today = opts.today ?? new Date()
// The 'quarterly' fallback only shapes state that is never shown: the view
// gates on a missing moms_period (and on a missing settings row) before
// rendering a declaration.
const cadence = momsPeriod ?? 'quarterly'
if (cadence === 'yearly') {
return { periodType: 'yearly', year: today.getFullYear(), period: 1 }
}
const ended = mostRecentEndedVatPeriod(cadence, today, { over40m })
return { periodType: cadence, year: ended.year, period: ended.period }
}