Fix/transaction inconsitensies (#190)

* fix: request explicit page size from Enable Banking API to fetch all transactions

The API defaults to ~10 transactions per page when no limit is specified,
causing incomplete syncs for users with more transactions.


* fix: enhance DELETE operations and add missing RLS policies for multi-tenant support
This commit is contained in:
Mattsson
2026-04-08 11:25:46 +02:00
committed by GitHub
parent 3dd1f782c3
commit 6486e0d9e2
10 changed files with 381 additions and 45 deletions
@@ -44,12 +44,19 @@ const ARCIM_PROVIDERS: { id: ArcimProvider; name: string; authType: 'oauth' | 't
{ id: 'briox', name: 'Briox', authType: 'token' },
]
interface SkipReasons {
duplicate?: number
inactive?: number
failed?: number
noMatch?: number
}
interface MigrationResults {
companyInfo?: { imported: boolean }
customers?: { total: number; imported: number; skipped: number }
suppliers?: { total: number; imported: number; skipped: number }
salesInvoices?: { total: number; imported: number; skipped: number }
supplierInvoices?: { total: number; imported: number; skipped: number }
customers?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons }
suppliers?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons }
salesInvoices?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons }
supplierInvoices?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons }
}
import AccountMappingStep from '@/components/import/AccountMappingStep'
import type { AccountMapping, ImportResult, ParsedSIEFile } from '@/lib/import/types'
@@ -120,6 +127,7 @@ interface PreviewData {
transactionCount: number
fiscalYears: number[]
} | null
hasSieData: boolean
}
interface SIEFileStatus {
@@ -476,6 +484,7 @@ function ConnectStep({
<Input
id="apiToken"
type="password"
autoComplete="off"
placeholder={provider === 'briox' ? 'Klistra in din applikationstoken' : 'Klistra in din API-nyckel'}
value={apiToken}
onChange={(e) => setApiToken(e.target.value)}
@@ -489,6 +498,7 @@ function ConnectStep({
</label>
<Input
id="companyId"
autoComplete="off"
placeholder={isClientCredentials ? 'GUID från företagsinställningar' : 'GUID från URL:en, t.ex. 14ccad83-67f6-49bd-...'}
value={companyId}
onChange={(e) => setCompanyId(e.target.value)}
@@ -584,17 +594,36 @@ function PreviewStep({
</div>
)}
{preview && !preview.sieAvailable && !isLoading && (
<div className="flex gap-3 rounded-lg border border-warning/30 bg-warning/10 p-4">
<Info className="mt-0.5 h-5 w-5 shrink-0 text-warning-foreground" />
{preview && !preview.sieAvailable && !isLoading && preview.hasSieData && (
<div className="flex gap-3 rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-4">
<CheckCircle className="mt-0.5 h-5 w-5 shrink-0 text-emerald-500" />
<div>
<p className="text-sm font-medium text-warning-foreground">SIE-hämtning inte tillgänglig</p>
<p className="text-sm font-medium">SIE-data redan importerad</p>
<p className="text-xs text-muted-foreground">
SIE-hämtning är inte tillgänglig för denna leverantör ännu. Du kan importera SIE-filen manuellt via <Link href="/import?mode=sie" className="underline hover:text-foreground">SIE-importen</Link>.
Bokföringsdata har redan importerats via SIE-fil. Du kan fortsätta med att importera kunder, leverantörer och fakturor.
</p>
</div>
</div>
)}
{preview && !preview.sieAvailable && !isLoading && !preview.hasSieData && (
<div className="flex gap-3 rounded-lg border border-destructive/20 bg-destructive/5 p-4">
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0 text-destructive" />
<div>
<p className="text-sm font-medium text-destructive">SIE-import krävs</p>
<p className="text-xs text-muted-foreground">
Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil innan kunder, leverantörer och fakturor kan hämtas. Exportera en SIE-fil från {ARCIM_PROVIDERS.find(p => p.id === preview.consent.provider)?.name ?? 'ditt bokföringssystem'} och ladda upp den i gnubok.
</p>
<Button asChild variant="outline" size="sm" className="mt-3">
<Link href="/import?mode=sie">
<BookOpen className="mr-2 h-4 w-4" />
Gå till SIE-importen
<ExternalLink className="ml-2 h-3.5 w-3.5" />
</Link>
</Button>
</div>
</div>
)}
</CardContent>
</Card>
@@ -603,7 +632,7 @@ function PreviewStep({
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button className="min-h-11" onClick={onContinue} disabled={isLoading}>
<Button className="min-h-11" onClick={onContinue} disabled={isLoading || (!!preview && !preview.sieAvailable && !preview.hasSieData)}>
Fortsätt
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
@@ -817,15 +846,15 @@ function OptionsStep({
/>
<OptionRow
icon={<FileText className="h-4 w-4" />}
label="Kundfakturor (öppna)"
description="Obetalda kundfakturor"
label="Kundfakturor"
description="Alla kundfakturor (betalda och obetalda)"
checked={options.importSalesInvoices}
onChange={() => toggleOption('importSalesInvoices')}
/>
<OptionRow
icon={<FileText className="h-4 w-4" />}
label="Leverantörsfakturor (öppna)"
description="Obetalda leverantörsfakturor"
label="Leverantörsfakturor"
description="Alla leverantörsfakturor (betalda och obetalda)"
checked={options.importSupplierInvoices}
onChange={() => toggleOption('importSupplierInvoices')}
/>
@@ -1292,7 +1321,7 @@ function ResultStep({
label="Kunder"
status="success"
statusText={`${results.customers!.imported} importerade`}
detail={results.customers!.skipped > 0 ? `${results.customers!.skipped} fanns redan` : undefined}
detail={results.customers!.skipped > 0 ? formatSkipReasons(results.customers!.skipReasons, 'customer') ?? `${results.customers!.skipped} hoppades över` : undefined}
/>
)}
{hasSuppliers && (
@@ -1301,7 +1330,7 @@ function ResultStep({
label="Leverantörer"
status="success"
statusText={`${results.suppliers!.imported} importerade`}
detail={results.suppliers!.skipped > 0 ? `${results.suppliers!.skipped} fanns redan` : undefined}
detail={results.suppliers!.skipped > 0 ? formatSkipReasons(results.suppliers!.skipReasons, 'supplier') ?? `${results.suppliers!.skipped} hoppades över` : undefined}
/>
)}
{hasSalesInvoices && (
@@ -1310,7 +1339,7 @@ function ResultStep({
label="Kundfakturor"
status="success"
statusText={`${results.salesInvoices!.imported} importerade`}
detail={results.salesInvoices!.skipped > 0 ? `${results.salesInvoices!.skipped} hoppades över` : undefined}
detail={results.salesInvoices!.skipped > 0 ? formatSkipReasons(results.salesInvoices!.skipReasons, 'invoice') ?? `${results.salesInvoices!.skipped} hoppades över` : undefined}
/>
)}
{hasSupplierInvoices && (
@@ -1319,7 +1348,7 @@ function ResultStep({
label="Leverantörsfakturor"
status="success"
statusText={`${results.supplierInvoices!.imported} importerade`}
detail={results.supplierInvoices!.skipped > 0 ? `${results.supplierInvoices!.skipped} hoppades över` : undefined}
detail={results.supplierInvoices!.skipped > 0 ? formatSkipReasons(results.supplierInvoices!.skipReasons, 'invoice') ?? `${results.supplierInvoices!.skipped} hoppades över` : undefined}
/>
)}
</div>
@@ -1387,6 +1416,19 @@ function ResultStep({
)
}
function formatSkipReasons(reasons?: SkipReasons, entityType?: 'customer' | 'supplier' | 'invoice'): string | undefined {
if (!reasons) return undefined
const parts: string[] = []
if (reasons.duplicate) parts.push(`${reasons.duplicate} fanns redan`)
if (reasons.inactive) parts.push(`${reasons.inactive} inaktiv${reasons.inactive > 1 ? 'a' : ''}`)
if (reasons.noMatch) {
const matchLabel = entityType === 'invoice' ? 'utan matchning' : 'utan matchning'
parts.push(`${reasons.noMatch} ${matchLabel}`)
}
if (reasons.failed) parts.push(`${reasons.failed} misslyckades`)
return parts.length > 0 ? parts.join(', ') : undefined
}
/** Simple row for non-SIE entity results (customers, invoices, etc.) */
function EntityResultRow({
icon,