mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
Compare commits
4 Commits
0248c4cad1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f5bd7d61a | |||
| f1313f0e2f | |||
| 44f1ec36e1 | |||
| 4bd08a9b02 |
24
.woodpecker.yml
Normal file
24
.woodpecker.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Woodpecker CI pipeline for Helix Engage Server (sidecar)
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: [push, manual]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
unit-tests:
|
||||||
|
image: node:20
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
- npm test -- --ci --forceExit
|
||||||
|
|
||||||
|
notify-teams:
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
TEAMS_WEBHOOK:
|
||||||
|
from_secret: teams_webhook
|
||||||
|
commands:
|
||||||
|
- >
|
||||||
|
curl -s -X POST "$TEAMS_WEBHOOK"
|
||||||
|
-H "Content-Type:application/json"
|
||||||
|
-d '{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","content":{"type":"AdaptiveCard","version":"1.4","body":[{"type":"TextBlock","size":"Medium","weight":"Bolder","text":"Helix Engage Server — Build #'"$CI_PIPELINE_NUMBER"'"},{"type":"TextBlock","text":"Branch: '"$CI_COMMIT_BRANCH"'","wrap":true},{"type":"TextBlock","text":"'"$(echo $CI_COMMIT_MESSAGE | head -c 80)"'","wrap":true}],"actions":[{"type":"Action.OpenUrl","title":"View Pipeline","url":"https://operations.healix360.net/repos/2/pipeline/'"$CI_PIPELINE_NUMBER"'"}]}}]}'
|
||||||
|
when:
|
||||||
|
- status: [success, failure]
|
||||||
@@ -62,7 +62,8 @@ export const DEFAULT_TELEPHONY_CONFIG: TelephonyConfig = {
|
|||||||
// Field-by-field mapping from legacy env var names to config paths. Used by
|
// Field-by-field mapping from legacy env var names to config paths. Used by
|
||||||
// the first-boot seeder. Keep in sync with the migration target sites.
|
// the first-boot seeder. Keep in sync with the migration target sites.
|
||||||
export const TELEPHONY_ENV_SEEDS: Array<{ env: string; path: string[] }> = [
|
export const TELEPHONY_ENV_SEEDS: Array<{ env: string; path: string[] }> = [
|
||||||
{ env: 'OZONETEL_AGENT_ID', path: ['ozonetel', 'agentId'] },
|
// OZONETEL_AGENT_ID removed — agentId is per-user on the Agent entity,
|
||||||
|
// not a sidecar-level config. All endpoints require agentId from caller.
|
||||||
{ env: 'OZONETEL_AGENT_PASSWORD', path: ['ozonetel', 'agentPassword'] },
|
{ env: 'OZONETEL_AGENT_PASSWORD', path: ['ozonetel', 'agentPassword'] },
|
||||||
{ env: 'OZONETEL_DID', path: ['ozonetel', 'did'] },
|
{ env: 'OZONETEL_DID', path: ['ozonetel', 'did'] },
|
||||||
{ env: 'OZONETEL_SIP_ID', path: ['ozonetel', 'sipId'] },
|
{ env: 'OZONETEL_SIP_ID', path: ['ozonetel', 'sipId'] },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Post, UseGuards, Logger } from '@nestjs/common';
|
import { Body, Controller, HttpException, Post, UseGuards, Logger } from '@nestjs/common';
|
||||||
import { MaintGuard } from './maint.guard';
|
import { MaintGuard } from './maint.guard';
|
||||||
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
|
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
|
||||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||||
@@ -22,11 +22,14 @@ export class MaintController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('force-ready')
|
@Post('force-ready')
|
||||||
async forceReady() {
|
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 oz = this.telephony.getConfig().ozonetel;
|
||||||
const agentId = oz.agentId || 'agent3';
|
const password = oz.agentPassword;
|
||||||
const password = oz.agentPassword || 'Test123$';
|
if (!password) throw new HttpException('agent password not configured', 400);
|
||||||
const sipId = oz.sipId || '521814';
|
const sipId = oz.sipId;
|
||||||
|
if (!sipId) throw new HttpException('SIP ID not configured', 400);
|
||||||
|
|
||||||
this.logger.log(`[MAINT] Force ready: agent=${agentId}`);
|
this.logger.log(`[MAINT] Force ready: agent=${agentId}`);
|
||||||
|
|
||||||
@@ -48,8 +51,9 @@ export class MaintController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('unlock-agent')
|
@Post('unlock-agent')
|
||||||
async unlockAgent() {
|
async unlockAgent(@Body() body: { agentId: string }) {
|
||||||
const agentId = this.telephony.getConfig().ozonetel.agentId || 'agent3';
|
if (!body?.agentId) throw new HttpException('agentId required', 400);
|
||||||
|
const agentId = body.agentId;
|
||||||
this.logger.log(`[MAINT] Unlock agent session: ${agentId}`);
|
this.logger.log(`[MAINT] Unlock agent session: ${agentId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,15 +20,9 @@ export class OzonetelAgentController {
|
|||||||
private readonly supervisor: SupervisorService,
|
private readonly supervisor: SupervisorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Read-through accessors so admin updates take effect immediately.
|
private requireAgentId(agentId: string | undefined | null): string {
|
||||||
private get defaultAgentId(): string {
|
if (!agentId) throw new HttpException('agentId required', 400);
|
||||||
return this.telephony.getConfig().ozonetel.agentId || 'agent3';
|
return agentId;
|
||||||
}
|
|
||||||
private get defaultAgentPassword(): string {
|
|
||||||
return this.telephony.getConfig().ozonetel.agentPassword;
|
|
||||||
}
|
|
||||||
private get defaultSipId(): string {
|
|
||||||
return this.telephony.getConfig().ozonetel.sipId || '521814';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('agent-login')
|
@Post('agent-login')
|
||||||
@@ -67,17 +61,18 @@ export class OzonetelAgentController {
|
|||||||
|
|
||||||
@Post('agent-state')
|
@Post('agent-state')
|
||||||
async agentState(
|
async agentState(
|
||||||
@Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string },
|
@Body() body: { agentId: string; state: 'Ready' | 'Pause'; pauseReason?: string },
|
||||||
) {
|
) {
|
||||||
if (!body.state) {
|
if (!body.state) {
|
||||||
throw new HttpException('state required', 400);
|
throw new HttpException('state required', 400);
|
||||||
}
|
}
|
||||||
|
const agentId = this.requireAgentId(body.agentId);
|
||||||
|
|
||||||
this.logger.log(`[AGENT-STATE] ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? 'none'})`);
|
this.logger.log(`[AGENT-STATE] ${agentId} → ${body.state} (${body.pauseReason ?? 'none'})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.ozonetelAgent.changeAgentState({
|
const result = await this.ozonetelAgent.changeAgentState({
|
||||||
agentId: this.defaultAgentId,
|
agentId,
|
||||||
state: body.state,
|
state: body.state,
|
||||||
pauseReason: body.pauseReason,
|
pauseReason: body.pauseReason,
|
||||||
});
|
});
|
||||||
@@ -86,7 +81,7 @@ export class OzonetelAgentController {
|
|||||||
// Auto-assign missed call when agent goes Ready
|
// Auto-assign missed call when agent goes Ready
|
||||||
if (body.state === 'Ready') {
|
if (body.state === 'Ready') {
|
||||||
try {
|
try {
|
||||||
const assigned = await this.missedQueue.assignNext(this.defaultAgentId);
|
const assigned = await this.missedQueue.assignNext(agentId);
|
||||||
if (assigned) {
|
if (assigned) {
|
||||||
this.logger.log(`[AGENT-STATE] Auto-assigned missed call ${assigned.id}`);
|
this.logger.log(`[AGENT-STATE] Auto-assigned missed call ${assigned.id}`);
|
||||||
return { ...result, assignedCall: assigned };
|
return { ...result, assignedCall: assigned };
|
||||||
@@ -112,7 +107,7 @@ export class OzonetelAgentController {
|
|||||||
@Body() body: {
|
@Body() body: {
|
||||||
ucid: string;
|
ucid: string;
|
||||||
disposition: string;
|
disposition: string;
|
||||||
agentId?: string;
|
agentId: string;
|
||||||
callerPhone?: string;
|
callerPhone?: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
durationSec?: number;
|
durationSec?: number;
|
||||||
@@ -125,7 +120,7 @@ export class OzonetelAgentController {
|
|||||||
throw new HttpException('ucid and disposition required', 400);
|
throw new HttpException('ucid and disposition required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentId = body.agentId ?? this.defaultAgentId;
|
const agentId = this.requireAgentId(body.agentId);
|
||||||
const ozonetelDisposition = this.mapToOzonetelDisposition(body.disposition);
|
const ozonetelDisposition = this.mapToOzonetelDisposition(body.disposition);
|
||||||
|
|
||||||
// Cancel the ACW auto-dispose timer — the frontend submitted disposition
|
// Cancel the ACW auto-dispose timer — the frontend submitted disposition
|
||||||
@@ -200,7 +195,7 @@ export class OzonetelAgentController {
|
|||||||
|
|
||||||
// Auto-assign next missed call to this agent
|
// Auto-assign next missed call to this agent
|
||||||
try {
|
try {
|
||||||
await this.missedQueue.assignNext(this.defaultAgentId);
|
await this.missedQueue.assignNext(agentId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(`Auto-assignment after dispose failed: ${err}`);
|
this.logger.warn(`Auto-assignment after dispose failed: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -209,7 +204,7 @@ export class OzonetelAgentController {
|
|||||||
this.eventBus.emit(Topics.CALL_COMPLETED, {
|
this.eventBus.emit(Topics.CALL_COMPLETED, {
|
||||||
callId: null,
|
callId: null,
|
||||||
ucid: body.ucid,
|
ucid: body.ucid,
|
||||||
agentId: this.defaultAgentId,
|
agentId,
|
||||||
callerPhone: body.callerPhone ?? '',
|
callerPhone: body.callerPhone ?? '',
|
||||||
direction: body.direction ?? 'INBOUND',
|
direction: body.direction ?? 'INBOUND',
|
||||||
durationSec: body.durationSec ?? 0,
|
durationSec: body.durationSec ?? 0,
|
||||||
@@ -224,13 +219,13 @@ export class OzonetelAgentController {
|
|||||||
|
|
||||||
@Post('dial')
|
@Post('dial')
|
||||||
async dial(
|
async dial(
|
||||||
@Body() body: { phoneNumber: string; agentId?: string; campaignName?: string; leadId?: string },
|
@Body() body: { phoneNumber: string; agentId: string; campaignName?: string; leadId?: string },
|
||||||
) {
|
) {
|
||||||
if (!body.phoneNumber) {
|
if (!body.phoneNumber) {
|
||||||
throw new HttpException('phoneNumber required', 400);
|
throw new HttpException('phoneNumber required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentId = body.agentId ?? this.defaultAgentId;
|
const agentId = this.requireAgentId(body.agentId);
|
||||||
const did = this.telephony.getConfig().ozonetel.did;
|
const did = this.telephony.getConfig().ozonetel.did;
|
||||||
const campaignName = body.campaignName
|
const campaignName = body.campaignName
|
||||||
|| this.telephony.getConfig().ozonetel.campaignName
|
|| this.telephony.getConfig().ozonetel.campaignName
|
||||||
@@ -323,7 +318,7 @@ export class OzonetelAgentController {
|
|||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
async performance(@Query('date') date?: string, @Query('agentId') agentId?: string) {
|
async performance(@Query('date') date?: string, @Query('agentId') agentId?: string) {
|
||||||
const agent = agentId ?? this.defaultAgentId;
|
const agent = this.requireAgentId(agentId);
|
||||||
const targetDate = date ?? new Date().toISOString().split('T')[0];
|
const targetDate = date ?? new Date().toISOString().split('T')[0];
|
||||||
this.logger.log(`Performance: date=${targetDate} agent=${agent}`);
|
this.logger.log(`Performance: date=${targetDate} agent=${agent}`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user