import db from '../config/database'; import { DomainEventBus } from '../core/runtime/DomainEventBus'; import { logger } from '../utils/logger'; export interface PlatformAccount { id: string; tenantId: string; platform: string; accountName: string; accountId: string; status: 'ACTIVE' | 'INACTIVE' | 'EXPIRED' | 'ERROR'; token?: string; refreshToken?: string; expiresAt?: string; shopId?: string; config?: Record; lastSyncAt?: string; createdAt: string; updatedAt: string; } export interface PlatformAccountStats { total: number; active: number; inactive: number; expired: number; error: number; } export class PlatformAccountService { private static TABLE = 'cf_platform_account'; static async initTable(): Promise { const hasTable = await db.schema.hasTable(this.TABLE); if (!hasTable) { await db.schema.createTable(this.TABLE, (table) => { table.string('id', 64).primary(); table.string('tenant_id', 64).notNullable(); table.string('platform', 64).notNullable(); table.string('account_name', 128).notNullable(); table.string('account_id', 128).notNullable(); table.enum('status', ['ACTIVE', 'INACTIVE', 'EXPIRED', 'ERROR']).defaultTo('ACTIVE'); table.text('token'); table.text('refresh_token'); table.timestamp('expires_at'); table.string('shop_id', 64); table.json('config'); table.timestamp('last_sync_at'); table.timestamps(true, true); table.index(['tenant_id', 'platform']); table.index(['shop_id']); table.unique(['tenant_id', 'platform', 'account_id']); }); } } static async list(tenantId: string, filters?: { platform?: string; status?: string; shopId?: string }): Promise { let query = db(this.TABLE).where('tenant_id', tenantId); if (filters?.platform) query = query.where('platform', filters.platform); if (filters?.status) query = query.where('status', filters.status); if (filters?.shopId) query = query.where('shop_id', filters.shopId); return query.orderBy('created_at', 'desc'); } static async getById(id: string): Promise { const account = await db(this.TABLE).where({ id }).first(); return account || null; } static async create(tenantId: string, data: { platform: string; accountName: string; accountId: string; token?: string; refreshToken?: string; expiresAt?: string; shopId?: string; config?: Record; }): Promise { const id = this.generateId(); const now = new Date().toISOString(); const account: PlatformAccount = { id, tenantId, platform: data.platform, accountName: data.accountName, accountId: data.accountId, status: 'ACTIVE', token: data.token, refreshToken: data.refreshToken, expiresAt: data.expiresAt, shopId: data.shopId, config: data.config, createdAt: now, updatedAt: now, }; await db(this.TABLE).insert({ id: account.id, tenant_id: account.tenantId, platform: account.platform, account_name: account.accountName, account_id: account.accountId, status: account.status, token: account.token, refresh_token: account.refreshToken, expires_at: account.expiresAt, shop_id: account.shopId, config: account.config ? JSON.stringify(account.config) : null, created_at: account.createdAt, updated_at: account.updatedAt, }); await DomainEventBus.publish('platform_account.created', { accountId: id, tenantId, platform: data.platform }); return account; } static async update(id: string, data: Partial): Promise { const updateData: Record = { updated_at: new Date().toISOString() }; if (data.accountName) updateData.account_name = data.accountName; if (data.status) updateData.status = data.status; if (data.token !== undefined) updateData.token = data.token; if (data.refreshToken !== undefined) updateData.refresh_token = data.refreshToken; if (data.expiresAt !== undefined) updateData.expires_at = data.expiresAt; if (data.shopId !== undefined) updateData.shop_id = data.shopId; if (data.config) updateData.config = JSON.stringify(data.config); await db(this.TABLE).where({ id }).update(updateData); const account = await this.getById(id); await DomainEventBus.publish('platform_account.updated', { accountId: id }); return account!; } static async delete(id: string): Promise { await db(this.TABLE).where({ id }).delete(); await DomainEventBus.publish('platform_account.deleted', { accountId: id }); } static async refreshToken(id: string): Promise<{ success: boolean; message: string }> { const account = await this.getById(id); if (!account) return { success: false, message: 'Account not found' }; if (!account.refreshToken) return { success: false, message: 'No refresh token available' }; try { const newToken = await this.callPlatformRefreshApi(account.platform, account.refreshToken); await db(this.TABLE).where({ id }).update({ token: newToken.token, expires_at: newToken.expiresAt, status: 'ACTIVE', updated_at: new Date().toISOString(), }); await DomainEventBus.publish('platform_account.token_refreshed', { accountId: id }); return { success: true, message: 'Token refreshed successfully' }; } catch (error: any) { await db(this.TABLE).where({ id }).update({ status: 'ERROR', updated_at: new Date().toISOString(), }); return { success: false, message: error.message }; } } static async testConnection(id: string): Promise<{ success: boolean; message: string }> { const account = await this.getById(id); if (!account) return { success: false, message: 'Account not found' }; try { const isValid = await this.validateToken(account.platform, account.token!); if (isValid) { await db(this.TABLE).where({ id }).update({ status: 'ACTIVE', updated_at: new Date().toISOString() }); return { success: true, message: 'Connection successful' }; } await db(this.TABLE).where({ id }).update({ status: 'EXPIRED', updated_at: new Date().toISOString() }); return { success: false, message: 'Token expired' }; } catch (error: any) { await db(this.TABLE).where({ id }).update({ status: 'ERROR', updated_at: new Date().toISOString() }); return { success: false, message: error.message }; } } static async syncAccount(id: string): Promise<{ success: boolean; message: string }> { const account = await this.getById(id); if (!account) return { success: false, message: 'Account not found' }; try { await db(this.TABLE).where({ id }).update({ last_sync_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); await DomainEventBus.publish('platform_account.sync_requested', { accountId: id, platform: account.platform }); return { success: true, message: 'Sync initiated' }; } catch (error: any) { return { success: false, message: error.message }; } } static async getStats(tenantId: string): Promise { const accounts = await this.list(tenantId); return { total: accounts.length, active: accounts.filter((a) => a.status === 'ACTIVE').length, inactive: accounts.filter((a) => a.status === 'INACTIVE').length, expired: accounts.filter((a) => a.status === 'EXPIRED').length, error: accounts.filter((a) => a.status === 'ERROR').length, }; } static generateOAuthUrl(accountId: string, platform: string): string { const OAUTH_CONFIG: Record = { AMAZON: { authUrl: 'https://sellercentral.amazon.com/apps/authorize/consent', clientId: process.env.AMAZON_CLIENT_ID || '', scope: 'sellingpartnerapi::migration', }, EBAY: { authUrl: 'https://auth.ebay.com/oauth2/authorize', clientId: process.env.EBAY_CLIENT_ID || '', scope: 'https://api.ebay.com/oauth/api_scope', }, SHOPIFY: { authUrl: `https://{shop}.myshopify.com/admin/oauth/authorize`, clientId: process.env.SHOPIFY_CLIENT_ID || '', scope: 'read_products,write_products,read_orders,write_orders', }, SHOPEE: { authUrl: 'https://partner.shopeemobile.com/api/v2/shop/auth_partner', clientId: process.env.SHOPEE_PARTNER_ID || '', scope: '', }, TIKTOK: { authUrl: 'https://services.tiktokshop.com/open/authorize', clientId: process.env.TIKTOK_APP_ID || '', scope: '', }, }; const config = OAUTH_CONFIG[platform.toUpperCase()]; if (!config) { throw new Error(`Unsupported platform: ${platform}`); } const redirectUri = `${process.env.API_BASE_URL}/api/platform-auth/callback/${platform.toLowerCase()}`; const state = Buffer.from(JSON.stringify({ accountId, timestamp: Date.now() })).toString('base64'); const params = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, response_type: 'code', scope: config.scope, state, }); return `${config.authUrl}?${params.toString()}`; } static async handleOAuthCallback(platform: string, code: string, state: string): Promise<{ success: boolean; error?: string }> { try { const stateData = JSON.parse(Buffer.from(state, 'base64').toString()); const { accountId } = stateData; const account = await this.getById(accountId); if (!account) { return { success: false, error: 'Account not found' }; } const tokens = await this.exchangeCodeForTokens(platform, code); await db(this.TABLE).where({ id: accountId }).update({ token: tokens.accessToken, refresh_token: tokens.refreshToken, expires_at: tokens.expiresAt, status: 'ACTIVE', updated_at: new Date().toISOString(), }); await DomainEventBus.publish('platform_account.authorized', { accountId, platform }); return { success: true }; } catch (error: any) { logger.error(`[PlatformAccountService] OAuth callback error: ${error.message}`); return { success: false, error: error.message }; } } static async disconnect(id: string): Promise { await db(this.TABLE).where({ id }).update({ status: 'INACTIVE', token: null, refresh_token: null, updated_at: new Date().toISOString(), }); await DomainEventBus.publish('platform_account.disconnected', { accountId: id }); } private static async exchangeCodeForTokens(platform: string, code: string): Promise<{ accessToken: string; refreshToken: string; expiresAt: string; }> { return { accessToken: `access_${platform}_${Date.now()}`, refreshToken: `refresh_${platform}_${Date.now()}`, expiresAt: new Date(Date.now() + 3600000 * 24 * 30).toISOString(), }; } private static async callPlatformRefreshApi(platform: string, refreshToken: string): Promise<{ token: string; expiresAt: string }> { return { token: `new_token_${Date.now()}`, expiresAt: new Date(Date.now() + 3600000).toISOString(), }; } private static async validateToken(platform: string, token: string): Promise { return !!token; } private static generateId(): string { return `pa_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }