/** * Snapshots interesting ids/phones/customer keys that arrive on `globalEvent` * payloads so the Methods tab can pre-fill them as `{{placeholders}}` (mirrors * the debuggerOmni `extractAndSaveContext` / `applySavedContextToSample` * pattern). Lossy by design — only the fields we know how to use elsewhere. */ export type AafEventContext = { /** Voice / chat call id (sometimes `crtObjectId`). */ crtObjectId?: string; /** Customer id from `voiceCustomerLinked`, `customerCallMemberCreated`, etc. */ customerId?: string; /** Active campaign id from `campaignSelectionChange` / agent session. */ campaignId?: string; /** Latest dialled / connected phone. */ phone?: string; /** Display variant of the phone if the host normalised it. */ displayPhone?: string; /** Chat or voice user object id (when the host shares it). */ userCrtObjectId?: string; /** Chat or voice customer object id. */ customerCrtObjectId?: string; /** Ticket id from attachment / disposition events. */ ticketId?: string; }; const STRING_FIELDS = new Set([ "crtObjectId", "customerId", "campaignId", "phone", "displayPhone", "userCrtObjectId", "customerCrtObjectId", "ticketId" ]); function asString(v: unknown): string | undefined { if (v === null || v === undefined) return undefined; if (typeof v === "string") return v.trim() === "" ? undefined : v; if (typeof v === "number" || typeof v === "bigint") return String(v); return undefined; } function pickStringFromAny(value: unknown, key: string, depth: number, seen: WeakSet): string | undefined { if (value === null || value === undefined) return undefined; if (typeof value !== "object") return undefined; if (depth > 4) return undefined; if (seen.has(value as object)) return undefined; seen.add(value as object); // `campaignSelectionChange` (and others) deliver arrays at the top level — // walk every entry as if it were a sibling object. if (Array.isArray(value)) { for (const item of value) { const nested = pickStringFromAny(item, key, depth + 1, seen); if (nested) return nested; } return undefined; } const obj = value as Record; if (key in obj) { const direct = asString(obj[key]); if (direct) return direct; } for (const v of Object.values(obj)) { if (v && typeof v === "object") { const nested = pickStringFromAny(v, key, depth + 1, seen); if (nested) return nested; } } return undefined; } /** Merge anything we can recognise from `payload` into `current`. Returns a NEW object only when something changed. */ export function extractContextUpdates( current: AafEventContext, payload: unknown ): AafEventContext | null { if (!payload || typeof payload !== "object") return null; const next: AafEventContext = { ...current }; let changed = false; for (const field of STRING_FIELDS) { const found = pickStringFromAny(payload, field as string, 0, new WeakSet()); if (found && found !== next[field]) { next[field] = found; changed = true; } } // Some Ameyo payloads expose `phone1` (column name) — fall back when no `phone` key exists. if (!next.phone) { const alt = pickStringFromAny(payload, "phone1", 0, new WeakSet()); if (alt) { next.phone = alt; changed = true; } } // ECC `ContextEventService` and ticket payloads use `interactionId` for the // ticket / call id; treat that as ticketId for placeholder fill-in. if (!next.ticketId) { const alt = pickStringFromAny(payload, "interactionId", 0, new WeakSet()) ?? pickStringFromAny(payload, "ticketId", 0, new WeakSet()); if (alt) { next.ticketId = alt; changed = true; } } return changed ? next : null; } /** * Replaces `{{customerId}}`, `{{campaignId}}`, `{{phone}}`, `{{userCrtObjectId}}`, * `{{customerCrtObjectId}}`, `{{crtObjectId}}`, `{{ticketId}}`, `{{displayPhone}}` * tokens with the latest captured value, or sensible literal fallbacks so the * sample is still valid JSON. */ export function applyContextToSample(template: string, ctx: AafEventContext): string { if (!template) return template; const fallbacks: Record = { crtObjectId: "crt-object-id-from-event", customerId: "1", campaignId: "1", phone: "9999999999", displayPhone: "9999999999", userCrtObjectId: "user-crt-from-event", customerCrtObjectId: "customer-crt-from-event", ticketId: "ticket-id-from-event" }; return template.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => { const k = key as keyof AafEventContext; return ctx[k] ?? fallbacks[k] ?? `{{${key}}}`; }); }