Skip to main content

babelforce_manager_sdk/resources/
calls.rs

1use crate::error::{map_manager_err, ManagerError};
2use crate::gen::manager::apis::configuration::Configuration;
3use crate::gen::manager::apis::{call_api, manager_api, queue_api, reporting_call_api};
4use crate::gen::manager::models;
5use crate::http::{collect_all, fetch_page, Page};
6use crate::resources::raw::{get_json, page_parts, post_json};
7use crate::retry::{with_retry, RetryPolicy};
8use crate::token::SharedCfg;
9
10/// Filters for the detailed call report (`/api/v2/calls/reporting`). String-typed on purpose —
11/// values are validated against the API's enums on use, so a bad value is an
12/// [`ManagerError::InvalidArgument`] naming the field, never a silent mismatch.
13#[derive(Debug, Clone, Default)]
14pub struct CallReportFilter {
15    /// Caller number (E.164 or fragment).
16    pub from_number: Option<String>,
17    /// Dialed number.
18    pub to_number: Option<String>,
19    /// Unix seconds, inclusive lower bound.
20    pub time_start: Option<i32>,
21    /// Unix seconds, exclusive upper bound.
22    pub time_end: Option<i32>,
23    /// Only calls handled by this agent (UUID).
24    pub agent_id: Option<String>,
25    /// Call state, e.g. `completed` | `no-answer` | `busy`.
26    pub state: Option<String>,
27    /// Call type, e.g. `inbound` | `outbound`.
28    pub r#type: Option<String>,
29    /// Why the call ended.
30    pub finish_reason: Option<String>,
31    /// Free-text search.
32    pub q: Option<String>,
33}
34
35/// Parse a string into a serde-renamed API enum (e.g. `"completed"` → `CallState::Completed`),
36/// mapping failure to an [`ManagerError::InvalidArgument`] naming the field.
37fn parse_enum<T: serde::de::DeserializeOwned>(field: &str, value: &str) -> Result<T, ManagerError> {
38    serde_json::from_value(serde_json::Value::String(value.to_string()))
39        .map_err(|_| ManagerError::InvalidArgument(format!("{field}: unknown value '{value}'")))
40}
41
42impl CallReportFilter {
43    #[allow(clippy::type_complexity)]
44    fn to_params(
45        &self,
46    ) -> Result<
47        (
48            Option<models::ListReportingCallsTypeParameter>,
49            Option<models::ListReportingCallsToParameter>,
50            Option<models::ListReportingCallsIdParameter>,
51            Option<models::ListReportingCallsStateParameter>,
52            Option<models::ListReportingCallsFinishReasonParameter>,
53        ),
54        ManagerError,
55    > {
56        let r#type = self
57            .r#type
58            .as_deref()
59            .map(|v| parse_enum::<models::CallType>("type", v))
60            .transpose()?
61            .map(models::ListReportingCallsTypeParameter::CallType);
62        let to_number = self
63            .to_number
64            .clone()
65            .map(models::ListReportingCallsToParameter::ReportingNumberFilter);
66        let agent_id = self
67            .agent_id
68            .as_deref()
69            .map(|v| {
70                uuid::Uuid::parse_str(v)
71                    .map_err(|e| ManagerError::InvalidArgument(format!("agent_id: {e}")))
72            })
73            .transpose()?
74            .map(models::ListReportingCallsIdParameter::ObjectUuid);
75        let state = self
76            .state
77            .as_deref()
78            .map(|v| parse_enum::<models::CallState>("state", v))
79            .transpose()?
80            .map(models::ListReportingCallsStateParameter::CallState);
81        let finish_reason = self
82            .finish_reason
83            .as_deref()
84            .map(|v| parse_enum::<models::CallFinishReason>("finish_reason", v))
85            .transpose()?
86            .map(models::ListReportingCallsFinishReasonParameter::CallFinishReason);
87        Ok((r#type, to_number, agent_id, state, finish_reason))
88    }
89}
90
91/// Call reporting and call control — `/api/v2/calls`, with a nested `reporting` sub-resource.
92pub struct CallsResource {
93    pub(crate) cfg: SharedCfg<Configuration>,
94    pub(crate) retry: RetryPolicy,
95    /// Call reporting — `/api/v2/calls/reporting`.
96    pub reporting: ReportingResource,
97}
98
99impl CallsResource {
100    /// Get a single call by id as **raw JSON** (the full item-response envelope). The typed
101    /// [`Self::get`] can fail on real payloads — the generated models are stricter than the live
102    /// API (e.g. `bridged.queueId` is a required UUID in the model but null for agent-bridged calls).
103    pub async fn get_raw(&self, id: &str) -> Result<serde_json::Value, ManagerError> {
104        let cfg = self.cfg.get().await?;
105        let cfg = cfg.as_ref();
106        let path = format!("/api/v2/calls/{}", crate::gen::manager::apis::urlencode(id));
107        with_retry(&self.retry, true, || get_json(cfg, &path)).await
108    }
109
110    /// Get a single call by id.
111    pub async fn get(&self, id: &str) -> Result<models::CallItemResponse, ManagerError> {
112        let cfg = self.cfg.get().await?;
113        let cfg = cfg.as_ref();
114        with_retry(&self.retry, true, || call_api::get_call(cfg, id))
115            .await
116            .map_err(map_manager_err)
117    }
118
119    /// Hang up a live call; returns the updated call.
120    pub async fn hangup(&self, id: &str) -> Result<models::CallItemResponse, ManagerError> {
121        let cfg = self.cfg.get().await?;
122        let cfg = cfg.as_ref();
123        with_retry(&self.retry, false, || manager_api::hangup_call(cfg, id))
124            .await
125            .map_err(map_manager_err)
126    }
127
128    /// Hang up a live call; returns the updated call as **raw JSON**. The typed [`Self::hangup`]
129    /// decodes `CallItemResponse`, which breaks on live agent-bridged calls (`bridged.queueId` is
130    /// a required UUID in the model but null in production) — and a just-hung-up bridged call is
131    /// the common case for this endpoint.
132    pub async fn hangup_raw(&self, id: &str) -> Result<serde_json::Value, ManagerError> {
133        let cfg = self.cfg.get().await?;
134        let cfg = cfg.as_ref();
135        let path = format!(
136            "/api/v2/calls/{}/hangup",
137            crate::gen::manager::apis::urlencode(id)
138        );
139        // Non-idempotent action → no retry on ambiguous failures.
140        with_retry(&self.retry, false, || post_json(cfg, &path)).await
141    }
142
143    /// Create an inbound test call.
144    pub async fn create_test_call(
145        &self,
146        body: models::CreateTestCallRequest,
147    ) -> Result<models::CallItemResponse, ManagerError> {
148        let cfg = self.cfg.get().await?;
149        let cfg = cfg.as_ref();
150        with_retry(&self.retry, false, || {
151            call_api::create_inbound_test_call(cfg, body.clone())
152        })
153        .await
154        .map_err(map_manager_err)
155    }
156
157    /// Set session variables on a call.
158    pub async fn set_session_variables(
159        &self,
160        id: &str,
161        variables: models::SetCallSessionVariablesRequest,
162    ) -> Result<models::SetCallSessionVariablesResponse, ManagerError> {
163        let cfg = self.cfg.get().await?;
164        let cfg = cfg.as_ref();
165        with_retry(&self.retry, false, || {
166            manager_api::set_call_session_variables(cfg, id, Some(variables.clone()))
167        })
168        .await
169        .map_err(map_manager_err)
170    }
171
172    /// Cancel a queued or scheduled call; returns the updated call.
173    pub async fn cancel(&self, id: &str) -> Result<models::CallItemResponse, ManagerError> {
174        let cfg = self.cfg.get().await?;
175        let cfg = cfg.as_ref();
176        with_retry(&self.retry, false, || manager_api::cancel_call(cfg, id))
177            .await
178            .map_err(map_manager_err)
179    }
180
181    /// List the calls waiting in a queue, collecting every call across pages.
182    pub async fn list_queued(
183        &self,
184        queue_id: &str,
185    ) -> Result<Vec<models::QueuedCall>, ManagerError> {
186        let cfg = self.cfg.get().await?;
187        let cfg = cfg.as_ref();
188        collect_all(&self.retry, map_manager_err, |page| async move {
189            let r = queue_api::list_queued_calls(cfg, queue_id, Some(page), None).await?;
190            let (pages, current) = r
191                .pagination
192                .as_ref()
193                .map(|p| (p.pages.unwrap_or(1), p.current.unwrap_or(1)))
194                .unwrap_or((1, 1));
195            Ok((r.items, pages, current))
196        })
197        .await
198    }
199
200    /// Request a callback for a caller waiting in a queue.
201    pub async fn queue_callback(
202        &self,
203        queue_id: &str,
204        body: models::QueueCallbackRequest,
205    ) -> Result<models::QueueCallbackResponse, ManagerError> {
206        let cfg = self.cfg.get().await?;
207        let cfg = cfg.as_ref();
208        with_retry(&self.retry, false, || {
209            queue_api::queue_callback(cfg, queue_id, body.clone())
210        })
211        .await
212        .map_err(map_manager_err)
213    }
214}
215
216/// Call reporting — `/api/v2/calls/reporting`.
217pub struct ReportingResource {
218    pub(crate) cfg: SharedCfg<Configuration>,
219    pub(crate) retry: RetryPolicy,
220}
221
222impl ReportingResource {
223    /// The detailed call report, collecting every call across pages.
224    pub async fn list_all(&self) -> Result<Vec<models::Call>, ManagerError> {
225        let cfg = self.cfg.get().await?;
226        let cfg = cfg.as_ref();
227        collect_all(&self.retry, map_manager_err, |page| async move {
228            let r = call_api::list_reporting_calls(
229                cfg,
230                Some(page),
231                None,
232                None,
233                None,
234                None,
235                None,
236                None,
237                None,
238                None,
239                None,
240                None,
241                None,
242                None,
243                None,
244                None,
245                None,
246                None,
247                None,
248                None,
249                None,
250                None,
251                None,
252                None,
253                None,
254                None,
255                None,
256                None,
257                None,
258                None,
259                None,
260                None,
261                None,
262                None,
263                None,
264                None,
265                None,
266                None,
267                None,
268            )
269            .await?;
270            Ok((
271                r.items,
272                r.pagination.pages.unwrap_or(1),
273                r.pagination.current.unwrap_or(1),
274            ))
275        })
276        .await
277    }
278
279    /// One page of the detailed call report as **raw JSON rows** (no typed decode), narrowed by
280    /// [`CallReportFilter`] (still validated: bad enum values are `InvalidArgument` before any
281    /// request). The generated `Call` model is stricter than the live API — e.g.
282    /// `bridged.queueId`/`queueName` are required in the model but null whenever a call bridged
283    /// to an **agent** rather than a queue — so the typed [`Self::list_page`] fails to decode
284    /// real production pages. Consumers that only re-serialize rows read raw and lose nothing.
285    pub async fn list_page_raw(
286        &self,
287        page: i32,
288        per_page: Option<i32>,
289        filter: &CallReportFilter,
290    ) -> Result<Page<serde_json::Value>, ManagerError> {
291        let cfg = self.cfg.get().await?;
292        let cfg = cfg.as_ref();
293        // Validate + convert exactly like the typed path, then mirror the generated client's wire
294        // format: plain values for strings/ints, JSON-encoded values for the oneOf enum params.
295        let (r#type, to_number, agent_id, state, finish_reason) = filter.to_params()?;
296        use crate::gen::manager::apis::urlencode as enc;
297        let mut q = vec![format!("page={page}")];
298        if let Some(max) = per_page {
299            q.push(format!("max={max}"));
300        }
301        if let Some(v) = &filter.from_number {
302            q.push(format!("fromNumber={}", enc(v)));
303        }
304        if let Some(v) = filter.time_start {
305            q.push(format!("time.start={v}"));
306        }
307        if let Some(v) = filter.time_end {
308            q.push(format!("time.end={v}"));
309        }
310        if let Some(v) = &filter.q {
311            q.push(format!("q={}", enc(v)));
312        }
313        if let Some(v) = &to_number {
314            let s = serde_json::to_string(v).unwrap_or_default();
315            q.push(format!("toNumber={}", enc(&s)));
316        }
317        if let Some(v) = &agent_id {
318            let s = serde_json::to_string(v).unwrap_or_default();
319            q.push(format!("agentId={}", enc(&s)));
320        }
321        if let Some(v) = &state {
322            let s = serde_json::to_string(v).unwrap_or_default();
323            q.push(format!("state={}", enc(&s)));
324        }
325        if let Some(v) = &r#type {
326            let s = serde_json::to_string(v).unwrap_or_default();
327            q.push(format!("type={}", enc(&s)));
328        }
329        if let Some(v) = &finish_reason {
330            let s = serde_json::to_string(v).unwrap_or_default();
331            q.push(format!("finishReason={}", enc(&s)));
332        }
333        let path = format!("/api/v2/calls/reporting?{}", q.join("&"));
334        fetch_page(
335            &self.retry,
336            |e| e,
337            || async {
338                let v = get_json(cfg, &path).await?;
339                Ok(page_parts(v, page))
340            },
341        )
342        .await
343    }
344
345    /// One page of the simple (timing-metrics) call report as **raw JSON rows** — same
346    /// stricter-than-reality rationale as [`Self::list_page_raw`].
347    pub async fn simple_page_raw(
348        &self,
349        page: i32,
350        per_page: Option<i32>,
351    ) -> Result<Page<serde_json::Value>, ManagerError> {
352        let cfg = self.cfg.get().await?;
353        let cfg = cfg.as_ref();
354        let mut q = vec![format!("page={page}")];
355        if let Some(max) = per_page {
356            q.push(format!("max={max}"));
357        }
358        let path = format!("/api/v2/calls/reporting/simple?{}", q.join("&"));
359        fetch_page(
360            &self.retry,
361            |e| e,
362            || async {
363                let v = get_json(cfg, &path).await?;
364                Ok(page_parts(v, page))
365            },
366        )
367        .await
368    }
369
370    /// One page of the detailed call report, narrowed by [`CallReportFilter`].
371    pub async fn list_page(
372        &self,
373        page: i32,
374        per_page: Option<i32>,
375        filter: &CallReportFilter,
376    ) -> Result<Page<models::Call>, ManagerError> {
377        let cfg = self.cfg.get().await?;
378        let cfg = cfg.as_ref();
379        let (r#type, to_number, agent_id, state, finish_reason) = filter.to_params()?;
380        fetch_page(&self.retry, map_manager_err, || {
381            let (r#type, to_number, agent_id, state, finish_reason) = (
382                r#type.clone(),
383                to_number.clone(),
384                agent_id.clone(),
385                state.clone(),
386                finish_reason.clone(),
387            );
388            async move {
389                let r = call_api::list_reporting_calls(
390                    cfg,
391                    Some(page),
392                    per_page,
393                    None,
394                    None,
395                    None,
396                    None,
397                    r#type,
398                    None,
399                    filter.from_number.as_deref(),
400                    None,
401                    to_number,
402                    filter.time_start,
403                    filter.time_end,
404                    agent_id,
405                    filter.q.as_deref(),
406                    state,
407                    None,
408                    None,
409                    finish_reason,
410                    None,
411                    None,
412                    None,
413                    None,
414                    None,
415                    None,
416                    None,
417                    None,
418                    None,
419                    None,
420                    None,
421                    None,
422                    None,
423                    None,
424                    None,
425                    None,
426                    None,
427                    None,
428                    None,
429                )
430                .await?;
431                Ok((
432                    r.items,
433                    r.pagination.pages.unwrap_or(1),
434                    r.pagination.current.unwrap_or(1),
435                    r.pagination.total,
436                ))
437            }
438        })
439        .await
440    }
441
442    /// One page of the simple (timing-metrics) call report.
443    pub async fn simple_page(
444        &self,
445        page: i32,
446        per_page: Option<i32>,
447    ) -> Result<Page<models::ReportingCall>, ManagerError> {
448        let cfg = self.cfg.get().await?;
449        let cfg = cfg.as_ref();
450        fetch_page(&self.retry, map_manager_err, || async move {
451            let r = reporting_call_api::list_all_simple_reporting_calls(
452                cfg,
453                Some(page),
454                per_page,
455                None,
456                None,
457                None,
458                None,
459                None,
460                None,
461                None,
462                None,
463                None,
464                None,
465                None,
466                None,
467                None,
468                None,
469                None,
470                None,
471                None,
472                None,
473                None,
474                None,
475                None,
476                None,
477                None,
478                None,
479                None,
480                None,
481                None,
482                None,
483                None,
484                None,
485                None,
486            )
487            .await?;
488            Ok((
489                r.items,
490                r.pagination.pages.unwrap_or(1),
491                r.pagination.current.unwrap_or(1),
492                r.pagination.total,
493            ))
494        })
495        .await
496    }
497
498    /// The simple call report across all report types, collecting every call across pages.
499    pub async fn simple_all(&self) -> Result<Vec<models::ReportingCall>, ManagerError> {
500        let cfg = self.cfg.get().await?;
501        let cfg = cfg.as_ref();
502        collect_all(&self.retry, map_manager_err, |page| async move {
503            let r = reporting_call_api::list_all_simple_reporting_calls(
504                cfg,
505                Some(page),
506                None,
507                None,
508                None,
509                None,
510                None,
511                None,
512                None,
513                None,
514                None,
515                None,
516                None,
517                None,
518                None,
519                None,
520                None,
521                None,
522                None,
523                None,
524                None,
525                None,
526                None,
527                None,
528                None,
529                None,
530                None,
531                None,
532                None,
533                None,
534                None,
535                None,
536                None,
537                None,
538            )
539            .await?;
540            Ok((
541                r.items,
542                r.pagination.pages.unwrap_or(1),
543                r.pagination.current.unwrap_or(1),
544            ))
545        })
546        .await
547    }
548
549    /// The simple inbound call report, collecting every call across pages.
550    pub async fn inbound_simple_all(&self) -> Result<Vec<models::ReportingCall>, ManagerError> {
551        let cfg = self.cfg.get().await?;
552        let cfg = cfg.as_ref();
553        collect_all(&self.retry, map_manager_err, |page| async move {
554            let r = reporting_call_api::list_inbound_simple_reporting_calls(cfg, Some(page), None)
555                .await?;
556            Ok((
557                r.items,
558                r.pagination.pages.unwrap_or(1),
559                r.pagination.current.unwrap_or(1),
560            ))
561        })
562        .await
563    }
564}