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:
co-authored by
greptile-apps[bot]
parent
6c79f21679
commit
211033410c
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user