Fix/import data (#200)

* fix: enhance import data handling and consent management across components

* feat: Enhance SIE import functionality with validation and error handling improvements

- Added validation errors and warnings state management in SIEImportWizard.
- Improved error handling for duplicate, validation, and parsing errors during SIE file import.
- Enhanced user feedback with actionable guidance for common import errors.
- Updated SIEUploadStep to display validation errors and warnings.
- Improved error messages in API routes for better clarity and user experience.
- Added file size and type validation in the SIE parse route.
- Enhanced parsing logic to provide more detailed error messages for unbalanced vouchers and missing amounts.
- Created a new storage bucket for SIE file archival in Supabase with appropriate policies for user access.
- Updated tests to reflect changes in error messages and validation logic.

* fix: Improve type assertion for response in getPage method

* Update extensions/general/arcim-migration/lib/migration-orchestrator.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update supabase/migrations/20260408130000_sie_files_storage_bucket.sql

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: Add company ID verification for consent handling in accept and disconnect endpoints

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Mattsson
2026-04-08 18:17:00 +02:00
committed by GitHub
co-authored by greptile-apps[bot]
parent 6c79f21679
commit 211033410c
20 changed files with 529 additions and 126 deletions
+58 -16
View File
@@ -1,6 +1,7 @@
'use client'
import { useState, useCallback, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Button } from '@/components/ui/button'
@@ -323,6 +324,8 @@ function SIEImportWizard() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [errorType, setErrorType] = useState<'duplicate' | 'duplicate_period' | 'validation' | 'parse' | undefined>()
const [validationErrors, setValidationErrors] = useState<string[]>([])
const [validationWarnings, setValidationWarnings] = useState<string[]>([])
const [file, setFile] = useState<File | null>(null)
const [, setParsed] = useState<ParsedSIEFile | null>(null)
@@ -347,6 +350,8 @@ function SIEImportWizard() {
setFile(selectedFile)
setError(null)
setErrorType(undefined)
setValidationErrors([])
setValidationWarnings([])
setIsLoading(true)
try {
@@ -361,15 +366,21 @@ function SIEImportWizard() {
const data = await res.json()
if (!res.ok) {
if (data.error === 'duplicate' || data.error === 'duplicate_period') {
setErrorType(data.error)
const type = data.error as typeof errorType
if (type === 'duplicate' || type === 'duplicate_period') {
setErrorType(type)
setError(data.message)
} else if (data.error === 'validation') {
toast({ title: type === 'duplicate' ? 'Filen har redan importerats' : 'Överlappande räkenskapsår', description: data.message, variant: 'destructive' })
} else if (type === 'validation') {
setErrorType('validation')
setError(`${data.message}: ${data.errors?.join(', ') || 'Unknown validation error'}`)
setError(data.message || 'SIE-filen innehåller valideringsfel.')
setValidationErrors(data.errors || [])
setValidationWarnings(data.warnings || [])
toast({ title: 'Valideringsfel i SIE-filen', description: `${(data.errors || []).length} fel hittades som måste åtgärdas.`, variant: 'destructive' })
} else {
setErrorType('parse')
setError(data.error || 'Failed to parse file')
setError(data.message || data.error || 'Kunde inte tolka filen.')
toast({ title: 'Kunde inte läsa filen', description: data.message || data.error || 'Kontrollera att filen är en giltig SIE-fil.', variant: 'destructive' })
}
return
}
@@ -402,7 +413,13 @@ function SIEImportWizard() {
description: `${data.parsed.stats.totalAccounts} konton och ${data.parsed.stats.totalVouchers} verifikationer hittades`,
})
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to parse file')
const isNetworkError = err instanceof TypeError && (err.message === 'Failed to fetch' || err.message.includes('NetworkError'))
const message = isNetworkError
? 'Kunde inte nå servern. Kontrollera din internetanslutning och försök igen.'
: err instanceof Error ? err.message : 'Ett oväntat fel uppstod.'
setErrorType('parse')
setError(message)
toast({ title: 'Anslutningsfel', description: message, variant: 'destructive' })
} finally {
setIsLoading(false)
}
@@ -506,9 +523,17 @@ function SIEImportWizard() {
if (!res.ok) {
if (data.error === 'duplicate') {
setError(data.message || 'Denna fil har redan importerats')
toast({ title: 'Filen har redan importerats', description: data.message, variant: 'destructive' })
return
}
if (data.result) {
setImportResult(data.result)
} else {
const msg = data.message || data.error || 'Importen misslyckades.'
setError(msg)
toast({ title: 'Import misslyckades', description: msg, variant: 'destructive' })
return
}
if (data.result) { setImportResult(data.result) } else { setError(data.error || 'Import failed'); return }
} else {
setImportResult(data.result)
}
@@ -516,21 +541,38 @@ function SIEImportWizard() {
setStep('result')
if (data.result?.success) {
toast({ title: 'Import genomförd', description: `${data.result.journalEntriesCreated} verifikationer skapades` })
const created = data.result.journalEntriesCreated
const skipped = data.result.details?.skippedVouchers?.total || 0
toast({
title: 'Import genomförd',
description: `${created} verifikationer skapades${skipped > 0 ? ` (${skipped} hoppades över)` : ''}`,
})
} else if (data.result && !data.result.success) {
toast({
title: 'Import slutförd med problem',
description: `${data.result.errors?.length || 0} fel uppstod under importen. Se resultatet för detaljer.`,
variant: 'destructive',
})
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Import failed')
const isNetworkError = err instanceof TypeError && (err.message === 'Failed to fetch' || err.message.includes('NetworkError'))
const msg = isNetworkError
? 'Tappade anslutningen till servern under importen. Kontrollera din internetanslutning och se om importen genomfördes under Bokföring.'
: err instanceof Error ? err.message : 'Ett oväntat fel uppstod.'
setError(msg)
toast({ title: 'Import avbröts', description: msg, variant: 'destructive' })
} finally {
setIsLoading(false)
}
}, [file, mappings, toast])
const goToStep = (targetStep: ImportWizardStep) => { setStep(targetStep); setError(null) }
const goToStep = (targetStep: ImportWizardStep) => { setStep(targetStep); setError(null); setValidationErrors([]); setValidationWarnings([]) }
const goBack = () => { const i = sieSteps.indexOf(step); if (i > 0) setStep(sieSteps[i - 1]) }
const handleNewImport = () => {
setStep('upload'); setFile(null); setParsed(null); setMappings([])
setPreview(null); setIssues([]); setImportResult(null); setError(null); setErrorType(undefined)
setValidationErrors([]); setValidationWarnings([])
setSieAccounts([]); setIsCreatingAccounts(false)
}
@@ -557,7 +599,7 @@ function SIEImportWizard() {
</CardContent>
</Card>
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} errorType={errorType} />}
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} errorType={errorType} validationErrors={validationErrors} validationWarnings={validationWarnings} />}
{step === 'preview' && preview && (
<SIEPreviewStep preview={preview} issues={issues} missingAccounts={missingAccounts}
onCreateAccounts={handleCreateAccounts} isCreatingAccounts={isCreatingAccounts}
@@ -794,19 +836,19 @@ export default function ImportPage() {
})
}, [])
// Auto-detect OAuth callback or deep-link mode from query params (disabled in sandbox)
// Sync mode from URL search params (reacts to client-side navigation changes)
const searchParams = useSearchParams()
useEffect(() => {
if (isSandbox) return
const params = new URLSearchParams(window.location.search)
if (params.get('migration')) {
if (searchParams.get('migration')) {
setMode('migration')
} else {
const modeParam = params.get('mode')
const modeParam = searchParams.get('mode')
if (modeParam && ['psd2', 'bank', 'sie', 'migration'].includes(modeParam)) {
setMode(modeParam as ImportMode)
}
}
}, [isSandbox])
}, [isSandbox, searchParams])
// Extensions are active if compiled in — no runtime toggle check needed
const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking')
const hasMigrationExtension = ENABLED_EXTENSION_IDS.has('arcim-migration')
+3 -3
View File
@@ -71,7 +71,7 @@ export async function POST(request: Request) {
const accounts: SIEAccount[] = body.accounts
if (!accounts || !Array.isArray(accounts) || accounts.length === 0) {
return NextResponse.json({ error: 'No accounts provided' }, { status: 400 })
return NextResponse.json({ error: 'Inga konton att skapa.' }, { status: 400 })
}
// Prepare accounts for upsert (idempotent — safe to retry)
@@ -117,7 +117,7 @@ export async function POST(request: Request) {
if (error) {
console.error('Error upserting accounts batch:', error)
return NextResponse.json({
error: `Failed to create accounts: ${error.message}`,
error: `Kunde inte skapa konton (batch ${Math.floor(i / batchSize) + 1}): ${error.message}. ${totalCreated} konton skapades innan felet.`,
created: totalCreated,
}, { status: 500 })
}
@@ -134,7 +134,7 @@ export async function POST(request: Request) {
} catch (error) {
console.error('Create accounts error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to create accounts' },
{ error: `Kunde inte skapa konton: ${error instanceof Error ? error.message : 'Okänt fel'}. Försök igen.` },
{ status: 500 }
)
}
+10 -5
View File
@@ -37,7 +37,7 @@ export async function POST(request: Request) {
const optionsJson = formData.get('options') as string | null
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
return NextResponse.json({ error: 'Ingen fil bifogad. Gå tillbaka och ladda upp filen igen.' }, { status: 400 })
}
// Parse options
@@ -87,9 +87,11 @@ export async function POST(request: Request) {
// Validate all accounts are mapped
const unmapped = mappings.filter((m) => !m.targetAccount)
if (unmapped.length > 0) {
const accountList = unmapped.slice(0, 5).map((m) => `${m.sourceAccount} (${m.sourceName})`).join(', ')
const remaining = unmapped.length > 5 ? ` och ${unmapped.length - 5} till` : ''
return NextResponse.json({
error: 'validation',
message: `${unmapped.length} account(s) are not mapped`,
message: `${unmapped.length} konto(n) saknar mappning: ${accountList}${remaining}. Gå tillbaka till kontomappningssteget och koppla alla konton.`,
unmappedAccounts: unmapped.map((m) => ({
account: m.sourceAccount,
name: m.sourceName,
@@ -183,7 +185,7 @@ export async function POST(request: Request) {
if (activateError) {
return NextResponse.json({
error: `Failed to activate accounts: ${activateError.message}`,
error: `Kunde inte aktivera konton i kontoplanen: ${activateError.message}. Kontrollera att kontona inte redan finns med andra inställningar.`,
}, { status: 500 })
}
}
@@ -208,7 +210,7 @@ export async function POST(request: Request) {
if (!result.success) {
return NextResponse.json({
error: 'import',
message: 'Import completed with errors',
message: 'Importen slutfördes med fel. Se detaljerna nedan för att förstå vad som gick snett.',
result,
}, { status: 400 })
}
@@ -219,8 +221,11 @@ export async function POST(request: Request) {
})
} catch (error) {
console.error('SIE import error:', error)
const detail = error instanceof Error ? error.message : ''
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to import SIE file' },
{
error: `Importen avbröts oväntat. Ingen data har sparats.${detail ? ` (${detail})` : ''} Försök igen — om felet kvarstår, kontakta support.`,
},
{ status: 500 }
)
}
+25 -4
View File
@@ -36,14 +36,31 @@ export async function POST(request: Request) {
const file = formData.get('file') as File | null
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
return NextResponse.json({ error: 'parse', message: 'Ingen fil bifogad i förfrågan.' }, { status: 400 })
}
// Validate file type
const filename = file.name.toLowerCase()
if (!filename.endsWith('.sie') && !filename.endsWith('.se')) {
return NextResponse.json(
{ error: 'Invalid file type. Please upload a .sie file' },
{ error: 'parse', message: 'Filtypen stöds inte. Ladda upp en fil med ändelsen .sie eller .se.' },
{ status: 400 }
)
}
// Validate file size (max 50 MB)
const MAX_FILE_SIZE = 50 * 1024 * 1024
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: 'parse', message: `Filen är för stor (${(file.size / 1024 / 1024).toFixed(1)} MB). Maxstorlek är 50 MB.` },
{ status: 400 }
)
}
// Validate file is not empty
if (file.size === 0) {
return NextResponse.json(
{ error: 'parse', message: 'Filen är tom (0 bytes). Kontrollera att exporten från bokföringsprogrammet genomfördes korrekt.' },
{ status: 400 }
)
}
@@ -92,7 +109,7 @@ export async function POST(request: Request) {
if (!validation.valid) {
return NextResponse.json({
error: 'validation',
message: 'SIE file has validation errors',
message: 'SIE-filen innehåller valideringsfel som måste åtgärdas innan import.',
errors: validation.errors,
warnings: validation.warnings,
}, { status: 400 })
@@ -151,8 +168,12 @@ export async function POST(request: Request) {
})
} catch (error) {
console.error('SIE parse error:', error)
const detail = error instanceof Error ? error.message : ''
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to parse SIE file' },
{
error: 'parse',
message: `Kunde inte tolka SIE-filen. Filen kan vara skadad eller i ett format som inte stöds.${detail ? ` (${detail})` : ''}`,
},
{ status: 500 }
)
}