import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; @Injectable() export class OzonetelAgentService { private readonly logger = new Logger(OzonetelAgentService.name); private readonly apiDomain: string; private readonly apiKey: string; private readonly accountId: string; private cachedToken: string | null = null; private tokenExpiry: number = 0; constructor(private config: ConfigService) { this.apiDomain = config.get('exotel.subdomain') ?? 'in1-ccaas-api.ozonetel.com'; this.apiKey = config.get('exotel.apiKey') ?? ''; this.accountId = config.get('exotel.accountSid') ?? ''; } private async getToken(): Promise { if (this.cachedToken && Date.now() < this.tokenExpiry) { return this.cachedToken; } return this.refreshToken(); } async refreshToken(): Promise { const url = `https://${this.apiDomain}/ca_apis/CAToken/generateToken`; this.logger.log('Generating CloudAgent API token'); const response = await axios.post( url, { userName: this.accountId }, { headers: { apiKey: this.apiKey, 'Content-Type': 'application/json' }, }, ); const data = response.data; if (data.token) { this.cachedToken = data.token; this.tokenExpiry = Date.now() + 10 * 60 * 1000; // 10 min cache (Ozonetel expires in ~15 min) this.logger.log('CloudAgent token generated successfully'); return data.token; } throw new Error(data.message ?? 'Token generation failed'); } private invalidateToken(): void { this.cachedToken = null; this.tokenExpiry = 0; } async loginAgent(params: { agentId: string; password: string; phoneNumber: string; mode?: string; }): Promise<{ status: string; message: string }> { const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; this.logger.log( `Logging in agent ${params.agentId} with phone ${params.phoneNumber}`, ); try { const response = await axios.post( url, new URLSearchParams({ userName: this.accountId, apiKey: this.apiKey, phoneNumber: params.phoneNumber, action: 'login', mode: params.mode ?? 'blended', state: 'Ready', }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, auth: { username: params.agentId, password: params.password, }, }, ); const data = response.data; // "already logged in" — force logout + re-login to refresh SIP phone mapping if ( data.status === 'error' && data.message?.includes('already logged in') ) { this.logger.log( `Agent ${params.agentId} already logged in — forcing logout + re-login`, ); try { await this.logoutAgent({ agentId: params.agentId, password: params.password, }); const retryResponse = await axios.post( url, new URLSearchParams({ userName: this.accountId, apiKey: this.apiKey, phoneNumber: params.phoneNumber, action: 'login', mode: params.mode ?? 'blended', state: 'Ready', }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, auth: { username: params.agentId, password: params.password }, }, ); this.logger.log( `Agent re-login response: ${JSON.stringify(retryResponse.data)}`, ); return retryResponse.data; } catch (retryErr: any) { this.logger.error(`Agent re-login failed: ${retryErr.message}`); return { status: 'success', message: 'Re-login attempted' }; } } this.logger.log(`Agent login response: ${JSON.stringify(data)}`); return data; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); this.logger.error(`Agent login failed: ${error.message}`); throw error; } } async manualDial(params: { agentId: string; campaignName: string; customerNumber: string; }): Promise<{ status: string; ucid?: string; message?: string }> { const url = `https://${this.apiDomain}/ca_apis/AgentManualDial`; this.logger.log( `Manual dial: agent=${params.agentId} campaign=${params.campaignName} number=${params.customerNumber}`, ); try { const token = await this.getToken(); const response = await axios.post( url, { userName: this.accountId, agentID: params.agentId, campaignName: params.campaignName, customerNumber: params.customerNumber, UCID: 'true', }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }, ); this.logger.log(`Manual dial response: ${JSON.stringify(response.data)}`); return response.data; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error(`Manual dial failed: ${error.message} ${responseData}`); throw error; } } async changeAgentState(params: { agentId: string; state: 'Ready' | 'Pause'; pauseReason?: string; }): Promise<{ status: string; message: string }> { const url = `https://${this.apiDomain}/ca_apis/changeAgentState`; this.logger.log( `Changing agent ${params.agentId} state to ${params.state}`, ); try { const body: Record = { userName: this.accountId, agentId: params.agentId, state: params.state, }; if (params.pauseReason) { body.pauseReason = params.pauseReason; } const token = await this.getToken(); const response = await axios.post(url, body, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); this.logger.log( `Change agent state response: ${JSON.stringify(response.data)}`, ); return response.data; } catch (error: any) { const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error( `Change agent state failed: ${error.message} ${responseData}`, ); throw error; } } async setDisposition(params: { agentId: string; ucid: string; disposition: string; }): Promise<{ status: string; message?: string; details?: string }> { const url = `https://${this.apiDomain}/ca_apis/DispositionAPIV2`; const did = process.env.OZONETEL_DID ?? '918041763265'; this.logger.log( `Set disposition: agent=${params.agentId} ucid=${params.ucid} disposition=${params.disposition}`, ); try { const token = await this.getToken(); const response = await axios.post( url, { userName: this.accountId, agentID: params.agentId, did, ucid: params.ucid, action: 'Set', disposition: params.disposition, autoRelease: 'true', }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }, ); this.logger.log( `Set disposition response: ${JSON.stringify(response.data)}`, ); return response.data; } catch (error: any) { const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error( `Set disposition failed: ${error.message} ${responseData}`, ); throw error; } } async callControl(params: { action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL'; ucid: string; conferenceNumber?: string; }): Promise<{ status: string; message: string; ucid?: string }> { const url = `https://${this.apiDomain}/ca_apis/CallControl_V4`; const did = process.env.OZONETEL_DID ?? '918041763265'; const agentPhoneName = process.env.OZONETEL_SIP_ID ?? '523590'; this.logger.log( `Call control: action=${params.action} ucid=${params.ucid} conference=${params.conferenceNumber ?? 'none'}`, ); try { const token = await this.getToken(); const body: Record = { userName: this.accountId, action: params.action, ucid: params.ucid, did, agentPhoneName, }; if (params.conferenceNumber) { body.conferenceNumber = params.conferenceNumber; } const response = await axios.post(url, body, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); this.logger.log( `Call control response: ${JSON.stringify(response.data)}`, ); return response.data; } catch (error: any) { const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error( `Call control failed: ${error.message} ${responseData}`, ); throw error; } } async pauseRecording(params: { ucid: string; action: 'pause' | 'unPause'; }): Promise<{ status: string; message: string }> { const url = `https://${this.apiDomain}/CAServices/Call/Record.php`; this.logger.log(`Recording ${params.action}: ucid=${params.ucid}`); try { const response = await axios.get(url, { params: { userName: this.accountId, apiKey: this.apiKey, action: params.action, ucid: params.ucid, }, }); this.logger.log( `Recording control response: ${JSON.stringify(response.data)}`, ); return response.data; } catch (error: any) { const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error( `Recording control failed: ${error.message} ${responseData}`, ); throw error; } } async getAbandonCalls(params?: { fromTime?: string; toTime?: string; campaignName?: string; }): Promise< Array<{ monitorUCID: string; type: string; status: string; campaign: string; callerID: string; did: string; agentID: string; agent: string; hangupBy: string; callTime: string; }> > { const url = `https://${this.apiDomain}/ca_apis/abandonCalls`; this.logger.log('Fetching abandon calls'); try { const token = await this.getToken(); const body: Record = { userName: this.accountId }; if (params?.fromTime) body.fromTime = params.fromTime; if (params?.toTime) body.toTime = params.toTime; if (params?.campaignName) body.campaignName = params.campaignName; const response = await axios({ method: 'GET', url, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, data: JSON.stringify(body), }); const data = response.data; this.logger.log( `Abandon calls response: ${JSON.stringify(data).substring(0, 200)}`, ); if (data.status === 'success' && Array.isArray(data.message)) { return data.message; } return []; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); this.logger.error(`Abandon calls failed: ${error.message}`); return []; } } async fetchCDR(params: { date: string; // YYYY-MM-DD campaignName?: string; status?: string; callType?: string; }): Promise>> { const url = `https://${this.apiDomain}/ca_reports/fetchCDRDetails`; this.logger.log(`Fetch CDR: date=${params.date}`); try { const token = await this.getToken(); const body: Record = { userName: this.accountId, fromDate: `${params.date} 00:00:00`, toDate: `${params.date} 23:59:59`, }; if (params.campaignName) body.campaignName = params.campaignName; if (params.status) body.status = params.status; if (params.callType) body.callType = params.callType; const response = await axios({ method: 'GET', url, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, data: JSON.stringify(body), }); const data = response.data; if (data.status === 'success' && Array.isArray(data.details)) { return data.details; } return []; } catch (error: any) { const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; this.logger.error(`Fetch CDR failed: ${error.message} ${responseData}`); return []; } } async getAgentSummary( agentId: string, date: string, ): Promise<{ totalLoginDuration: string; totalBusyTime: string; totalIdleTime: string; totalPauseTime: string; totalWrapupTime: string; totalDialTime: string; } | null> { const url = `https://${this.apiDomain}/ca_reports/summaryReport`; try { const token = await this.getToken(); const response = await axios({ method: 'GET', url, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, data: JSON.stringify({ userName: this.accountId, agentId, fromDate: `${date} 00:00:00`, toDate: `${date} 23:59:59`, }), }); const data = response.data; if (data.status === 'success' && data.message) { const record = Array.isArray(data.message) ? data.message[0] : data.message; return { totalLoginDuration: record.TotalLoginDuration ?? '00:00:00', totalBusyTime: record.TotalBusyTime ?? '00:00:00', totalIdleTime: record.TotalIdleTime ?? '00:00:00', totalPauseTime: record.TotalPauseTime ?? '00:00:00', totalWrapupTime: record.TotalWrapupTime ?? '00:00:00', totalDialTime: record.TotalDialTime ?? '00:00:00', }; } return null; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); this.logger.error(`Agent summary failed: ${error.message}`); return null; } } async getAHT(agentId: string): Promise { const url = `https://${this.apiDomain}/ca_apis/aht`; try { const token = await this.getToken(); const response = await axios({ method: 'GET', url, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, data: JSON.stringify({ userName: this.accountId, agentId, }), }); const data = response.data; if (data.status === 'success') { return data.AHT ?? '00:00:00'; } return '00:00:00'; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); this.logger.error(`AHT failed: ${error.message}`); return '00:00:00'; } } async logoutAgent(params: { agentId: string; password: string; }): Promise<{ status: string; message: string }> { const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; this.logger.log(`Logging out agent ${params.agentId}`); try { const response = await axios.post( url, new URLSearchParams({ userName: this.accountId, apiKey: this.apiKey, action: 'logout', mode: 'blended', state: 'Ready', }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, auth: { username: params.agentId, password: params.password, }, }, ); this.logger.log( `Agent logout response: ${JSON.stringify(response.data)}`, ); return response.data; } catch (error: any) { if (error?.response?.status === 401) this.invalidateToken(); this.logger.error(`Agent logout failed: ${error.message}`); throw error; } } }