feat: allow replacing completed SIE imports (#227)
* feat: allow replacing completed SIE imports Users who import a SIE file, make adjustments in the source system, and re-export can now replace the old import instead of being permanently blocked by the "overlapping fiscal year" guard. The old import's entries are cancelled (posted → cancelled) and the import is marked as 'replaced'. Nothing is deleted — full audit trail preserved per BFL 5 kap 5§ (rättelse) and BFNAR 2013:2 kap 8. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — atomic RPC, locked_at check - P1: Wrap entry cancellation + import status update in a single DB RPC (replace_sie_import) to prevent inconsistent state on partial failure - P2: Check locked_at in addition to is_closed for fiscal period guard - P2: Use RPC return value for accurate cancelled entry count Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
973a2b81a0
commit
258a64a849
@@ -129,6 +129,66 @@ export async function checkDuplicatePeriodImport(
|
||||
return data as SIEImport | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace (cancel) a completed SIE import so the user can re-import corrected
|
||||
* data for the same fiscal period.
|
||||
*
|
||||
* Per BFL 5 kap 5§ (rättelse), the original entries are preserved with
|
||||
* status='cancelled'. The import record is marked as 'replaced' with a
|
||||
* timestamp for audit trail (BFNAR 2013:2 kap 8 behandlingshistorik).
|
||||
* Nothing is deleted.
|
||||
*
|
||||
* The actual cancellation + status update is atomic via the replace_sie_import
|
||||
* DB RPC to prevent inconsistent state.
|
||||
*/
|
||||
export async function replaceSIEImport(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
importId: string
|
||||
): Promise<{ success: boolean; cancelledEntries: number; error?: string }> {
|
||||
// 1. Fetch and validate the import record
|
||||
const { data: importRecord } = await supabase
|
||||
.from('sie_imports')
|
||||
.select('status, fiscal_period_id')
|
||||
.eq('id', importId)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (!importRecord) {
|
||||
return { success: false, cancelledEntries: 0, error: 'Import hittades inte' }
|
||||
}
|
||||
|
||||
if (importRecord.status !== 'completed') {
|
||||
return { success: false, cancelledEntries: 0, error: `Kan bara ersätta slutförda importer (status: ${importRecord.status})` }
|
||||
}
|
||||
|
||||
// 2. Check that the fiscal period is not closed or locked
|
||||
if (importRecord.fiscal_period_id) {
|
||||
const { data: period } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.select('is_closed, locked_at')
|
||||
.eq('id', importRecord.fiscal_period_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (period?.is_closed || period?.locked_at) {
|
||||
return { success: false, cancelledEntries: 0, error: 'Kan inte ersätta import i ett låst eller stängt räkenskapsår. Öppna perioden först.' }
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Atomically cancel entries and mark import as replaced via DB RPC
|
||||
const { data: cancelledCount, error: rpcError } = await supabase.rpc('replace_sie_import', {
|
||||
p_company_id: companyId,
|
||||
p_import_id: importId,
|
||||
})
|
||||
|
||||
if (rpcError) {
|
||||
return { success: false, cancelledEntries: 0, error: `Kunde inte ersätta import: ${rpcError.message}` }
|
||||
}
|
||||
|
||||
return { success: true, cancelledEntries: cancelledCount as number }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale pending/failed import records for a given file hash.
|
||||
* Prevents UNIQUE constraint conflicts when re-importing after a failure.
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@ export type SIEType = 1 | 2 | 3 | 4
|
||||
export type SIEEncoding = 'cp437' | 'utf8' | 'windows1252'
|
||||
|
||||
// Import status
|
||||
export type SIEImportStatus = 'pending' | 'mapped' | 'completed' | 'failed'
|
||||
export type SIEImportStatus = 'pending' | 'mapped' | 'completed' | 'failed' | 'replaced'
|
||||
|
||||
// Match type for account mapping
|
||||
export type AccountMatchType = 'exact' | 'name' | 'class' | 'manual' | 'bas_range'
|
||||
@@ -193,6 +193,7 @@ export interface SIEImport {
|
||||
imported_at: string | null
|
||||
migration_documentation: MigrationDocumentation | null
|
||||
file_storage_path: string | null
|
||||
replaced_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user