mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: track UCID from SIP headers for Ozonetel disposition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ export class SIPClient {
|
||||
constructor(
|
||||
private config: SIPConfig,
|
||||
private onConnectionChange: (status: ConnectionStatus) => void,
|
||||
private onCallStateChange: (state: CallState, callerNumber?: string) => void,
|
||||
private onCallStateChange: (state: CallState, callerNumber?: string, ucid?: string) => void,
|
||||
) {}
|
||||
|
||||
connect(): void {
|
||||
@@ -66,8 +66,10 @@ export class SIPClient {
|
||||
|
||||
this.currentSession = session;
|
||||
|
||||
// Extract caller number — try multiple SIP headers
|
||||
const callerNumber = this.extractCallerNumber(session);
|
||||
// Extract caller number and UCID — try event request first, then session
|
||||
const sipRequest = (data as any).request ?? (session as any)._request ?? null;
|
||||
const callerNumber = this.extractCallerNumber(session, sipRequest);
|
||||
const ucid = sipRequest?.getHeader ? sipRequest.getHeader('X-UCID') ?? null : null;
|
||||
|
||||
// Setup audio for this session
|
||||
session.on('peerconnection', (e: PeerConnectionEvent) => {
|
||||
@@ -83,16 +85,16 @@ export class SIPClient {
|
||||
});
|
||||
|
||||
session.on('accepted', (() => {
|
||||
this.onCallStateChange('active', callerNumber);
|
||||
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
|
||||
}) as CallListener);
|
||||
|
||||
session.on('confirmed', () => {
|
||||
this.onCallStateChange('active', callerNumber);
|
||||
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
|
||||
});
|
||||
|
||||
session.on('progress', (() => {
|
||||
if (session.direction === 'outgoing') {
|
||||
this.onCallStateChange('ringing-out', callerNumber);
|
||||
this.onCallStateChange('ringing-out', callerNumber, ucid ?? undefined);
|
||||
}
|
||||
}) as CallListener);
|
||||
|
||||
@@ -107,7 +109,7 @@ export class SIPClient {
|
||||
});
|
||||
|
||||
if (session.direction === 'incoming') {
|
||||
this.onCallStateChange('ringing-in', callerNumber);
|
||||
this.onCallStateChange('ringing-in', callerNumber, ucid ?? undefined);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,12 +224,14 @@ export class SIPClient {
|
||||
}
|
||||
}
|
||||
|
||||
private extractCallerNumber(session: RTCSession): string {
|
||||
private extractCallerNumber(session: RTCSession, sipRequest?: any): string {
|
||||
try {
|
||||
const request = session.direction === 'incoming' ? (session as any)._request : null;
|
||||
const request = sipRequest ?? (session.direction === 'incoming' ? (session as any)._request : null);
|
||||
console.log('[SIP] extractCallerNumber: request exists:', !!request, 'direction:', session.direction);
|
||||
if (request) {
|
||||
// Ozonetel sends the real caller number in X-CALLERNO header
|
||||
const xCallerNo = request.getHeader('X-CALLERNO');
|
||||
const xCallerNo = request.getHeader ? request.getHeader('X-CALLERNO') : null;
|
||||
console.log('[SIP] X-CALLERNO header:', xCallerNo);
|
||||
if (xCallerNo) {
|
||||
// Remove leading 0s or country code prefix (00919... → 919...)
|
||||
const cleaned = xCallerNo.replace(/^0+/, '');
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
sipIsOnHoldAtom,
|
||||
sipCallDurationAtom,
|
||||
sipCallStartTimeAtom,
|
||||
sipCallUcidAtom,
|
||||
} from '@/state/sip-state';
|
||||
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
||||
import type { SIPConfig } from '@/types/sip';
|
||||
@@ -24,6 +25,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
||||
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
||||
const [callState, setCallState] = useAtom(sipCallStateAtom);
|
||||
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
||||
const setCallUcid = useSetAtom(sipCallUcidAtom);
|
||||
const setCallDuration = useSetAtom(sipCallDurationAtom);
|
||||
const setCallStartTime = useSetAtom(sipCallStartTimeAtom);
|
||||
|
||||
@@ -33,8 +35,9 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
||||
setConnectionStatus,
|
||||
setCallState,
|
||||
setCallerNumber,
|
||||
setCallUcid,
|
||||
});
|
||||
}, [setConnectionStatus, setCallState, setCallerNumber]);
|
||||
}, [setConnectionStatus, setCallState, setCallerNumber, setCallUcid]);
|
||||
|
||||
// Auto-connect SIP on mount
|
||||
useEffect(() => {
|
||||
@@ -82,6 +85,7 @@ export const useSip = () => {
|
||||
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
|
||||
const [callState] = useAtom(sipCallStateAtom);
|
||||
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
|
||||
const [callUcid] = useAtom(sipCallUcidAtom);
|
||||
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
|
||||
const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom);
|
||||
const [callDuration] = useAtom(sipCallDurationAtom);
|
||||
@@ -117,6 +121,7 @@ export const useSip = () => {
|
||||
connectionStatus,
|
||||
callState,
|
||||
callerNumber,
|
||||
callUcid,
|
||||
isMuted,
|
||||
isOnHold,
|
||||
callDuration,
|
||||
|
||||
@@ -5,11 +5,13 @@ import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
|
||||
let sipClient: SIPClient | null = null;
|
||||
let connected = false;
|
||||
let outboundPending = false;
|
||||
let outboundActive = false;
|
||||
|
||||
type StateUpdater = {
|
||||
setConnectionStatus: (status: ConnectionStatus) => void;
|
||||
setCallState: (state: CallState) => void;
|
||||
setCallerNumber: (number: string | null) => void;
|
||||
setCallUcid: (ucid: string | null) => void;
|
||||
};
|
||||
|
||||
let stateUpdater: StateUpdater | null = null;
|
||||
@@ -46,21 +48,34 @@ export function connectSip(config: SIPConfig): void {
|
||||
sipClient = new SIPClient(
|
||||
config,
|
||||
(status) => stateUpdater?.setConnectionStatus(status),
|
||||
(state, number) => {
|
||||
// Auto-answer SIP when it's a bridge from our outbound Kookoo call
|
||||
(state, number, ucid) => {
|
||||
// Auto-answer SIP when it's a bridge from our outbound call
|
||||
if (state === 'ringing-in' && outboundPending) {
|
||||
outboundPending = false;
|
||||
outboundActive = true;
|
||||
console.log('[SIP] Outbound bridge detected — auto-answering');
|
||||
setTimeout(() => {
|
||||
sipClient?.answer();
|
||||
// Force active state in case SIP callbacks don't fire
|
||||
setTimeout(() => stateUpdater?.setCallState('active'), 300);
|
||||
}, 500);
|
||||
// Store UCID even for outbound bridge calls
|
||||
if (ucid) stateUpdater?.setCallUcid(ucid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't overwrite caller number on outbound calls — it was set by click-to-call
|
||||
stateUpdater?.setCallState(state);
|
||||
if (number !== undefined) stateUpdater?.setCallerNumber(number ?? null);
|
||||
if (!outboundActive && number !== undefined) {
|
||||
stateUpdater?.setCallerNumber(number ?? null);
|
||||
}
|
||||
|
||||
// Store UCID if provided
|
||||
if (ucid) stateUpdater?.setCallUcid(ucid);
|
||||
|
||||
// Reset outbound flag when call ends
|
||||
if (state === 'ended' || state === 'failed') {
|
||||
outboundActive = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -72,7 +87,9 @@ export function disconnectSip(): void {
|
||||
sipClient = null;
|
||||
connected = false;
|
||||
outboundPending = false;
|
||||
outboundActive = false;
|
||||
stateUpdater?.setConnectionStatus('disconnected');
|
||||
stateUpdater?.setCallUcid(null);
|
||||
}
|
||||
|
||||
export function getSipClient(): SIPClient | null {
|
||||
|
||||
@@ -8,3 +8,4 @@ export const sipIsMutedAtom = atom<boolean>(false);
|
||||
export const sipIsOnHoldAtom = atom<boolean>(false);
|
||||
export const sipCallDurationAtom = atom<number>(0);
|
||||
export const sipCallStartTimeAtom = atom<Date | null>(null);
|
||||
export const sipCallUcidAtom = atom<string | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user