fix(import): let provider re-sync re-import an earlier fiscal year after data deletion (#1763)

* fix(import): let provider re-sync re-import an earlier fiscal year after data deletion

After partially deleting imported data, a provider re-sync could not bring
back the previous fiscal year: the sie_imports 'completed' watermark
survives data deletion, the replace path aborted the whole year when the
prior import row could not be resolved, and prior-import detection picked
an arbitrary row when several overlapped the same year.

- findOverlappingPeriodImports returns ALL overlapping completed rows,
  newest first; checkDuplicatePeriodImport now picks deterministically.
- executeSIEImport replace mode resolves every overlapping row. A row that
  is gone or no longer 'completed' (replaceSIEImport codes not_found /
  not_completed) is a stale watermark: skip it with a warning and import
  the year fresh instead of stranding the user. Locked/closed periods and
  RPC failures still abort the year.
- The arcim-migration wizard names the fiscal year in every per-file
  import failure and shows the newest prior import in the options step.

Fixes #1667

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(import): fail closed when the replace pre-check query errors

replaceSIEImport's pre-check discarded the .single() error, so a
transient query failure (statement timeout, network error, 5xx via
PostgREST) was indistinguishable from a genuinely absent row and got
classified not_found. The replace loop in executeSIEImport then treated
it as a stale watermark and imported the fiscal year fresh while the
prior completed import's verifikationer were still in the ledger, with
duplicate checks skipped in replace mode: silent duplicate
verifikationer for a whole year (BFL 4:1 risk).

Only PGRST116 (zero rows from .single()) now classifies as not_found;
any other pre-check error returns rpc_error, which aborts the year in
the replace loop. Tests cover both classifications plus the
executeSIEImport-level abort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(import): fail closed on overlap lookup, verify zero surviving entries before a stale-watermark skip

Review findings on #1763 (CodeRabbit + Swedish compliance review):
- findOverlappingPeriodImports now uses fetchAllRows: query errors throw
  instead of returning [] (which let replace mode import fresh over rows it
  never resolved), pagination passes the PostgREST row cap, id tiebreak
  keeps the order total.
- A stale-watermark skip (not_found/not_completed) is only trusted after a
  positive check that zero posted import entries survive in the fiscal
  year: replace_sie_import deletes by fiscal period, so entries can outlive
  their sie_imports row. Survivors or a failed check abort the year.
- Contract comment tying the stale-race regex to the RPC's RAISE wording.
- Suite-level beforeEach clears mocks and the event bus (repo convention).

Signed-off-by: Emil <emilmattsson14@gmail.com>

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-20 21:11:15 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 3f6f1ab06e
commit febb4cc0c2
5 changed files with 689 additions and 47 deletions
+12 -11
View File
@@ -31,7 +31,7 @@ import { parseSIEFile, validateSIEFile } from '@/lib/import/sie-parser'
import { mergeParsedSIEFiles } from '@/lib/import/sie-merge'
import { scanSieForCp1252Artifacts, formatSieArtifactWarning } from '@/lib/import/sie-artifact-scan'
import { suggestMappings, getMappingStats, isSystemAccount } from '@/lib/import/account-mapper'
import { loadMappings, generateImportPreview, executeSIEImport } from '@/lib/import/sie-import'
import { loadMappings, generateImportPreview, executeSIEImport, findOverlappingPeriodImports } from '@/lib/import/sie-import'
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-reference'
import type { ProviderName } from '@/lib/providers/types'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
@@ -921,16 +921,17 @@ export const arcimMigrationExtension: Extension = {
} | null = null
if (fyStart && fyEnd) {
const { data } = await supabase
.from('sie_imports')
.select('imported_at, fiscal_year_start, fiscal_year_end')
.eq('company_id', companyId)
.eq('status', 'completed')
.lte('fiscal_year_start', fyEnd)
.gte('fiscal_year_end', fyStart)
.limit(1)
.maybeSingle()
priorImport = data
// Newest first: several completed rows can overlap the same
// year (manual upload + provider sync, or residue from
// partially deleted data). The unordered .limit(1) this
// replaces picked an arbitrary row, so the wizard could show
// a stale "ersätter tidigare import från <date>" (issue #1667).
// Import-time replace resolves ALL rows regardless of which
// one is displayed here.
const overlapping = await findOverlappingPeriodImports(
supabase, companyId, fyStart, fyEnd
)
priorImport = overlapping[0] ?? null
}
fileStatuses.push({