fix(sie-import): reject overlap-but-not-contain fiscal periods (#421)

* fix(sie-import): reject overlap-but-not-contain fiscal periods

ensureFiscalPeriod previously fell back silently to any partially-overlapping
fiscal_period when no period fully contained the SIE file's #RAR range. That
stamped every imported voucher with a fiscal_period_id whose date window
didn't cover the voucher's own entry_date — breaking the SIE invariant that
#VER dates fall inside #RAR and BFL 5 kap. (verifikationsnummer i obruten
serie per räkenskapsår). Reproduced in production: a customer with a broken
fiscal year (Mar–Feb) had their previous-year SIE collapse into a calendar
2026 period, mixing 116 prior-year vouchers into the current period's
voucher sequence.

Now: fully-contained → reuse; partial overlap → reject with a clear Swedish
message pointing at the period dates that would need to be fixed; no overlap
→ create a new period as before.

Adds a defense-in-depth pre-check in executeImport that rejects with a
Swedish error if any individual #VER date falls outside the resolved fiscal
period — catches multi-year SIE files (which gnubok does not yet support)
without producing corrupted journal entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sie-import): fail closed on period-fetch error in voucher-date guard

Greptile correctly flagged that the voucher-date guard wrapped the period
fetch in `if (resolvedPeriod)` without checking the .single() error. A
transient network error or RLS failure returned null for data, the guard
body skipped, and importVouchers ran with no date validation — the same
data-corruption path the guard exists to close.

Now: surface the fetch error to the user (Swedish) and abort the import.

Also switches the date comparison from millisecond timestamps to YYYY-MM-DD
string compare. SIE per spec is date-only and our parser normalizes to
midnight, but string compare matches the underlying DATE columns exactly and
removes a latent off-by-one risk on the period's last day if a future
parser change ever attached a time component to v.date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-05-08 17:17:36 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 81e9dd224e
commit 11e07b34c3
2 changed files with 86 additions and 4 deletions
+31
View File
@@ -421,6 +421,37 @@ describe('ensureFiscalPeriod validation', () => {
expect(id).toBe('existing-period-id')
})
it('rejects when an existing period overlaps the range but does not fully contain it', async () => {
// Regression: previously fell through to the overlapping period silently,
// which stamped every imported voucher with a fiscal_period_id whose
// window did not cover the voucher's own entry_date — breaking the SIE
// invariant and BFL 5 kap. (verifikationsnummer per räkenskapsår).
const { supabase, enqueueMany } = createQueuedMockSupabase()
enqueueMany([
{ data: null, error: null }, // containing check — no match
{
data: [
{
id: 'calendar-2026',
period_start: '2026-01-01',
period_end: '2026-12-31',
name: 'Räkenskapsår 2026',
},
],
error: null,
},
])
await expect(
ensureFiscalPeriod(
supabase as unknown as Supabase,
'company-id',
'2025-03-01', // Capelix-style broken FY March–Feb
'2026-02-28',
),
).rejects.toThrow(/överlappar men matchar inte/)
})
})
describe('linkOpeningBalanceEntryToPeriod', () => {