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
@@ -326,6 +326,8 @@ function SIEImportWizard() {
|
||||
const [errorType, setErrorType] = useState<'duplicate' | 'duplicate_period' | 'validation' | 'parse' | undefined>()
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([])
|
||||
const [validationWarnings, setValidationWarnings] = useState<string[]>([])
|
||||
const [duplicatePeriodImportId, setDuplicatePeriodImportId] = useState<string | null>(null)
|
||||
const [isReplacing, setIsReplacing] = useState(false)
|
||||
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [, setParsed] = useState<ParsedSIEFile | null>(null)
|
||||
@@ -370,6 +372,9 @@ function SIEImportWizard() {
|
||||
if (type === 'duplicate' || type === 'duplicate_period') {
|
||||
setErrorType(type)
|
||||
setError(data.message)
|
||||
if (type === 'duplicate_period' && data.importId) {
|
||||
setDuplicatePeriodImportId(data.importId)
|
||||
}
|
||||
toast({ title: type === 'duplicate' ? 'Filen har redan importerats' : 'Överlappande räkenskapsår', description: data.message, variant: 'destructive' })
|
||||
} else if (type === 'validation') {
|
||||
setErrorType('validation')
|
||||
@@ -425,6 +430,39 @@ function SIEImportWizard() {
|
||||
}
|
||||
}, [toast])
|
||||
|
||||
const handleReplace = useCallback(async (importId: string) => {
|
||||
if (!file) return
|
||||
|
||||
setIsReplacing(true)
|
||||
try {
|
||||
const res = await fetch(`/api/import/sie/${importId}/replace`, { method: 'POST' })
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Kunde inte ersätta import', description: data.error || 'Ett fel uppstod', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Import ersatt',
|
||||
description: `${data.cancelledEntries} verifikation${data.cancelledEntries === 1 ? '' : 'er'} makulerades. Importerar ny fil...`,
|
||||
})
|
||||
|
||||
// Clear error state and re-trigger the file upload
|
||||
setError(null)
|
||||
setErrorType(undefined)
|
||||
setDuplicatePeriodImportId(null)
|
||||
|
||||
// Small delay so the user sees the success toast before re-upload starts
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
handleFileSelect(file)
|
||||
} catch {
|
||||
toast({ title: 'Anslutningsfel', description: 'Kunde inte nå servern.', variant: 'destructive' })
|
||||
} finally {
|
||||
setIsReplacing(false)
|
||||
}
|
||||
}, [file, handleFileSelect, toast])
|
||||
|
||||
const handleMappingChange = useCallback((sourceAccount: string, targetAccount: string, targetName: string) => {
|
||||
setMappings((prev) => applyMappingOverride(prev, sourceAccount, targetAccount, targetName))
|
||||
|
||||
@@ -572,7 +610,7 @@ function SIEImportWizard() {
|
||||
const handleNewImport = () => {
|
||||
setStep('upload'); setFile(null); setParsed(null); setMappings([])
|
||||
setPreview(null); setIssues([]); setImportResult(null); setError(null); setErrorType(undefined)
|
||||
setValidationErrors([]); setValidationWarnings([])
|
||||
setValidationErrors([]); setValidationWarnings([]); setDuplicatePeriodImportId(null)
|
||||
setSieAccounts([]); setIsCreatingAccounts(false)
|
||||
}
|
||||
|
||||
@@ -599,7 +637,7 @@ function SIEImportWizard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} errorType={errorType} validationErrors={validationErrors} validationWarnings={validationWarnings} />}
|
||||
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} errorType={errorType} validationErrors={validationErrors} validationWarnings={validationWarnings} duplicatePeriodImportId={duplicatePeriodImportId} onReplace={handleReplace} isReplacing={isReplacing} />}
|
||||
{step === 'preview' && preview && (
|
||||
<SIEPreviewStep preview={preview} issues={issues} missingAccounts={missingAccounts}
|
||||
onCreateAccounts={handleCreateAccounts} isCreatingAccounts={isCreatingAccounts}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { requireCompanyId } from '@/lib/company/context'
|
||||
import { requireWritePermission } from '@/lib/auth/require-write'
|
||||
import { replaceSIEImport } from '@/lib/import/sie-import'
|
||||
|
||||
/**
|
||||
* POST /api/import/sie/[id]/replace
|
||||
* Replace a completed SIE import by cancelling its entries, allowing
|
||||
* the user to re-import corrected data for the same fiscal period.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const supabase = await createClient()
|
||||
const { id } = await params
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const writeCheck = await requireWritePermission(supabase, user.id)
|
||||
if (!writeCheck.ok) return writeCheck.response
|
||||
|
||||
const companyId = await requireCompanyId(supabase, user.id)
|
||||
|
||||
const result = await replaceSIEImport(supabase, companyId, id)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
cancelledEntries: result.cancelledEntries,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user