import type { VoipWebhookEvent } from '@mento/shared'; /** پیشوند ذخیره‌شده در recordingUrl برای واکشی بعدی با توکن API */ export const DAFTARESHOMA_RECORDING_PREFIX = 'daftareshoma-recording:'; export function daftareshomaRecordingRef(recordingId: string): string { return `${DAFTARESHOMA_RECORDING_PREFIX}${recordingId}`; } export function parseDaftaresomaRecordingRef( value: string | null | undefined, ): string | null { const raw = String(value ?? '').trim(); if (!raw.startsWith(DAFTARESHOMA_RECORDING_PREFIX)) return null; const id = raw.slice(DAFTARESHOMA_RECORDING_PREFIX.length).trim(); return id || null; } type Loose = Record; function str(v: unknown): string { return v == null ? '' : String(v).trim(); } function pick(obj: Loose, ...keys: string[]): string { for (const k of keys) { const v = str(obj[k]); if (v) return v; } return ''; } function parseDurationSec(raw: unknown): number | undefined { if (raw == null || raw === '') return undefined; if (typeof raw === 'number' && Number.isFinite(raw)) { return Math.max(0, Math.round(raw)); } const s = String(raw).trim(); if (!s) return undefined; if (/^\d+(\.\d+)?$/.test(s)) return Math.max(0, Math.round(Number(s))); const parts = s.split(':').map((p) => Number(p)); if (parts.some((n) => !Number.isFinite(n))) return undefined; if (parts.length === 3) { return Math.max(0, Math.round(parts[0]! * 3600 + parts[1]! * 60 + parts[2]!)); } if (parts.length === 2) { return Math.max(0, Math.round(parts[0]! * 60 + parts[1]!)); } return undefined; } /** * رویداد وب‌هوک دفتر شما را به VoipWebhookEvent تبدیل می‌کند. * اگر payload از قبل فرمت Montact باشد، همان را برمی‌گرداند. */ export function mapDaftaresomaWebhook( body: unknown, ): VoipWebhookEvent | null { if (!body || typeof body !== 'object') return null; const obj = body as Loose; const legacyCallId = pick(obj, 'callId'); if (legacyCallId && (obj.direction || obj.status || obj.from || obj.to)) { return body as VoipWebhookEvent; } const eventType = pick(obj, 'EventType', 'eventType', 'event'); const callId = pick(obj, 'CallId', 'callId', 'Id', 'id'); if (!callId) return null; const caller = pick( obj, 'CallerNumber', 'callerNumber', 'from', 'From', 'fromNumber', ); /** خط سازمانی که تماس روی آن آمده/رفته */ const lineExtension = pick(obj, 'CalleeExtension', 'calleeExtension', 'Extension', 'extension') || undefined; /** مقصد انتقال / شماره طرف مقابل روی خط */ const transferred = pick( obj, 'TransferredToNumber', 'transferredToNumber', ); const startedAt = pick(obj, 'StartDateTime', 'startDateTime', 'startedAt') || undefined; const endedAt = pick(obj, 'EndDateTime', 'endDateTime', 'endedAt', 'DateTime') || undefined; const durationSec = parseDurationSec( obj.DurationInSeconds ?? obj.durationInSeconds ?? obj.durationSec, ); const lower = eventType.toLowerCase(); const isOutbound = lower.includes('outgoing') || lower.includes('outbound'); const isStarted = lower.includes('started') || lower.includes('ringing') || lower.includes('transferstarted'); const isEnded = lower.includes('ended') || lower.includes('completed') || lower.includes('hangup'); let status: string = 'completed'; if (isStarted && !isEnded) { status = 'ringing'; } else if (isEnded) { const result = obj.Result ?? obj.result; if (result === false || result === 'false' || result === 0) { status = durationSec && durationSec > 0 ? 'completed' : 'missed'; } else if (result === true || result === 'true' || result === 1) { status = 'completed'; } else { status = 'completed'; } } const recordingId = pick( obj, 'RecordingId', 'recordingId', 'RecordId', 'recordId', ); // inbound: from=مشتری، to=همکار/مقصد انتقال یا خود خط // outbound: from=همکار، to=مشتری (TransferredToNumber) let from = caller || undefined; let to: string | undefined; if (isOutbound) { to = transferred || undefined; } else { to = transferred || lineExtension || undefined; } const event: VoipWebhookEvent = { callId, direction: isOutbound ? 'outbound' : 'inbound', status, from, to, lineExtension, startedAt, endedAt, durationSec, caller: from, callee: to, }; if (recordingId) { event.recordingUrl = daftareshomaRecordingRef(recordingId); } return event; } export function isDaftaresomaWebhookPayload(body: unknown): boolean { if (!body || typeof body !== 'object') return false; const obj = body as Loose; return Boolean( pick(obj, 'EventType', 'eventType') || (pick(obj, 'CallId') && pick(obj, 'CallerNumber', 'callerNumber')), ); }