diff --git a/.changeset/phone-integration-active-call.md b/.changeset/phone-integration-active-call.md new file mode 100644 index 00000000..67a19968 --- /dev/null +++ b/.changeset/phone-integration-active-call.md @@ -0,0 +1,6 @@ +--- +"@epilot/phone-integration-client": minor +"@epilot/sdk": minor +--- + +Phone Integration: the active call of the signed-in user — `getActiveCall` (is the user on a call, and the phone system's `conversation_id` that ties journey submissions, tickets and the `call` entity together), `reportActiveCall` and `clearActiveCall` (the epilot 360 phone reports the call it holds). diff --git a/clients/phone-integration-client/src/client.test.ts b/clients/phone-integration-client/src/client.test.ts index 32133d22..5c14b22b 100644 --- a/clients/phone-integration-client/src/client.test.ts +++ b/clients/phone-integration-client/src/client.test.ts @@ -9,6 +9,9 @@ describe('Phone Integration client', () => { expect(client.defaults.baseURL).toBe('https://phone-integration-api.sls.epilot.io'); expect(client.api.getOperations().map((operation) => operation.operationId)).toEqual([ 'batchSearchCustomers', + 'getActiveCall', + 'reportActiveCall', + 'clearActiveCall', 'getManagerStatus', 'listPhoneAgents', 'assignPhoneAgent', @@ -84,6 +87,53 @@ describe('Phone Integration client', () => { expect(response.data).toEqual(result); }); + it('reads, reports and clears the active call on one path', async () => { + const client = createClient(); + const onCall: Components.Schemas.ActiveCall = { + on_call: true, + conversation_id: '01KW704DAM1PM5SDGKFDY9BNK0', + started_at: '2026-11-10T10:00:00.000Z', + source: 'softphone', + expires_at: '2026-11-10T10:02:00.000Z', + }; + const requests: Array<{ method?: string; url?: string; params?: unknown; data?: unknown }> = []; + client.defaults.adapter = async (config) => { + requests.push({ method: config.method, url: config.url, params: config.params, data: config.data }); + return config.method === 'delete' + ? { config, data: '', status: 204, statusText: 'No Content', headers: {} } + : { config, data: onCall, status: 200, statusText: 'OK', headers: {} }; + }; + + const reported = await client.reportActiveCall(null, { + conversation_id: '01KW704DAM1PM5SDGKFDY9BNK0', + started_at: '2026-11-10T10:00:00.000Z', + }); + const read = await client.getActiveCall(); + const cleared = await client.clearActiveCall({ conversation_id: '01KW704DAM1PM5SDGKFDY9BNK0' }); + + expect(reported.data).toEqual(onCall); + expect(read.data.conversation_id).toBe('01KW704DAM1PM5SDGKFDY9BNK0'); + expect(cleared.status).toBe(204); + expect(requests).toEqual([ + { + method: 'put', + url: '/v1/calls/active', + params: {}, + data: JSON.stringify({ + conversation_id: '01KW704DAM1PM5SDGKFDY9BNK0', + started_at: '2026-11-10T10:00:00.000Z', + }), + }, + { method: 'get', url: '/v1/calls/active', params: {}, data: undefined }, + { + method: 'delete', + url: '/v1/calls/active', + params: { conversation_id: '01KW704DAM1PM5SDGKFDY9BNK0' }, + data: undefined, + }, + ]); + }); + it('caches getClient and keeps fresh client credentials isolated', () => { const singleton = getClient(); singleton.defaults.headers.common.Authorization = 'Bearer singleton-token'; diff --git a/clients/phone-integration-client/src/openapi-runtime.json b/clients/phone-integration-client/src/openapi-runtime.json index a969a2bb..7a284bae 100644 --- a/clients/phone-integration-client/src/openapi-runtime.json +++ b/clients/phone-integration-client/src/openapi-runtime.json @@ -58,6 +58,33 @@ "responses": {} } }, + "/v1/calls/active": { + "get": { + "operationId": "getActiveCall", + "responses": {} + }, + "put": { + "operationId": "reportActiveCall", + "requestBody": { + "required": true, + "content": { + "application/json": {} + } + }, + "responses": {} + }, + "delete": { + "operationId": "clearActiveCall", + "parameters": [ + { + "name": "conversation_id", + "in": "query", + "required": false + } + ], + "responses": {} + } + }, "/v1/manager/status": { "get": { "operationId": "getManagerStatus", @@ -395,7 +422,7 @@ "name": "tag", "in": "path", "required": true, - "description": "The skill, URL-encoded (e.g. `language%3Agerman`)" + "description": "The skill, URL-encoded (e.g. `language%3Agerman`), in any letter case (see `Skill`)" } ], "delete": { @@ -415,7 +442,7 @@ "name": "tag", "in": "path", "required": true, - "description": "The skill, URL-encoded (e.g. `language%3Agerman`)" + "description": "The skill, URL-encoded (e.g. `language%3Agerman`), in any letter case (see `Skill`)" } ], "put": { diff --git a/clients/phone-integration-client/src/openapi.d.ts b/clients/phone-integration-client/src/openapi.d.ts index 877a0cda..69d3ed3c 100644 --- a/clients/phone-integration-client/src/openapi.d.ts +++ b/clients/phone-integration-client/src/openapi.d.ts @@ -15,6 +15,53 @@ declare namespace Components { export type Unauthorized = Schemas.ErrorResponse; } namespace Schemas { + /** + * The phone call a user is on right now. + */ + export interface ActiveCall { + /** + * Whether the user is on an answered call. The other fields are present only when true. + */ + on_call: boolean; + /** + * The phone system's conversation id: the same on every leg of a transferred or conferenced + * call, and the `external_id` of the `call` entity created after the call. + * + * example: + * 01KW704DAM1PM5SDGKFDY9BNK0 + */ + conversation_id?: string; + /** + * When the user's part of the call started + */ + started_at?: string; // date-time + /** + * `softphone`: reported by the epilot 360 phone. `provider`: read from the phone system's + * live state because no current report exists. + * + */ + source?: "softphone" | "provider"; + /** + * Until when the report is valid unless renewed (`softphone` only) + */ + expires_at?: string; // date-time + } + export interface ActiveCallReport { + /** + * The phone system's conversation id of the call (not the id of the user's own leg) + * example: + * 01KW704DAM1PM5SDGKFDY9BNK0 + */ + conversation_id: string; + /** + * When the user's part of the call started, as an ISO 8601 time. Without one (or with a + * time that is not one, or lies in the future) it is the first report of the conversation. + * + * example: + * 2026-11-10T10:00:00.000Z + */ + started_at?: string; + } export interface AgentActivity { provider_agent_id: string; from: string; // date-time @@ -128,6 +175,9 @@ declare namespace Components { * A skill as the phone system holds it (Babelforce: an agent tag). epilot's convention for skills is * `: