fix: await Ozonetel logout + per-agent sipPassword + campaign name on missed calls

Three changes:

1. Await Ozonetel logout in /auth/logout — prevents race condition when
   agent re-logs in quickly via "Remember me". The fire-and-forget
   logoutAgent() left a window where the next loginAgent() arrived
   while Ozonetel was still processing the previous logout, leaving
   the agent stuck in "Telephony Unavailable". (#559)

2. Use agentConfig.sipPassword (from Agent entity) instead of
   OZONETEL_AGENT_PASSWORD env var for login/logout/force-ready.
   The env var was a single shared credential that ignored per-agent
   passwords. Removed hardcoded "Test123$" fallback. Force-ready
   now looks up the Agent entity by ozonetelAgentId to get the
   correct sipPassword + sipExtension.

3. Missed-calls worklist query now fetches campaign { id campaignName }
   so the frontend Branch column can show the campaign name instead
   of the raw DID phone number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 16:54:08 +05:30
parent a00668c517
commit 2666a10f48
3 changed files with 28 additions and 10 deletions

View File

@@ -138,10 +138,9 @@ export class AuthController {
this.logger.warn(`Ozonetel token refresh on login failed: ${err.message}`);
});
const ozAgentPassword = this.telephony.getConfig().ozonetel.agentPassword || 'Test123$';
this.ozonetelAgent.loginAgent({
agentId: agentConfig.ozonetelAgentId,
password: ozAgentPassword,
password: agentConfig.sipPassword,
phoneNumber: agentConfig.sipExtension,
mode: 'blended',
}).catch(err => {
@@ -250,9 +249,14 @@ export class AuthController {
await this.sessionService.unlockSession(agentConfig.ozonetelAgentId);
this.logger.log(`Session unlocked for ${agentConfig.ozonetelAgentId}`);
this.ozonetelAgent.logoutAgent({
// Await the Ozonetel logout so it completes before the
// HTTP response returns. Without this, a fast re-login
// (e.g. "remember me" auto-fill) races the logout and
// the agent lands in "Telephony Unavailable" because
// Ozonetel receives login while still processing logout.
await this.ozonetelAgent.logoutAgent({
agentId: agentConfig.ozonetelAgentId,
password: this.telephony.getConfig().ozonetel.agentPassword || 'Test123$',
password: agentConfig.sipPassword,
}).catch(err => this.logger.warn(`Ozonetel logout failed: ${err.message}`));
this.agentConfigService.clearCache(memberId);

View File

@@ -31,13 +31,26 @@ export class MaintController {
async forceReady(@Body() body: { agentId: string }) {
if (!body?.agentId) throw new HttpException('agentId required', 400);
const agentId = body.agentId;
const oz = this.telephony.getConfig().ozonetel;
const password = oz.agentPassword;
if (!password) throw new HttpException('agent password not configured', 400);
const sipId = oz.sipId;
if (!sipId) throw new HttpException('SIP ID not configured', 400);
this.logger.log(`[MAINT] Force ready: agent=${agentId}`);
// Look up the Agent entity to get sipPassword + sipExtension.
// Password comes from the Agent record, not an env var — each
// agent owns their own Ozonetel credential.
const agentData = await this.platform.query<any>(
`{ agents(first: 1, filter: { ozonetelAgentId: { eq: "${agentId}" } }) { edges { node {
id sipExtension sipPassword
} } } }`,
).catch(() => null);
const agent = agentData?.agents?.edges?.[0]?.node;
if (!agent) throw new HttpException(`Agent ${agentId} not found in platform`, 404);
const password = agent.sipPassword ?? agent.sipExtension;
if (!password) throw new HttpException(`Agent ${agentId} has no sipPassword configured`, 400);
const sipId = agent.sipExtension;
if (!sipId) throw new HttpException(`Agent ${agentId} has no sipExtension configured`, 400);
this.logger.log(`[MAINT] Force ready: agent=${agentId} ext=${sipId}`);
try {
await this.ozonetel.logoutAgent({ agentId, password });

View File

@@ -177,6 +177,7 @@ export class WorklistService {
startedAt endedAt durationSec
disposition leadId leadName
callbackStatus callSourceNumber missedCallCount callbackAttemptedAt
campaign { id campaignName }
} } pageInfo { hasNextPage endCursor } } }`,
'calls',
authHeader,