* fix(arcim): allow Fortnox re-sync to replace prior SIE import per fiscal year
A user reported a sync failure when retrying Fortnox after adding more
verifications:
Import failed: Failed to create pending import record:
duplicate key value violates unique constraint
"sie_imports_company_id_file_hash_active_idx"
Root cause: Fortnox embeds the export-time #GEN date in every SIE export,
so the file hash always differs between syncs. The wizard's hash-based
duplicate detection treated each sync as a brand new file, but the engine
still rejected the insert because the per-period import slot was held by
the prior 'completed' row.
This change reframes Fortnox re-sync as a replace operation rather than a
fresh import:
- New executeSIEImport option `onExistingPeriod: 'block' | 'replace'`.
Manual SIE upload at /api/import/sie keeps default 'block' (current
behavior, no regression). The Fortnox /import-sie endpoint passes
'replace', which runs replaceSIEImport on any overlapping completed
import before insert. Imported journal entries from the prior import are
cancelled per BFL 5 kap 5§; user-created entries (manual, transaction,
invoice) are untouched.
- /sie-data switches from hash-based to period-based duplicate detection
and returns previousImport metadata per fiscal year.
- Wizard drops the alreadyImported skip filter, surfaces an amber callout
in the confirm dialog listing fiscal years that will be replaced, and
shows "ersatte N tidigare importerade verifikationer" per year.
- createPendingImportRecord translates 23505 partial-index violations to
a clear Swedish recovery message instead of leaking the raw constraint
name.
- cleanupStaleImportRecords drops the 1-hour age gate and also cleans
status='mapped' orphans. SIE imports are single-flight per company so
the gate just made legitimate retries fail.
- After replace, the fiscal_periods row's opening_balances_set and
opening_balance_entry_id are cleared (only when they pointed at the
cancelled prior IB entry), so the new IB import isn't skipped.
Schema-drift migration captures the partial unique index
sie_imports_company_id_file_hash_active_idx that already exists in
production (added out-of-band) and drops the now-superseded plain
sie_imports_company_id_file_hash_key constraint. Both statements are
idempotent — verified no-op against production.
Tests: new pg-real test covers the partial index admit-replaced
semantics, source_type='import'-only cancellation in replace_sie_import,
and the post-replace insert path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(arcim): address PR review — restore 5-min cleanup gate, fix test source_type
Greptile P2: `cleanupStaleImportRecords` was deleting `pending` rows
unconditionally, which could wipe a concurrent in-flight import in
another tab/session. Restored a 5-minute age gate (long enough for any
normal interactive import, short enough that legitimate retries after
a crash still succeed). Also dropped `mapped` from the cleanup — it is
defined in SIEImportStatus but no code path writes it, so including it
was both unnecessary and added the concurrent-session risk Greptile
flagged.
pg-real test: insertPostedEntry used `source_type='transaction'` which
is not a valid value per the journal_entries_source_type_check
constraint (migration 20260516060000). Switched to `'bank_transaction'`
— the actual source_type emitted when a user categorizes a bank
transaction in gnubok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>