import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Not, Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import { google } from 'googleapis'; import { IntegrationConfig } from '../entity/integration-config.entity'; import { UserIntegration } from '../entity/user-integration.entity'; import { IntegrationEntityMapper } from '../entity/integration-entity-mapper.entity'; import { IntegrationFactory } from '../factories/integration.factory'; import { IntegrationResult } from '../strategies/integration.strategy'; import { GmailApiStrategy } from '../strategies/email/gmail-api.strategy'; import { SendGridApiStrategy } from '../strategies/email/sendgrid-api.strategy'; import { IntegrationQueueService } from './integration-queue.service'; import { BulkMessageDto, BulkCreateUserIntegrationDto, CreateUserIntegrationDto, UpdateUserIntegrationDto, } from '../dto/create-config.dto'; import { FieldMapperService } from '../../mapper/service/field-mapper.service'; import { COMM_TEMPLATE, ENTITYTYPE_MEDIA, } from '../../../constant/global.constant'; import { EntityServiceImpl } from '../../meta/service/entity-service-impl.service'; import { MediaDataService } from '../../meta/service/media-data.service'; import axios from 'axios'; import { ReflectionHelper } from '../../../utils/service/reflection-helper.service'; export interface SendMessageDto { levelId: number; levelType: string; app_code: string; to: string; message: string; mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE'; priority?: number; user_id?: number; } export interface GenericMessageDto { levelId: number; levelType: string; app_code: string; to: string | string[]; message: string; subject?: string; type?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE'; priority?: 'high' | 'medium' | 'low'; cc?: string | string[]; bcc?: string | string[]; html?: string; attachments?: any[]; mediaUrl?: string; templateId?: string; variables?: Record; user_id?: number; entity_type?: string; entity_id?: number; organization_id?: number; enterprise_id?: number; mapped_entities?: any; } export interface IntegrationConfigWithConfig extends IntegrationConfig { config?: any; } interface GmailOAuthState { levelId: number; levelType: string; app_code: string; email?: string; timestamp: number; } export interface GmailSSOResult { hubId: number; configId: number; } @Injectable() export class IntegrationService { private readonly logger = new Logger(IntegrationService.name); private readonly gmailOAuthStates = new Map(); constructor( @InjectRepository(IntegrationConfig) private readonly configRepository: Repository, @InjectRepository(UserIntegration) private readonly userIntegrationRepository: Repository, @InjectRepository(IntegrationEntityMapper) private readonly entityMapperRepository: Repository, private readonly dataSource: DataSource, private readonly integrationFactory: IntegrationFactory, private readonly gmailApiStrategy: GmailApiStrategy, private readonly sendGridApiStrategy: SendGridApiStrategy, private readonly configService: ConfigService, private readonly mediaService: MediaDataService, @Inject('FieldMapperService') private readonly fieldMapperService: FieldMapperService, private readonly entityService: EntityServiceImpl, private readonly reflectionHelper: ReflectionHelper, @Inject(forwardRef(() => IntegrationQueueService)) private readonly queueService?: IntegrationQueueService, ) {} private deriveServiceType( integration_type: string, integration_provider: string, config_json: any, ): string { // All integrations use API only (no SMTP support) return 'API'; } async sendMessage({ levelId, levelType, app_code, to, message, mode, priority = 1, user_id, }: SendMessageDto): Promise { try { // Get active communication configs for the level const configs = await this.getActiveConfigs( levelId, levelType, app_code, mode, ); if (!configs.length) { throw new Error( `No active communication configuration found for ${levelType} ${levelId}`, ); } // Sort by priority if provided const sortedConfigs = this.sortConfigsByPriority(configs, priority); // Try each config until one succeeds for (const config of sortedConfigs) { try { const result = await this.sendViaConfig(config, to, message, user_id); if (result.success) { this.logger.log( `Message sent successfully via ${config.integration_provider}`, ); return result; } this.logger.warn( `Failed to send via ${config.integration_provider}: ${result.error}`, ); } catch (error) { this.logger.error( `Error sending via ${config.integration_provider}:`, error.message, ); continue; } } throw new Error('All communication providers failed'); } catch (error) { this.logger.error('Communication service error:', error.message); throw error; } } async getActiveConfigs( levelId: number, levelType: string, appcode: string, mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', ): Promise { const queryBuilder = this.configRepository .createQueryBuilder('config') .where('config.level_id = :levelId', { levelId }) .andWhere('config.level_type = :levelType', { levelType }) .andWhere('config.app_code = :appcode', { appcode }) .andWhere('config.status = 1'); if (mode) { queryBuilder.andWhere('config.integration_type = :mode', { mode }); } return await queryBuilder.getMany(); } async getSingleActiveConfig( levelId: number, levelType: string, app_code: string, integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', ): Promise { const configs = await this.configRepository .createQueryBuilder('config') .where('config.level_id = :levelId', { levelId }) .andWhere('config.level_type = :levelType', { levelType }) .andWhere('config.app_code = :app_code', { app_code }) .andWhere('config.integration_type = :integration_type', { integration_type, }) .andWhere('config.status = 1') .orderBy('config.is_default', 'DESC') .addOrderBy('config.priority', 'ASC') .addOrderBy('config.created_at', 'DESC') .getMany(); return configs.length > 0 ? configs[0] : null; } async getAllIntegrationData( loggedInUser, integration_type?: string, ): Promise { try { const integrationSourceRepo = this.reflectionHelper.getRepoService('IntegrationSource'); const allIntegrationData = await integrationSourceRepo.find(); // if entityType is provided, filter the results if (integration_type) { return allIntegrationData.filter( (data) => data.integration_type.toLowerCase() === integration_type.toLowerCase(), ); } return allIntegrationData; } catch (error) { this.logger.error('Error fetching integration data:', error.message); return []; } } private sortConfigsByPriority( configs: IntegrationConfig[], _priority: number, ): IntegrationConfig[] { return configs.sort((a, b) => { // First sort by default (true comes first) if (a.is_default !== b.is_default) { return a.is_default ? -1 : 1; } // Then by priority (lower number = higher priority) return a.priority - b.priority; }); } private async sendViaConfig( config: IntegrationConfig, to: string, message: string, user_id?: number, ): Promise { const strategy = this.integrationFactory.create( config.integration_type, 'API', // service is deprecated, using default config.integration_provider, ); // Get user integration data if user_id provided let finalConfig = config.config_json; if (user_id) { const userIntegrationData = await this.getUserIntegrationForStrategy( user_id, config.id, ); if (userIntegrationData) { finalConfig = { ...config.config_json, external_user_id: userIntegrationData.external_user_id, }; } } const result = await strategy.sendMessage(to, message, finalConfig); // If token was refreshed, update it in the database if (result.refreshedToken && result.success) { try { const currentConfig = config.config_json as any; const updatedConfig = { ...currentConfig, accessToken: result.refreshedToken, }; await this.configRepository.update(config.id, { config_json: updatedConfig, } as any); this.logger.log( `Updated access token for ${config.integration_provider} configuration`, ); } catch (error) { this.logger.warn( `Failed to update refreshed token in database: ${error.message}`, ); } } return result; } async createIntegrationConfig( levelId: number, levelType: string, app_code: string, configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', provider: string, integration_source_id: number, config: any, priority?: number, is_default?: boolean, ): Promise< IntegrationConfig | { authUrl: string; state: string; message: string } > { // Validate that no duplicate provider configurations exist await this.validateUniqueActiveConfig( levelId, levelType, app_code, configType, provider, ); // Deactivate all other configurations of the same integration type await this.configRepository.update( { level_id: levelId, level_type: levelType, app_code: app_code, integration_type: configType, status: 1, }, { status: 0 }, ); // Validate provider and get service type from supported combinations const supportedCombinations = await this.getSupportedCombinations(); const validCombination = supportedCombinations.find( (combo) => combo.mode === configType && combo.provider.toLowerCase() === provider.toLowerCase(), ); if (!validCombination) { throw new Error(`Unsupported combination: ${configType}/${provider}`); } const service = validCombination.service; // Check if this requires OAuth flow const requiresOAuth = this.requiresOAuthFlow(configType, provider, config); if (requiresOAuth) { // Generate OAuth URL and return it instead of creating config immediately return await this.generateOAuthUrl( levelId, levelType, app_code, configType, provider, integration_source_id, config, priority, is_default, ); } // Direct config creation (non-OAuth flow) return await this.createDirectConfig( levelId, levelType, app_code, configType, provider, integration_source_id, config, priority, is_default, ); } getSupportedCombinations(): { mode: string; service: string; provider: string; }[] { return this.integrationFactory.getAllSupportedCombinations(); } async getLevelConfigs( levelId: number, levelType: string, filters?: { app_code?: string; integration_type?: 'WA' | 'SMS' | 'EMAIL' | 'TELEPHONE'; integration_provider?: string; }, ): Promise< Array > { const where: any = { level_id: levelId, level_type: levelType }; if (filters?.app_code) { where.app_code = filters.app_code; } if (filters?.integration_type) { where.integration_type = filters.integration_type; } if (filters?.integration_provider) { where.integration_provider = filters.integration_provider; } const hubs = await this.configRepository.find({ where, order: { created_at: 'DESC' }, }); // Enhance hubs with linked source information const enhancedHubs = await Promise.all( hubs.map(async (hub) => { try { const relatedConfig = await this.configRepository.findOne({ where: { id: hub.id }, }); if (!relatedConfig) { return { ...hub, linkedSource: 'Configuration not found', configDetails: null, }; } const linkedSource = this.extractLinkedSource( hub.integration_type, hub.integration_provider, relatedConfig.config_json, ); const configDetails = this.extractConfigDetails( hub.integration_type, hub.integration_provider, relatedConfig.config_json, ); return { ...hub, linkedSource, configDetails, }; } catch (error) { this.logger.warn( `Error extracting linked source for hub ${hub.id}:`, error.message, ); return { ...hub, linkedSource: 'Error retrieving source', configDetails: null, }; } }), ); return enhancedHubs; } private extractLinkedSource( configType: string, provider: string, configJson: any, ): string { const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`; try { switch (key) { // Gmail configurations case 'email_gmail': case 'email_smtp_gmail': return configJson?.email || 'Gmail account not configured'; // Outlook configurations case 'email_outlook': case 'email_smtp_outlook': return configJson?.email || 'Outlook account not configured'; // WhatsApp configurations case 'wa_api_whatsapp': return configJson?.phoneNumberId ? `WhatsApp Business: ${configJson.phoneNumberId}` : 'WhatsApp not configured'; // SMS configurations case 'sms_third_party_twilio': return configJson?.fromNumber ? `Twilio: ${configJson.fromNumber}` : 'Twilio number not configured'; case 'sms_third_party_knowlarity': return configJson?.callerNumber ? `Knowlarity: ${configJson.callerNumber}` : 'Knowlarity number not configured'; // Telephone configurations case 'telephone_third_party_knowlarity': return configJson?.callerNumber ? `Knowlarity Voice: ${configJson.callerNumber}` : 'Knowlarity voice number not configured'; case 'telephone_third_party_ozonetel': return configJson?.userName ? `Ozonetel Voice: ${configJson.userName}` : 'Ozonetel voice not configured'; // AWS SES configurations case 'email_aws-ses': case 'email_ses': return configJson?.fromEmail || 'AWS SES not configured'; // SendGrid configurations case 'email_smtp_sendgrid': return configJson?.from || 'SendGrid not configured'; // Generic SMTP configurations case 'email_smtp_custom': case 'email_smtp_generic': return configJson?.from || configJson?.user || 'SMTP not configured'; default: // Generic fallback - try to find common identifier fields if (configJson?.email) return configJson.email; if (configJson?.from) return configJson.from; if (configJson?.user) return configJson.user; if (configJson?.phoneNumberId) return configJson.phoneNumberId; if (configJson?.fromNumber) return configJson.fromNumber; if (configJson?.callerNumber) return configJson.callerNumber; return `${provider} configured`; } } catch (error) { return 'Configuration error'; } } private extractConfigDetails( configType: string, provider: string, configJson: any, ): any { const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`; try { switch (key) { // Gmail configurations case 'email_gmail': return { email: configJson?.email, authMethod: configJson?.authMethod || 'OAUTH2', hasRefreshToken: !!configJson?.refreshToken, isExpired: configJson?.expiryDate ? new Date(configJson.expiryDate) < new Date() : false, }; case 'email_smtp_gmail': return { email: configJson?.email, authMethod: 'SMTP', hasPassword: !!configJson?.password, }; // Outlook configurations case 'email_outlook': return { email: configJson?.email, authMethod: 'OAUTH2', hasRefreshToken: !!configJson?.refreshToken, tenantId: configJson?.tenantId, }; case 'email_smtp_outlook': return { email: configJson?.email, authMethod: 'SMTP', hasPassword: !!configJson?.password, }; // WhatsApp configurations case 'wa_api_whatsapp': return { phoneNumberId: configJson?.phoneNumberId, apiVersion: configJson?.apiVersion || 'v17.0', hasAccessToken: !!configJson?.accessToken, }; // SMS configurations case 'sms_third_party_twilio': return { fromNumber: configJson?.fromNumber, accountSid: configJson?.accountSid ? configJson.accountSid.substring(0, 8) + '...' : null, hasAuthToken: !!configJson?.authToken, }; case 'sms_third_party_knowlarity': return { callerNumber: configJson?.callerNumber, callType: configJson?.callType || 'sms', hasApiSecret: !!configJson?.apiSecret, }; // Telephone configurations case 'telephone_third_party_knowlarity': return { callerNumber: configJson?.callerNumber, callType: configJson?.callType || 'voice', language: configJson?.language || 'en', hasApiSecret: !!configJson?.apiSecret, }; case 'telephone_third_party_ozonetel': return { userName: configJson?.userName, agentID: configJson?.agentID, campaignName: configJson?.campaignName, agentLoginUrl: configJson?.agentLoginUrl, hasApiKey: !!configJson?.apiKey, }; // AWS SES configurations case 'email_aws-ses': case 'email_ses': return { fromEmail: configJson?.fromEmail, region: configJson?.region, hasCredentials: !!( configJson?.accessKeyId && configJson?.secretAccessKey ), }; // SendGrid configurations case 'email_smtp_sendgrid': return { from: configJson?.from, hasApiKey: !!configJson?.apiKey, templateId: configJson?.templateId, }; // Generic SMTP configurations case 'email_smtp_custom': case 'email_smtp_generic': return { host: configJson?.host, port: configJson?.port || 587, secure: configJson?.secure || false, from: configJson?.from || configJson?.user, hasCredentials: !!(configJson?.user && configJson?.password), }; default: { // Generic details - return safe subset of config const safeConfig: any = {}; // Include non-sensitive fields const safeFields = [ 'email', 'from', 'user', 'host', 'port', 'secure', 'phoneNumberId', 'fromNumber', 'callerNumber', 'region', 'apiVersion', 'callType', 'language', 'templateId', ]; safeFields.forEach((field) => { if (configJson?.[field]) { safeConfig[field] = configJson[field]; } }); // Include boolean indicators for sensitive fields const sensitiveFields = [ 'accessToken', 'refreshToken', 'password', 'authToken', 'apiKey', 'apiSecret', 'clientSecret', 'secretAccessKey', ]; sensitiveFields.forEach((field) => { if (configJson?.[field]) { safeConfig[ `has${field.charAt(0).toUpperCase() + field.slice(1)}` ] = true; } }); return safeConfig; } } } catch (error) { return { error: 'Unable to extract configuration details' }; } } async updateConfigStatus(hubId: number, status: number): Promise { // Find the hub to get level and type information const config = await this.configRepository.findOne({ where: { id: hubId }, }); if (!config) { throw new Error('Integration configuration not found'); } // If activating, deactivate ALL other configs of the same integration type if (status === 1) { await this.configRepository.update( { level_id: config.level_id, level_type: config.level_type, app_code: config.app_code, integration_type: config.integration_type, status: 1, id: Not(hubId), }, { status: 0 }, ); } // Update the requested config await this.configRepository.update(hubId, { status }); } async deleteConfiguration(configId: number): Promise { // Find the hub to get the id const config = await this.configRepository.findOne({ where: { id: configId }, }); if (!config) { throw new Error('Integration configuration not found'); } await this.configRepository.delete(configId); this.logger.log(`Integration configuration deleted: ${configId}`); } async updateConfiguration( hubId: number, updateData: { config?: any; priority?: number; is_default?: boolean; status?: number; }, ): Promise { // Find the existing hub const config = await this.configRepository.findOne({ where: { id: hubId }, }); if (!config) { throw new Error('Integration configuration not found'); } // Update configuration JSON if provided if (updateData.config) { // Merge the new config with existing config const updatedConfigJson = { ...config.config_json, ...updateData.config, }; await this.configRepository.update(config.id, { config_json: updatedConfigJson, }); } // Handle default configuration logic (simplified for now) if (updateData.is_default === true) { // Remove default from other configurations of same type and level await this.configRepository.update( { level_id: config.level_id, level_type: config.level_type, integration_type: config.integration_type, id: Not(hubId), }, { is_default: false }, ); } if (config.integration_type === 'TELEPHONE') { try { // Extract DID from config (could be did, callerNumber, or fromNumber) const did = config.config_json.did; if (did) { await this.entityMapperRepository.delete({ integration_config_id: config.id, }); const entityMapper = this.entityMapperRepository.create({ integration_config_id: config.id, level_id: String(config.level_id), level_type: config.level_type, appcode: config.app_code, did: did, campaign_name: config.config_json.campaignName || null, }); await this.entityMapperRepository.save(entityMapper); this.logger.log( `DID mapping created for TELEPHONE integration: ${did} for ${config.level_id} ${config.level_type}`, ); } } catch (error) { this.logger.warn( `Failed to create DID mapping for TELEPHONE integration: ${error.message}`, ); } } // Apply direct config updates if any const directUpdates: any = {}; if (updateData.priority !== undefined) directUpdates.priority = updateData.priority; if (updateData.is_default !== undefined) directUpdates.is_default = updateData.is_default; if (updateData.status !== undefined) directUpdates.status = updateData.status; if (Object.keys(directUpdates).length > 0) { await this.configRepository.update(config.id, directUpdates); } // Fetch and return updated config const updatedConfig = await this.configRepository.findOne({ where: { id: hubId }, }); return { ...updatedConfig, config: updatedConfig?.config_json, } as any; } async sendGenericMessage( messageDto: GenericMessageDto, ): Promise { try { const { levelId, levelType, app_code, to, message, type, priority = 'medium', html, cc, bcc, attachments, mediaUrl, templateId, user_id, entity_id, entity_type, organization_id, enterprise_id, mapped_entities, } = messageDto; let subject = messageDto.subject; // Auto-detect communication type if not specified const communicationType = this.detectCommunicationType(to, type); // Get active configs for the detected type const configs = await this.getActiveConfigs( levelId, levelType, app_code || 'DEFAULT', communicationType, ); if (!configs.length) { this.logger.warn( `No communication hubs found for ${levelType} ${levelId}. Please configure integration providers using the createIntegrationConfig method.`, ); throw new Error( `No active ${communicationType} configuration found for ${levelType} ${levelId}. Please configure a communication provider first.`, ); } // Sort hubs by priority and default preference const sortedConfigs = this.sortConfigsByPriorityAndDefault( configs, priority, ); // Prepare enhanced config with additional parameters const enhancedConfig = { subject, html, cc, bcc, attachments, mediaUrl, }; const loggedInUser = { id: user_id || null, organization_id: organization_id || -1, enterprise_id: enterprise_id || -1, level_id: levelId, level_type: levelType, app_code: app_code || 'DEFAULT', }; const subjectPrefix = this.configService.get('SUBJECT_PREFIX'); const profile = this.configService.get('PROFILE'); // Handle multiple recipients by sending to each individually if (Array.isArray(to)) { const results: IntegrationResult[] = []; let hasSuccess = false; for (const recipient of to) { for (const hub of sortedConfigs) { try { const serviceType = this.deriveServiceType( hub.integration_type, hub.integration_provider, hub.config_json, ); const strategy = this.integrationFactory.create( hub.integration_type, serviceType, hub.integration_provider, ); // Process template if provided let variables; let richText; let externalTemplateId: string | undefined = undefined; if (templateId) { const templateData = await this.processTemplate( parseInt(templateId, 10), entity_type, entity_id, loggedInUser, mapped_entities, ); variables = templateData.variables; externalTemplateId = templateData.externalTemplateId; richText = templateData.rich_text; subject = templateData.subject; } if (subjectPrefix) { const updatedSubject = profile?.charAt(0).toUpperCase() + profile?.slice(1) + ' : ' + subject; // << use current subject subject = updatedSubject; } // Merge config with enhanced parameters let finalConfig = { ...hub.config_json, ...enhancedConfig, templateId: externalTemplateId, variables, richText, subject, }; // Handle user integration if user_id provided if (user_id) { const userIntegrationData = await this.getUserIntegrationForStrategy(user_id, hub.id); if (userIntegrationData) { finalConfig = { ...finalConfig, external_user_id: userIntegrationData.external_user_id, }; } } const result = await strategy.sendMessage( recipient, message, finalConfig, ); if (result.success) { this.logger.log( `Generic message sent successfully via ${hub.integration_provider} to ${recipient}`, ); results.push(result); hasSuccess = true; break; // Move to next recipient } this.logger.warn( `Failed to send via ${hub.integration_provider} to ${recipient}: ${result.error}`, ); } catch (error) { this.logger.error( `Error sending via ${hub.integration_provider} to ${recipient}:`, error.message, ); continue; } } } if (hasSuccess) { return { success: true, messageId: results.map((r) => r.messageId).join(','), provider: 'multiple', service: 'multiple', timestamp: new Date(), message: results.map((r) => r.message).join(','), }; } } else { // Handle single recipient for (const hub of sortedConfigs) { try { const serviceType = this.deriveServiceType( hub.integration_type, hub.integration_provider, hub.config_json, ); const strategy = this.integrationFactory.create( hub.integration_type, serviceType, hub.integration_provider, ); // Process template if provided let variables; let externalTemplateId: string | undefined = undefined; let richText; if (templateId) { const templateData = await this.processTemplate( parseInt(templateId, 10), entity_type, entity_id, loggedInUser, mapped_entities, ); variables = templateData.variables; externalTemplateId = templateData.externalTemplateId; richText = templateData.rich_text; subject = templateData.subject; } if (subjectPrefix) { const updatedSubject = profile?.charAt(0).toUpperCase() + profile?.slice(1) + ' : ' + subject; // << use current subject subject = updatedSubject; } // Merge config with enhanced parameters let finalConfig = { ...hub.config_json, ...enhancedConfig, templateId: externalTemplateId, variables, richText, subject, }; // Handle user integration if user_id provided if (user_id) { const userIntegrationData = await this.getUserIntegrationForStrategy(user_id, hub.id); if (userIntegrationData) { finalConfig = { ...finalConfig, external_user_id: userIntegrationData.external_user_id, }; } } const result = await strategy.sendMessage(to, message, finalConfig); if (result.success) { this.logger.log( `Generic message sent successfully via ${hub.integration_provider} to ${to}`, ); return result; } this.logger.warn( `Failed to send via ${hub.integration_provider}: ${result.error}`, ); } catch (error) { this.logger.error( `Error sending via ${hub.integration_provider}:`, error.message, ); continue; } } } throw new Error('All communication providers failed'); } catch (error) { this.logger.error('Generic communication service error:', error.message); throw error; } } async sendBulkMessage( bulkDto: BulkMessageDto, ): Promise<{ results: IntegrationResult[]; summary: any }> { try { const { levelId, levelType, app_code, recipients, message, type, priority = 'low', subject, html, templateId, variables, batchSize = 10, } = bulkDto; const results: IntegrationResult[] = []; const batches = this.chunkArray(recipients, batchSize); for (let i = 0; i < batches.length; i++) { const batch = batches[i]; this.logger.log( `Processing batch ${i + 1}/${batches.length} with ${batch.length} recipients`, ); const batchPromises = batch.map(async (recipient) => { try { const messageDto: GenericMessageDto = { levelId, levelType, app_code, to: recipient, message, type, priority, subject, html, templateId, variables, }; return await this.sendGenericMessage(messageDto); } catch (error) { return { success: false, provider: 'unknown', service: 'unknown', error: error.message, timestamp: new Date(), } as IntegrationResult; } }); const batchResults = await Promise.allSettled(batchPromises); const processedResults = batchResults.map((result) => { if (result.status === 'fulfilled') { return result.value; } else { return { success: false, provider: 'unknown', service: 'unknown', error: result.reason?.message || 'Unknown error', timestamp: new Date(), } as IntegrationResult; } }); results.push(...processedResults); // Rate limiting delay between batches if (i < batches.length - 1) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } const summary = { total: results.length, successful: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length, successRate: ( (results.filter((r) => r.success).length / results.length) * 100 ).toFixed(2) + '%', }; this.logger.log( `Bulk message completed: ${summary.successful}/${summary.total} successful`, ); return { results, summary }; } catch (error) { this.logger.error('Bulk communication service error:', error.message); throw error; } } async scheduleMessage( scheduledDto: any, ): Promise<{ scheduled: boolean; scheduleId?: string }> { // For now, return a placeholder. In production, integrate with a job queue like Bull or Agenda this.logger.log(`Message scheduled for ${scheduledDto.scheduleFor}`); return { scheduled: true, scheduleId: `sched_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, }; } async sendTemplateMessage(templateDto: any): Promise { // Get template content (integrate with your template system) const template = await this.getTemplate(templateDto.templateId); const processedMessage = this.replaceTemplateVariables( template.content, templateDto.variables, ); const processedSubject = template.subject ? this.replaceTemplateVariables(template.subject, templateDto.variables) : undefined; const messageDto: GenericMessageDto = { levelId: templateDto.levelId, levelType: templateDto.levelType, app_code: templateDto.app_code, to: templateDto.to, message: processedMessage, subject: processedSubject, type: templateDto.type, templateId: templateDto.templateId, variables: templateDto.variables, }; return this.sendGenericMessage(messageDto); } private detectCommunicationType( to: string | string[], type?: string, ): 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE' { if (type) { return type as 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE'; } const recipient = Array.isArray(to) ? to[0] : to; // Email detection if (recipient.includes('@')) { return 'EMAIL'; } // Phone number detection (basic) if (/^\+?[\d\s\-\(\)]+$/.test(recipient)) { // Could be SMS or WhatsApp, default to SMS return 'SMS'; } // Default to EMAIL if unsure return 'EMAIL'; } private sortConfigsByPriorityAndDefault( configs: IntegrationConfigWithConfig[], _priority: string, ): IntegrationConfigWithConfig[] { return configs.sort((a, b) => { // First sort by default (true comes first) if (a.is_default !== b.is_default) { return b.is_default ? 1 : -1; } // Then sort by priority (lower number = higher priority) if (a.priority !== b.priority) { return a.priority - b.priority; } // Finally sort by created_at descending (newest first) return ( new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); }); } public async processTemplate( templateId: number, entity_type: any, entity_id: any, loggedInUser?: any, mappedEntities?: Record, ): Promise<{ variables?: Record; externalTemplateId?: string; rich_text?: string; subject?: string; }> { const commTemplate: any = await this.entityService.getEntityData( COMM_TEMPLATE, templateId, {} as any, ); if (!commTemplate) { return { variables: {}, externalTemplateId: undefined, }; } let variables = {}; if (commTemplate.mapper_id) { variables = await this.fieldMapperService.resolveData( commTemplate.mapper_id, 'LOOKUP', entity_type, entity_id, loggedInUser, {} as any, mappedEntities, ); } if (!commTemplate.template_id) { let richText = commTemplate.rich_text; if (!richText) { if (commTemplate.markup_id) { let url = await this.mediaService.getMediaDownloadUrl( commTemplate.markup_id, {}, 60000, ); if (url) { let response = await axios.get(url.signedUrl, { responseType: 'text', }); richText = response.data; } } } richText = richText.replace(/\{\{(\w+)\}\}/g, (match, key) => { const value = variables[key]; if (value === undefined) return match; // If value is an object with signedUrl, use only the signedUrl if ( typeof value === 'object' && value !== null && 'signedUrl' in value ) { return String(value.signedUrl); } return String(value); }); return { rich_text: richText, subject: commTemplate.subject, }; } return { variables, externalTemplateId: commTemplate.template_id, }; } private replaceTemplateVariables( template: string, variables: Record, ): string { return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { const value = variables[key]; return value !== undefined ? String(value) : match; }); } private async getTemplate(templateId: string) { // Placeholder for template system integration // In production, this would connect to your template database/service return { id: templateId, subject: 'Default Subject {{variable}}', content: 'Hello {{name}}, this is a template message with {{variable}}.', }; } private chunkArray(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } async initGmailOAuth( levelId: number, levelType: string, app_code: string, email?: string, ): Promise<{ authUrl: string; state: string }> { try { // Get system OAuth credentials from config const clientId = this.configService.get('CLIENT_ID'); const clientSecret = this.configService.get('CLIENT_SECRET'); if (!clientId || !clientSecret) { throw new Error('Gmail OAuth credentials not configured'); } const state = this.generateSecureState(); // Use the existing registered callback URL const callbackUrl = this.configService.get('CALLBACK_URL') || 'http://localhost:5001/auth/google/callback'; this.gmailOAuthStates.set(state, { levelId, levelType, app_code, email, timestamp: Date.now(), }); // Auto-cleanup after 10 minutes setTimeout( () => { this.gmailOAuthStates.delete(state); }, 10 * 60 * 1000, ); const oauth2Client = new google.auth.OAuth2( clientId, clientSecret, callbackUrl, ); const scopes = [ 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/calendar', ]; const authUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, state: `gmail_config:${state}`, // Prefix to identify Gmail config requests prompt: 'consent', login_hint: email, }); return { authUrl, state }; } catch (error) { this.logger.error('Error initializing Gmail OAuth:', error.message); throw new Error('Failed to initialize Gmail OAuth'); } } async handleGmailOAuthCallback( code: string, state: string, ): Promise { try { const oauthState = this.gmailOAuthStates.get(state); if (!oauthState) { throw new Error('Invalid or expired OAuth state'); } this.gmailOAuthStates.delete(state); if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) { throw new Error('OAuth state expired'); } // Get system OAuth credentials const clientId = this.configService.get('CLIENT_ID'); const clientSecret = this.configService.get('CLIENT_SECRET'); const callbackUrl = this.configService.get('CALLBACK_URL') || 'http://localhost:5001/auth/google/callback'; const oauth2Client = new google.auth.OAuth2( clientId, clientSecret, callbackUrl, ); const { tokens } = await oauth2Client.getToken(code); if (!tokens.access_token) { throw new Error('Failed to obtain access token'); } // Get user email from Google oauth2Client.setCredentials(tokens); const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); const userInfo = await oauth2.userinfo.get(); const email = userInfo.data.email; console.log('userInfo', userInfo); const fromName = userInfo.data.name || email; if (!email) { throw new Error('Failed to get user email'); } // Verify email matches if hint was provided if (oauthState.email && oauthState.email !== email) { throw new Error('Email mismatch with OAuth hint'); } const gmailConfig = { clientId, clientSecret, email: email, fromName, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, scope: tokens.scope, tokenType: tokens.token_type, expiryDate: tokens.expiry_date, }; // Validate that no active EMAIL configuration exists await this.validateUniqueActiveConfig( oauthState.levelId, oauthState.levelType, oauthState.app_code, 'EMAIL', 'gmail', ); // Create integration config const config = this.configRepository.create({ app_code: oauthState.app_code, integration_type: 'EMAIL', integration_provider: 'gmail', integration_source_id: 1, // Gmail source ID level_id: oauthState.levelId, level_type: oauthState.levelType, status: 1, priority: 1, is_default: false, config_json: gmailConfig as any, }); const savedConfig = await this.configRepository.save(config); this.logger.log( `Gmail OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`, ); return { hubId: savedConfig.id, configId: savedConfig.id, }; } catch (error) { this.logger.error('Error handling Gmail OAuth callback:', error.message); throw new Error(`Failed to complete Gmail OAuth: ${error.message}`); } } async handleGmailTokensCallback( email: string, accessToken: string, refreshToken: string, state: string, name?: { displayName?: string; givenName?: string; familyName?: string }, ): Promise { try { const oauthState = this.gmailOAuthStates.get(state); if (!oauthState) { throw new Error('Invalid or expired OAuth state'); } this.gmailOAuthStates.delete(state); if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) { throw new Error('OAuth state expired'); } // Verify email matches if hint was provided if (oauthState.email && oauthState.email !== email) { throw new Error('Email mismatch with OAuth hint'); } // Validate that no active EMAIL configuration exists await this.validateUniqueActiveConfig( oauthState.levelId, oauthState.levelType, oauthState.app_code, 'EMAIL', 'gmail', ); const displayName = name?.displayName || (name?.givenName && name?.familyName ? `${name?.givenName} ${name?.familyName}` : undefined) || email; // Pure SSO configuration - no client credentials stored per user const gmailConfig = { email: email, fromName: displayName, accessToken: accessToken, refreshToken: refreshToken, authMethod: 'GOOGLE_SSO', scopes: [ 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email', ], }; // Create integration config const config = this.configRepository.create({ app_code: oauthState.app_code, integration_type: 'EMAIL', integration_provider: 'gmail', integration_source_id: 1, // Gmail source ID level_id: oauthState.levelId, level_type: oauthState.levelType, status: 1, priority: 1, is_default: false, config_json: gmailConfig as any, }); const savedConfig = await this.configRepository.save(config); this.logger.log( `Gmail tokens configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`, ); return { hubId: savedConfig.id, configId: savedConfig.id, }; } catch (error) { this.logger.error('Error handling Gmail tokens callback:', error.message); throw new Error( `Failed to complete Gmail tokens callback: ${error.message}`, ); } } async testGmailConfig( hubId: number, ): Promise<{ success: boolean; error?: string }> { try { const integrationConfig = await this.configRepository.findOne({ where: { id: hubId }, }); if (!integrationConfig) { throw new Error('Integration config not found'); } const isValid = await this.gmailApiStrategy.validateConnection( integrationConfig.config_json, ); if (!isValid) { return { success: false, error: 'Gmail configuration is invalid or expired', }; } return { success: true }; } catch (error) { this.logger.error('Error testing Gmail config:', error.message); return { success: false, error: error.message }; } } private generateSecureState(): string { return Math.random().toString(36).substring(2) + Date.now().toString(36); } private async validateUniqueActiveConfig( levelId: number, levelType: string, app_code: string, configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', provider: string, excludeHubId?: number, ): Promise { // Find all active configurations of the same provider for this level and app_code const query = this.configRepository .createQueryBuilder('hub') .where('hub.level_id = :levelId', { levelId }) .andWhere('hub.level_type = :levelType', { levelType }) .andWhere('hub.app_code = :app_code', { app_code }) .andWhere('hub.integration_type = :configType', { configType }) .andWhere('hub.integration_provider = :provider', { provider }) .andWhere('hub.status = :status', { status: 1 }); // Exclude current hub if updating if (excludeHubId) { query.andWhere('hub.id != :excludeHubId', { excludeHubId }); } const existingActiveConfigs = await query.getMany(); // If there are existing active configurations of the same provider, throw error if (existingActiveConfigs.length > 0) { throw new Error( `A ${provider} configuration already exists for ${configType} in app_code ${app_code}, ${levelType} ${levelId}. Only one configuration per provider is allowed.`, ); } } private requiresOAuthFlow( configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', provider: string, config: any, ): boolean { // Check if config indicates OAuth is needed (missing tokens or explicit OAuth request) const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`; switch (key) { case 'email_gmail': // Requires OAuth if no access token provided or OAuth explicitly requested return !config.accessToken || config.useOAuth === true; case 'email_outlook': // Requires OAuth if no access token provided or OAuth explicitly requested return !config.accessToken || config.useOAuth === true; default: // Most other providers don't require OAuth return false; } } private async generateOAuthUrl( levelId: number, levelType: string, app_code: string, configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', provider: string, integration_source_id: number, config: any, priority?: number, is_default?: boolean, ): Promise<{ authUrl: string; state: string; message: string }> { const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`; switch (key) { case 'email_gmail': const gmailResult = await this.initGmailOAuth( levelId, levelType, app_code, config.email, ); return { authUrl: gmailResult.authUrl, state: gmailResult.state, message: 'Please complete Gmail OAuth authorization. Configuration will be created automatically after authorization.', }; case 'email_outlook': const outlookResult = await this.initOutlookOAuth( levelId, levelType, app_code, config.email, ); return { authUrl: outlookResult.authUrl, state: outlookResult.state, message: 'Please complete Outlook OAuth authorization. Configuration will be created automatically after authorization.', }; default: throw new Error(`OAuth not supported for ${configType}/${provider}`); } } private async createDirectConfig( levelId: number, levelType: string, app_code: string, configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', provider: string, integration_source_id: number, config: any, priority?: number, is_default?: boolean, ): Promise { // Validate that no duplicate provider configurations exist await this.validateUniqueActiveConfig( levelId, levelType, app_code, configType, provider, ); // Deactivate all other configurations of the same integration type await this.configRepository.update( { level_id: levelId, level_type: levelType, app_code: app_code, integration_type: configType, status: 1, }, { status: 0 }, ); // If setting as default, remove default from other configurations of same type and app_code if (is_default) { await this.configRepository.update( { level_id: levelId, level_type: levelType, app_code: app_code, integration_type: configType, }, { is_default: false }, ); } // Create integration config const integrationConfig = this.configRepository.create({ app_code: app_code, integration_type: configType, integration_provider: provider, integration_source_id: integration_source_id, level_id: levelId, level_type: levelType, status: 1, priority: priority || 1, is_default: is_default || false, config_json: config, }); const savedConfig = await this.configRepository.save(integrationConfig); this.logger.log( `Communication config created: ${configType}/${provider} for ${levelType} ${levelId}`, ); // Store DID mapping for TELEPHONE integration if (configType === 'TELEPHONE') { try { // Extract DID from config (could be did, callerNumber, or fromNumber) const did = config.did || config.callerNumber || config.fromNumber; if (did) { const entityMapper = this.entityMapperRepository.create({ integration_config_id: savedConfig.id, level_id: String(levelId), level_type: levelType, appcode: app_code, did: did, campaign_name: config.campaignName || null, }); await this.entityMapperRepository.save(entityMapper); this.logger.log( `DID mapping created for TELEPHONE integration: ${did} for ${levelType} ${levelId}`, ); } } catch (error) { this.logger.warn( `Failed to create DID mapping for TELEPHONE integration: ${error.message}`, ); } } return Array.isArray(savedConfig) ? savedConfig[0] : savedConfig; } async initOutlookOAuth( levelId: number, levelType: string, app_code: string, email?: string, ): Promise<{ authUrl: string; state: string }> { try { // Get system OAuth credentials from config const clientId = this.configService.get('OUTLOOK_CLIENT_ID'); const tenantId = this.configService.get('OUTLOOK_TENANT_ID'); if (!clientId || !tenantId) { throw new Error('Outlook OAuth credentials not configured'); } const state = this.generateSecureState(); const callbackUrl = this.configService.get('OUTLOOK_CALLBACK_URL') || 'http://localhost:5001/auth/outlook/callback'; this.gmailOAuthStates.set(state, { levelId, levelType, app_code, email, timestamp: Date.now(), }); // Auto-cleanup after 10 minutes setTimeout( () => { this.gmailOAuthStates.delete(state); }, 10 * 60 * 1000, ); const scopes = [ 'https://graph.microsoft.com/Mail.Send', 'https://graph.microsoft.com/User.Read', ]; const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` + `client_id=${clientId}&` + `response_type=code&` + `redirect_uri=${encodeURIComponent(callbackUrl)}&` + `scope=${encodeURIComponent(scopes.join(' '))}&` + `state=outlook_config:${state}&` + `response_mode=query&` + `prompt=consent` + (email ? `&login_hint=${encodeURIComponent(email)}` : ''); return { authUrl, state }; } catch (error) { this.logger.error('Error initializing Outlook OAuth:', error.message); throw new Error('Failed to initialize Outlook OAuth'); } } async handleOutlookOAuthCallback( code: string, state: string, ): Promise { try { const oauthState = this.gmailOAuthStates.get(state); if (!oauthState) { throw new Error('Invalid or expired OAuth state'); } this.gmailOAuthStates.delete(state); if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) { throw new Error('OAuth state expired'); } const clientId = this.configService.get('OUTLOOK_CLIENT_ID'); const clientSecret = this.configService.get( 'OUTLOOK_CLIENT_SECRET', ); const tenantId = this.configService.get('OUTLOOK_TENANT_ID'); const callbackUrl = this.configService.get('OUTLOOK_CALLBACK_URL') || 'http://localhost:5001/auth/outlook/callback'; // Exchange code for tokens const tokenResponse = await fetch( `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId!, client_secret: clientSecret!, code: code, redirect_uri: callbackUrl, grant_type: 'authorization_code', }), }, ); const tokens = await tokenResponse.json(); if (!tokens.access_token) { throw new Error('Failed to obtain access token'); } // Get user info from Microsoft Graph const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { Authorization: `Bearer ${tokens.access_token}`, }, }); const userInfo = await userResponse.json(); const email = userInfo.mail || userInfo.userPrincipalName; if (!email) { throw new Error('Failed to get user email'); } // Validate that no active EMAIL configuration exists await this.validateUniqueActiveConfig( oauthState.levelId, oauthState.levelType, oauthState.app_code, 'EMAIL', 'outlook', ); const outlookConfig = { clientId, tenantId, email: email, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, scope: tokens.scope, tokenType: tokens.token_type, expiresIn: tokens.expires_in, }; // Create integration config const config = this.configRepository.create({ app_code: oauthState.app_code, integration_type: 'EMAIL', integration_provider: 'outlook', integration_source_id: 1, // Outlook source ID level_id: oauthState.levelId, level_type: oauthState.levelType, status: 1, priority: 1, is_default: false, config_json: outlookConfig as any, }); const savedConfig = await this.configRepository.save(config); this.logger.log( `Outlook OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`, ); return { hubId: savedConfig.id, configId: savedConfig.id, }; } catch (error) { this.logger.error( 'Error handling Outlook OAuth callback:', error.message, ); throw new Error(`Failed to complete Outlook OAuth: ${error.message}`); } } async getIntegrationConfigById(hubId: number): Promise< | (IntegrationConfig & { config: IntegrationConfig; linkedSource?: string; configDetails?: any; }) | null > { try { // Find the integration config by ID const integrationConfig = await this.configRepository.findOne({ where: { id: hubId }, }); if (!integrationConfig) { return null; } // Extract linked source and config details const linkedSource = this.extractLinkedSource( integrationConfig.integration_type, integrationConfig.integration_provider, integrationConfig.config_json, ); const configDetails = this.extractConfigDetails( integrationConfig.integration_type, integrationConfig.integration_provider, integrationConfig.config_json, ); return { ...integrationConfig, config: integrationConfig, linkedSource, configDetails, }; } catch (error) { this.logger.error( `Error fetching communication config by ID ${hubId}:`, error.message, ); throw new Error( `Failed to fetch communication configuration: ${error.message}`, ); } } async getIntegrationTemplates( levelId: number, levelType: string, app_code: string, integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE', ): Promise<{ success: boolean; data?: Array<{ label: string; value: string; }>; error?: string; }> { try { // Get active configuration for the integration type const config = await this.getSingleActiveConfig( levelId, levelType, app_code, integration_type, ); if (!config) { return { success: false, error: `No active ${integration_type} configuration found for this level`, }; } // Create strategy instance const strategy = this.integrationFactory.create( config.integration_type, 'API', config.integration_provider, ); // Check if strategy supports getTemplates if (typeof (strategy as any).getTemplates !== 'function') { return { success: false, error: `Template retrieval not supported for provider: ${config.integration_provider}`, }; } // Call getTemplates method return await (strategy as any).getTemplates(config.config_json); } catch (error) { this.logger.error( `Error fetching templates for ${integration_type} level ${levelId}/${levelType}:`, error.message, ); return { success: false, error: `Failed to fetch templates: ${error.message}`, }; } } async getSendGridTemplates( levelId: number, levelType: string, app_code: string, ): Promise<{ success: boolean; data?: Array<{ label: string; value: string; }>; error?: string; }> { try { // Find active SendGrid configurations for this level const hubs = await this.getActiveConfigs( levelId, levelType, app_code, 'EMAIL', ); // Look for SendGrid provider const sendGridHub = hubs.find( (hub) => hub.integration_provider === 'sendgrid', ); if (!sendGridHub) { return { success: false, error: 'No active SendGrid configuration found for this level', }; } // Extract API key from configuration const apiKey = sendGridHub.config_json?.apiKey; if (!apiKey) { return { success: false, error: 'SendGrid API key not found in configuration', }; } // Use the SendGrid strategy to fetch templates return this.sendGridApiStrategy.getTemplates(apiKey); } catch (error) { this.logger.error( `Error fetching SendGrid templates for level ${levelId}/${levelType}:`, error.message, ); return { success: false, error: `Failed to fetch SendGrid templates: ${error.message}`, }; } } async getSendGridVerifiedSenders(apiKey: string): Promise<{ success: boolean; data?: Array<{ label: string; value: string; }>; error?: string; }> { try { if (!apiKey) { return { success: false, error: 'SendGrid API key not found in configuration', }; } // Use the SendGrid strategy to fetch verified senders return this.sendGridApiStrategy.getVerifiedSenders(apiKey); } catch (error) { this.logger.error( `Error fetching SendGrid verified senders`, error.message, ); return { success: false, error: `Failed to fetch SendGrid verified senders: ${error.message}`, }; } } // UserIntegration Management Methods async createUserIntegration( createDto: CreateUserIntegrationDto, ): Promise { try { // Check if mapping already exists const existing = await this.userIntegrationRepository.findOne({ where: { user_id: createDto.user_id, integration_config_id: createDto.integration_config_id, }, }); if (existing) { throw new Error('User integration mapping already exists'); } // Verify integration config exists const config = await this.configRepository.findOne({ where: { id: createDto.integration_config_id }, }); if (!config) { throw new Error('Integration configuration not found'); } const userIntegration = this.userIntegrationRepository.create(createDto); return await this.userIntegrationRepository.save(userIntegration); } catch (error) { this.logger.error( `Error creating user integration mapping: ${error.message}`, ); throw error; } } async bulkCreateUserIntegration( bulkDto: BulkCreateUserIntegrationDto, ): Promise<{ created: UserIntegration[]; updated: UserIntegration[]; failed: Array<{ data: CreateUserIntegrationDto; error: string }>; summary: { total: number; created: number; updated: number; failed: number; }; }> { const created: UserIntegration[] = []; const updated: UserIntegration[] = []; const failed: Array<{ data: CreateUserIntegrationDto; error: string }> = []; try { for (const createDto of bulkDto.user_integrations) { try { // Check if mapping already exists const existing = await this.userIntegrationRepository.findOne({ where: { user_id: createDto.user_id, integration_config_id: createDto.integration_config_id, }, }); if (existing) { // Update existing mapping if (createDto.external_user_id !== undefined) { existing.external_user_id = createDto.external_user_id; } if (createDto.external_user_data !== undefined) { existing.external_user_data = createDto.external_user_data; } if (createDto.is_active !== undefined) { existing.is_active = createDto.is_active; } const updatedIntegration = await this.userIntegrationRepository.save(existing); updated.push(updatedIntegration); this.logger.log( `Updated user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`, ); } else { // Verify integration config exists const config = await this.configRepository.findOne({ where: { id: createDto.integration_config_id }, }); if (!config) { failed.push({ data: createDto, error: 'Integration configuration not found', }); continue; } // Create new mapping const userIntegration = this.userIntegrationRepository.create(createDto); const savedIntegration = await this.userIntegrationRepository.save(userIntegration); created.push(savedIntegration); this.logger.log( `Created user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`, ); } } catch (error) { failed.push({ data: createDto, error: error.message || 'Unknown error', }); this.logger.error( `Error processing user integration for user ${createDto.user_id}: ${error.message}`, ); } } const summary = { total: bulkDto.user_integrations.length, created: created.length, updated: updated.length, failed: failed.length, }; this.logger.log( `Bulk user integration completed: ${summary.created} created, ${summary.updated} updated, ${summary.failed} failed`, ); return { created, updated, failed, summary }; } catch (error) { this.logger.error( `Error in bulk user integration creation: ${error.message}`, ); throw error; } } async getUserIntegrations(userId: number): Promise { try { return await this.userIntegrationRepository.find({ where: { user_id: userId, is_active: true }, order: { created_at: 'DESC' }, }); } catch (error) { this.logger.error( `Error fetching user integrations for user ${userId}: ${error.message}`, ); throw error; } } async getConfigUserIntegrations( configId: number, ): Promise { try { return await this.userIntegrationRepository.find({ where: { integration_config_id: configId, is_active: true }, order: { created_at: 'DESC' }, }); } catch (error) { this.logger.error( `Error fetching user integrations for config ${configId}: ${error.message}`, ); throw error; } } async getUserIntegrationByUserAndConfig( userId: number, configId: number, ): Promise { try { return await this.userIntegrationRepository.findOne({ where: { user_id: userId, integration_config_id: configId, is_active: true, }, }); } catch (error) { this.logger.error( `Error fetching user integration for user ${userId} and config ${configId}: ${error.message}`, ); throw error; } } async updateUserIntegration( id: number, updateDto: UpdateUserIntegrationDto, ): Promise { try { const userIntegration = await this.userIntegrationRepository.findOne({ where: { id }, }); if (!userIntegration) { throw new Error('User integration mapping not found'); } Object.assign(userIntegration, updateDto); return await this.userIntegrationRepository.save(userIntegration); } catch (error) { this.logger.error( `Error updating user integration ${id}: ${error.message}`, ); throw error; } } async deleteUserIntegration(id: number): Promise { try { const userIntegration = await this.userIntegrationRepository.findOne({ where: { id }, }); if (!userIntegration) { throw new Error('User integration mapping not found'); } await this.userIntegrationRepository.remove(userIntegration); } catch (error) { this.logger.error( `Error deleting user integration ${id}: ${error.message}`, ); throw error; } } /** * Get user integration data for a specific integration strategy * This method is intended to be used by strategies that need user mapping */ async getUserIntegrationForStrategy( userId: number, integrationConfigId: number, ): Promise { try { const userIntegration = await this.getUserIntegrationByUserAndConfig( userId, integrationConfigId, ); if (!userIntegration) { return null; } return { external_user_id: userIntegration.external_user_id, external_user_data: userIntegration.external_user_data, }; } catch (error) { this.logger.error( `Error fetching user integration data for strategy: ${error.message}`, ); return null; } } /** * Check agent status for TELEPHONE integration * Works with any provider that implements checkAgentStatus method */ async checkAgentStatus( levelId: number, levelType: string, appCode: string, userId: number, integrationType: 'TELEPHONE' = 'TELEPHONE', ): Promise<{ success: boolean; isReady?: boolean; state?: string; agentInfo?: any; error?: string; }> { try { // Get the active configuration const config = await this.getSingleActiveConfig( levelId, levelType, appCode, integrationType, ); if (!config) { return { success: false, error: 'No active TELEPHONE configuration found', }; } // Get user integration mapping const userIntegration = await this.getUserIntegrationByUserAndConfig( userId, config.id, ); if (!userIntegration) { return { success: false, error: 'User integration mapping not found. Please configure agent details.', }; } // Create strategy instance const strategy = this.integrationFactory.create( config.integration_type, 'API', // service is deprecated, using default config.integration_provider, ); // Get the merged config with user data let mergedConfig = config.config_json; if (strategy.createUserSpecificConfig) { mergedConfig = strategy.createUserSpecificConfig( config.config_json, userIntegration.external_user_id, ); } // Check if strategy supports agent status check if (typeof (strategy as any).checkAgentStatus !== 'function') { return { success: false, error: `Agent status check not supported for provider: ${config.integration_provider}`, }; } // Check agent status const statusResult = await (strategy as any).checkAgentStatus( mergedConfig, ); return { success: true, isReady: statusResult.isReady, state: statusResult.state, error: statusResult.error, }; } catch (error) { this.logger.error( `Error checking agent status: ${error.message}`, error.stack, ); return { success: false, error: error.message || 'Failed to check agent status', }; } } }