fix: allow retroactive first fiscal year via SIE import (#265)

The enforce_period_start_day trigger and its sie-import pre-validation
rejected any non-first-of-month period_start whenever *any other* fiscal
period existed for the company. That blocked a real user flow: after
onboarding creates a default period (e.g. current year, day 1), importing
an SIE for an older förlängt första räkenskapsår (e.g. 2017-07-28 –
2018-12-31) failed with "Non-first fiscal period must start on the 1st
of a month".

Per BFL 3 kap., the chronologically first fiscal year is the one that
may be 6–18 months and start mid-month — which is a property of *when*
the period starts relative to others, not of insert order. The trigger
and pre-validation now allow mid-month start iff no existing period
starts earlier.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-04-18 10:26:02 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent c8a5f044c3
commit 8ed943198f
3 changed files with 85 additions and 18 deletions
+28 -5
View File
@@ -323,12 +323,12 @@ describe('ensureFiscalPeriod validation', () => {
// actionable Swedish error instead of a raw Postgres message.
type Supabase = Parameters<typeof ensureFiscalPeriod>[0]
it('rejects mid-month start when another period exists for the company', async () => {
it('rejects mid-month start when an earlier period already exists', async () => {
const { supabase, enqueueMany } = createQueuedMockSupabase()
enqueueMany([
{ data: null, error: null }, // containing check — no match
{ data: [], error: null }, // overlapping check — none
{ data: null, error: null, count: 1 }, // count existing — 1 period already
{ data: [{ id: 'earlier' }], error: null }, // earlier period exists
])
await expect(
@@ -338,7 +338,7 @@ describe('ensureFiscalPeriod validation', () => {
'2026-04-16',
'2026-12-31',
),
).rejects.toThrow(/endast företagets första räkenskapsår får börja mitt i månaden/)
).rejects.toThrow(/kronologiskt första räkenskapsår får börja mitt i månaden/)
})
it('rejects end date that is not the last day of the month', async () => {
@@ -346,7 +346,7 @@ describe('ensureFiscalPeriod validation', () => {
enqueueMany([
{ data: null, error: null },
{ data: [], error: null },
{ data: null, error: null, count: 0 }, // first fiscal period
{ data: [], error: null }, // no earlier period
])
await expect(
@@ -364,7 +364,7 @@ describe('ensureFiscalPeriod validation', () => {
enqueueMany([
{ data: null, error: null },
{ data: [], error: null },
{ data: null, error: null, count: 0 }, // no existing periods
{ data: [], error: null }, // no earlier period
{ data: { id: 'new-period-id' }, error: null }, // insert result
])
@@ -378,6 +378,29 @@ describe('ensureFiscalPeriod validation', () => {
expect(id).toBe('new-period-id')
})
it('allows mid-month start when importing a retroactive earliest period', async () => {
// Scenario: onboarding created a 2026 fiscal period, user now imports
// an SIE for their förlängt första räkenskapsår 2017-07-28 – 2018-12-31.
// The 2017 period is chronologically earliest, so mid-month start is
// legal under BFL 3 kap.
const { supabase, enqueueMany } = createQueuedMockSupabase()
enqueueMany([
{ data: null, error: null }, // containing check — no match
{ data: [], error: null }, // overlapping check — none (2017 vs 2026)
{ data: [], error: null }, // no earlier period than 2017-07-28
{ data: { id: 'retro-first-year-id' }, error: null }, // insert
])
const id = await ensureFiscalPeriod(
supabase as unknown as Supabase,
'company-id',
'2017-07-28',
'2018-12-31',
)
expect(id).toBe('retro-first-year-id')
})
it('reuses an existing period that contains the range (no validation needed)', async () => {
const { supabase, enqueueMany } = createQueuedMockSupabase()
enqueueMany([