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
@@ -2711,6 +2711,13 @@ export default function ArcimMigrationWorkspace({
setMigrationProgress(progress)
setMigrationStep(`Importerar bokföringsdata (SIE): fil ${i + 1} av ${filesToImport.length}...`)
// Name the year in every per-file failure: a multi-year re-sync
// that dies on ONE year must say which, or the user cannot act on
// it (issue #1667: the current year re-imported, the prior year
// refused, and the error never said so).
const fiscalYear = filesToImport[i].status?.fiscalYear
const yearLabel = fiscalYear ? `Räkenskapsår ${fiscalYear}: ` : ''
const res = await fetch('/api/extensions/ext/arcim-migration/import-sie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -2728,7 +2735,10 @@ export default function ArcimMigrationWorkspace({
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw apiError(data, `SIE import HTTP ${res.status}`)
const err = apiError(data, `SIE import HTTP ${res.status}`)
throw err instanceof UserFacingError
? new UserFacingError(`${yearLabel}${err.message}`)
: err
}
const result = await res.json() as ImportResult
@@ -2740,8 +2750,8 @@ export default function ArcimMigrationWorkspace({
// först" message masks the real error.
if (!result.success) {
throw new UserFacingError(result.errors.length > 0
? result.errors.join('\n')
: 'SIE-importen misslyckades utan felmeddelande.')
? `${yearLabel}${result.errors.join('\n')}`
: `${yearLabel}SIE-importen misslyckades utan felmeddelande.`)
}
}
}