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
+96 -12
View File
@@ -8,6 +8,7 @@ import {
getAuthUrl,
exchangeAuthToken,
submitProviderToken,
acceptConsent,
deleteConsent,
resolveConsent,
fetchCompanyInfoDirect,
@@ -148,28 +149,53 @@ export const arcimMigrationExtension: Extension = {
}
try {
const { createServiceClient: createSvc } = await import('@/lib/supabase/server')
// Reuse existing accepted consent if one exists for this provider
const existingConsents = await listConsents(companyId)
const existing = existingConsents.find(c => c.provider === provider && c.status === 1)
const accepted = existingConsents.find(c => c.provider === provider && c.status === 1)
if (existing) {
if (accepted) {
// Already connected — skip OAuth, go straight to preview
if (ctx?.settings) {
await ctx.settings.set('consent_id', existing.id)
await ctx.settings.set('consent_id', accepted.id)
await ctx.settings.set('provider', provider)
}
return NextResponse.json({
consentId: existing.id,
consentId: accepted.id,
authType: providerInfo.authType,
alreadyConnected: true,
})
}
// Clean up abandoned consents (status 0 = Created but never completed OAuth)
const abandoned = existingConsents.filter(c => c.provider === provider && c.status === 0)
for (const a of abandoned) {
await deleteConsent(a.id)
// Check for status 0 consents that already have tokens stored (credentials submitted but migration not completed)
const pending = existingConsents.filter(c => c.provider === provider && c.status === 0)
if (pending.length > 0) {
const svc = createSvc()
for (const p of pending) {
const { data: tokens } = await svc
.from('provider_consent_tokens')
.select('id')
.eq('consent_id', p.id)
.limit(1)
if (tokens && tokens.length > 0) {
// Tokens exist — reuse this consent, skip credential entry
if (ctx?.settings) {
await ctx.settings.set('consent_id', p.id)
await ctx.settings.set('provider', provider)
}
return NextResponse.json({
consentId: p.id,
authType: providerInfo.authType,
alreadyConnected: true,
})
}
}
// No tokens found — clean up abandoned consents
for (const p of pending) {
await deleteConsent(p.id)
}
}
// Create new consent
@@ -385,9 +411,9 @@ export const arcimMigrationExtension: Extension = {
try {
const consent = await getConsent(consentId)
if (consent.status !== 1) {
if (consent.status !== 0 && consent.status !== 1) {
return NextResponse.json(
{ error: 'Consent is not accepted. Complete OAuth first.' },
{ error: 'Consent is not ready. Complete authentication first.' },
{ status: 400 }
)
}
@@ -859,9 +885,9 @@ export const arcimMigrationExtension: Extension = {
try {
const consent = await getConsent(consentId)
if (consent.status !== 1) {
if (consent.status !== 0 && consent.status !== 1) {
return NextResponse.json(
{ error: 'Consent is not accepted' },
{ error: 'Consent is not ready' },
{ status: 400 }
)
}
@@ -882,6 +908,9 @@ export const arcimMigrationExtension: Extension = {
log.info('Migration completed:', results)
// Mark consent as fully accepted now that data has been imported
await acceptConsent(consentId)
return NextResponse.json({ success: true, results })
} catch (error) {
log.error('Migration failed:', error)
@@ -893,6 +922,48 @@ export const arcimMigrationExtension: Extension = {
},
},
// ── Accept consent (mark as fully connected after import) ─────
{
method: 'POST',
path: '/accept',
handler: async (request: Request, ctx?: ExtensionContext) => {
const supabase = ctx?.supabase ?? await (await import('@/lib/supabase/server')).createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const companyId = ctx?.companyId ?? user.id
const { consentId } = await request.json() as { consentId: string }
if (!consentId) {
return NextResponse.json({ error: 'consentId is required' }, { status: 400 })
}
// Verify consent belongs to this company before mutating
const { data: consent } = await supabase
.from('provider_consents')
.select('id')
.eq('id', consentId)
.eq('company_id', companyId)
.single()
if (!consent) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
try {
await acceptConsent(consentId)
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to accept consent' },
{ status: 500 }
)
}
},
},
// ── Disconnect / revoke consent ───────────────────────────────
{
method: 'DELETE',
@@ -906,12 +977,25 @@ export const arcimMigrationExtension: Extension = {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const companyId = ctx?.companyId ?? user.id
const { consentId } = await request.json() as { consentId: string }
if (!consentId) {
return NextResponse.json({ error: 'consentId is required' }, { status: 400 })
}
// Verify consent belongs to this company before mutating
const { data: consent } = await supabase
.from('provider_consents')
.select('id')
.eq('id', consentId)
.eq('company_id', companyId)
.single()
if (!consent) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
try {
await deleteConsent(consentId)
@@ -61,6 +61,8 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
const accessToken = resolved.accessToken
const providerCompanyId = resolved.providerCompanyId
try {
// ── Step 1: Company information ───────────────────────────────
if (options.importCompanyInfo !== false) {
@@ -240,7 +240,7 @@ export async function submitProviderToken(
tokenExpiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()
}
// Store tokens
// Store tokens — consent stays at status 0 until migration/SIE import completes
await supabase
.from('provider_consent_tokens')
.upsert({
@@ -252,11 +252,14 @@ export async function submitProviderToken(
provider_company_id: companyId,
})
// Mark consent as accepted
return { success: true, consentId }
}
/** Mark a consent as accepted (status 1) — call after migration or SIE import succeeds */
export async function acceptConsent(consentId: string): Promise<void> {
const supabase = createServiceClient()
await supabase
.from('provider_consents')
.update({ status: 1 })
.eq('id', consentId)
return { success: true, consentId }
}