Files
accounted/lib/providers/resolve-consent.ts
T
Mattssonandgreptile-apps[bot] 211033410c 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>
2026-04-08 18:17:00 +02:00

123 lines
4.0 KiB
TypeScript

import { createServiceClient } from '@/lib/supabase/server';
import type { TokenResponse } from './types';
import { getOAuthConfig } from './oauth-config';
import { refreshFortnoxToken } from './fortnox/oauth';
import { refreshVismaToken } from './visma/oauth';
import { refreshBrioxToken } from './briox/oauth';
import { refreshBjornLundenToken } from './bjornlunden/oauth';
export interface ResolvedConsent {
consent: Record<string, unknown>;
accessToken: string;
providerCompanyId?: string;
}
export async function resolveConsent(companyId: string, consentId: string): Promise<ResolvedConsent> {
const supabase = createServiceClient();
// Load consent
const { data: consentRows } = await supabase
.from('provider_consents')
.select('*')
.eq('id', consentId)
.eq('company_id', companyId)
.limit(1);
if (!consentRows || consentRows.length === 0) {
throw { status: 404, message: 'Consent not found' };
}
const consent = consentRows[0]!;
// Accept status 0 (token submitted, migration pending) and 1 (fully accepted)
if (consent.status !== 0 && consent.status !== 1) {
throw { status: 403, message: 'Consent is not in a valid status' };
}
if (!consent.provider) {
throw { status: 400, message: 'Consent has no provider set — complete onboarding first' };
}
// Load tokens
const { data: tokenRows } = await supabase
.from('provider_consent_tokens')
.select('*')
.eq('consent_id', consentId)
.limit(1);
if (!tokenRows || tokenRows.length === 0) {
throw { status: 401, message: 'No tokens found for this consent — complete OAuth first' };
}
const tokens = tokenRows[0]!;
// Bokio: private API tokens that don't expire
if (consent.provider === 'bokio') {
return {
consent,
accessToken: tokens.access_token as string,
providerCompanyId: tokens.provider_company_id as string | undefined,
};
}
// Björn Lunden: client credentials — auto-refresh when expired
if (consent.provider === 'bjornlunden') {
if (tokens.token_expires_at && new Date(tokens.token_expires_at as string) < new Date()) {
const refreshed = await refreshBjornLundenToken();
const newExpiresAt = new Date(Date.now() + refreshed.expires_in * 1000).toISOString();
await supabase
.from('provider_consent_tokens')
.update({
access_token: refreshed.access_token,
token_expires_at: newExpiresAt,
})
.eq('consent_id', consentId);
return {
consent,
accessToken: refreshed.access_token,
providerCompanyId: tokens.provider_company_id as string | undefined,
};
}
return {
consent,
accessToken: tokens.access_token as string,
providerCompanyId: tokens.provider_company_id as string | undefined,
};
}
// Check expiry, auto-refresh if needed
if (tokens.token_expires_at && new Date(tokens.token_expires_at as string) < new Date()) {
if (!tokens.refresh_token) {
throw { status: 401, message: 'Access token expired and no refresh token available' };
}
const config = getOAuthConfig(consent.provider as string);
let refreshed: TokenResponse;
if (consent.provider === 'fortnox') {
refreshed = await refreshFortnoxToken(config, tokens.refresh_token as string);
} else if (consent.provider === 'briox') {
refreshed = await refreshBrioxToken(config.clientId, tokens.refresh_token as string);
} else {
refreshed = await refreshVismaToken(config, tokens.refresh_token as string);
}
const newExpiresAt = new Date(Date.now() + refreshed.expires_in * 1000).toISOString();
await supabase
.from('provider_consent_tokens')
.update({
access_token: refreshed.access_token,
refresh_token: refreshed.refresh_token,
token_expires_at: newExpiresAt,
})
.eq('consent_id', consentId);
return { consent, accessToken: refreshed.access_token };
}
return { consent, accessToken: tokens.access_token as string };
}