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
@@ -452,6 +452,13 @@ export const arcimMigrationExtension: Extension = {
}
}
// Check if the company already has completed SIE imports (from manual upload)
const { count: sieImportCount } = await supabase
.from('sie_imports')
.select('*', { count: 'exact', head: true })
.eq('company_id', companyId)
.eq('status', 'completed')
return NextResponse.json({
consent: {
id: consent.id,
@@ -462,6 +469,7 @@ export const arcimMigrationExtension: Extension = {
companyInfo: mapped,
sieAvailable,
sieStats,
hasSieData: (sieImportCount ?? 0) > 0,
})
} catch (error) {
log.error('Preview error:', error)
@@ -8,12 +8,12 @@
* 1. Company info → pre-fill company_settings
* 2. Customers → needed before sales invoices
* 3. Suppliers → needed before supplier invoices
* 4. Sales invoices (open only)
* 5. Supplier invoices (open only)
* 4. Sales invoices (all statuses, duplicates skipped)
* 5. Supplier invoices (all statuses, duplicates skipped)
*/
import type { SupabaseClient } from '@supabase/supabase-js'
import type { MigrationProgress, MigrationResults } from '../types'
import type { MigrationProgress, MigrationResults, SkipReasons } from '../types'
import type { ProviderName } from '@/lib/providers/types'
import { resolveConsent } from '@/lib/providers/resolve-consent'
import {
@@ -111,10 +111,12 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
const customers = await fetchCustomersDirect(provider, accessToken, providerCompanyId)
let imported = 0
let skipped = 0
const skipReasons: SkipReasons = {}
for (const customer of customers) {
if (!customer.active) {
console.log(`[migration] Customer skipped (inactive): ${customer.party.name}`)
skipReasons.inactive = (skipReasons.inactive ?? 0) + 1
skipped++
continue
}
@@ -132,6 +134,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (existing && existing.length > 0) {
console.log(`[migration] Customer skipped (duplicate org_number ${orgNumber}): ${customer.party.name}`)
customerIdMap.set(customer.id, existing[0].id)
skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1
skipped++
continue
}
@@ -146,6 +149,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (error || !inserted) {
console.error(`[migration] Customer insert failed: ${customer.party.name}`, error?.message)
skipReasons.failed = (skipReasons.failed ?? 0) + 1
skipped++
} else {
customerIdMap.set(customer.id, inserted.id)
@@ -153,7 +157,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
}
}
results.customers = { total: customers.length, imported, skipped }
results.customers = { total: customers.length, imported, skipped, skipReasons }
} catch (err) {
console.error('Failed to import customers:', err)
}
@@ -168,10 +172,12 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
const suppliers = await fetchSuppliersDirect(provider, accessToken, providerCompanyId)
let imported = 0
let skipped = 0
const skipReasons: SkipReasons = {}
for (const supplier of suppliers) {
if (!supplier.active) {
console.log(`[migration] Supplier skipped (inactive): ${supplier.party.name}`)
skipReasons.inactive = (skipReasons.inactive ?? 0) + 1
skipped++
continue
}
@@ -189,6 +195,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (existing && existing.length > 0) {
console.log(`[migration] Supplier skipped (duplicate org_number ${orgNumber}): ${supplier.party.name}`)
supplierIdMap.set(supplier.id, existing[0].id)
skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1
skipped++
continue
}
@@ -203,6 +210,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (error || !inserted) {
console.error(`[migration] Supplier insert failed: ${supplier.party.name}`, error?.message)
skipReasons.failed = (skipReasons.failed ?? 0) + 1
skipped++
} else {
supplierIdMap.set(supplier.id, inserted.id)
@@ -210,26 +218,24 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
}
}
results.suppliers = { total: suppliers.length, imported, skipped }
results.suppliers = { total: suppliers.length, imported, skipped, skipReasons }
} catch (err) {
console.error('Failed to import suppliers:', err)
}
}
// ── Step 4: Sales invoices (open/unpaid only) ─────────────────
// ── Step 4: Sales invoices ────────────────────────────────────
if (options.importSalesInvoices !== false) {
emitProgress(options, { status: 'importing', currentStep: 'Importerar kundfakturor...', progress: 60 })
try {
const invoices = await fetchSalesInvoicesDirect(provider, accessToken, providerCompanyId)
const openInvoices = invoices.filter(i =>
i.status === 'sent' || i.status === 'overdue' || i.status === 'booked'
)
console.log(`[migration] Sales invoices: ${invoices.length} total, ${openInvoices.length} open (filtered by status: sent/overdue/booked)`)
console.log(`[migration] Sales invoices: ${invoices.length} total`)
let imported = 0
let skipped = 0
const skipReasons: SkipReasons = {}
for (const inv of openInvoices) {
for (const inv of invoices) {
const customerOrgNumber = inv.customer.legalEntity?.companyId ||
inv.customer.identifications?.find(i => i.schemeId === 'SE:ORGNR')?.id
@@ -280,6 +286,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (!customerId) {
console.log(`[migration] Sales invoice ${inv.invoiceNumber} skipped — no customer match for "${inv.customer.name}" (org: ${customerOrgNumber || 'n/a'})`)
skipReasons.noMatch = (skipReasons.noMatch ?? 0) + 1
skipped++
continue
}
@@ -293,6 +300,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (existingInv && existingInv.length > 0) {
console.log(`[migration] Sales invoice ${inv.invoiceNumber} skipped — already exists`)
skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1
skipped++
continue
}
@@ -307,6 +315,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (invError || !insertedInv) {
console.error(`[migration] Sales invoice ${inv.invoiceNumber} insert failed:`, invError?.message)
skipReasons.failed = (skipReasons.failed ?? 0) + 1
skipped++
continue
}
@@ -322,26 +331,24 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
imported++
}
results.salesInvoices = { total: openInvoices.length, imported, skipped }
results.salesInvoices = { total: invoices.length, imported, skipped, skipReasons }
} catch (err) {
console.error('Failed to import sales invoices:', err)
}
}
// ── Step 5: Supplier invoices (open/unpaid only) ──────────────
// ── Step 5: Supplier invoices ─────────────────────────────────
if (options.importSupplierInvoices !== false) {
emitProgress(options, { status: 'importing', currentStep: 'Importerar leverantörsfakturor...', progress: 80 })
try {
const invoices = await fetchSupplierInvoicesDirect(provider, accessToken, providerCompanyId)
const openInvoices = invoices.filter(i =>
i.status === 'sent' || i.status === 'overdue' || i.status === 'booked' || i.status === 'draft'
)
console.log(`[migration] Supplier invoices: ${invoices.length} total, ${openInvoices.length} open (filtered by status: sent/overdue/booked/draft)`)
console.log(`[migration] Supplier invoices: ${invoices.length} total`)
let imported = 0
let skipped = 0
const skipReasons: SkipReasons = {}
for (const inv of openInvoices) {
for (const inv of invoices) {
const supplierOrgNumber = inv.supplier.legalEntity?.companyId ||
inv.supplier.identifications?.find(i => i.schemeId === 'SE:ORGNR')?.id
@@ -392,6 +399,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (!supplierId) {
console.log(`[migration] Supplier invoice ${inv.invoiceNumber} skipped — no supplier match for "${inv.supplier.name}" (org: ${supplierOrgNumber || 'n/a'})`)
skipReasons.noMatch = (skipReasons.noMatch ?? 0) + 1
skipped++
continue
}
@@ -406,6 +414,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (existingInv && existingInv.length > 0) {
console.log(`[migration] Supplier invoice ${inv.invoiceNumber} skipped — already exists for supplier "${inv.supplier.name}"`)
skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1
skipped++
continue
}
@@ -418,6 +427,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (arrivalError || arrivalNum == null) {
console.error(`[migration] Supplier invoice ${inv.invoiceNumber} skipped — could not get arrival number:`, arrivalError?.message)
skipReasons.failed = (skipReasons.failed ?? 0) + 1
skipped++
continue
}
@@ -432,6 +442,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
if (invError || !insertedInv) {
console.error(`[migration] Supplier invoice ${inv.invoiceNumber} insert failed for "${inv.supplier.name}":`, invError?.message, JSON.stringify(mappedInvoice, null, 2))
skipReasons.failed = (skipReasons.failed ?? 0) + 1
skipped++
continue
}
@@ -447,7 +458,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
imported++
}
results.supplierInvoices = { total: openInvoices.length, imported, skipped }
results.supplierInvoices = { total: invoices.length, imported, skipped, skipReasons }
} catch (err) {
console.error('Failed to import supplier invoices:', err)
}
+11 -4
View File
@@ -52,12 +52,19 @@ export interface MigrationProgress {
error?: string
}
export interface SkipReasons {
duplicate?: number
inactive?: number
failed?: number
noMatch?: number
}
export 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 }
}
// ── Consent flow ────────────────────────────────────────────────────
@@ -147,6 +147,7 @@ const FETCH_TIMEOUT_MS = 15_000
const MAX_RETRIES = 2
const RETRY_DELAY_MS = 1000
const MAX_PAGINATION_PAGES = 100
const DEFAULT_PAGE_SIZE = 500
// API Helper
@@ -467,6 +468,7 @@ export async function getAccountTransactions(
if (dateFrom) params.set('date_from', dateFrom)
if (dateTo) params.set('date_to', dateTo)
if (continuationKey) params.set('continuation_key', continuationKey)
params.set('limit', String(DEFAULT_PAGE_SIZE))
const queryString = params.toString()
const endpoint = `/accounts/${accountUid}/transactions${queryString ? `?${queryString}` : ''}`
@@ -542,6 +544,7 @@ export async function getAllTransactionsWithRaw(
if (dateFrom) params.set('date_from', dateFrom)
if (dateTo) params.set('date_to', dateTo)
if (continuationKey) params.set('continuation_key', continuationKey)
params.set('limit', String(DEFAULT_PAGE_SIZE))
const queryString = params.toString()
const endpoint = `/accounts/${accountUid}/transactions${queryString ? `?${queryString}` : ''}`