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, p.current))
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((r.items, r.pagination.pages, r.pagination.current))
271        })
272        .await
273    }
274
275    /// One page of the detailed call report as **raw JSON rows** (no typed decode), narrowed by
276    /// [`CallReportFilter`] (still validated: bad enum values are `InvalidArgument` before any
277    /// request). The generated `Call` model is stricter than the live API — e.g.
278    /// `bridged.queueId`/`queueName` are required in the model but null whenever a call bridged
279    /// to an **agent** rather than a queue — so the typed [`Self::list_page`] fails to decode
280    /// real production pages. Consumers that only re-serialize rows read raw and lose nothing.
281    pub async fn list_page_raw(
282        &self,
283        page: i32,
284        per_page: Option<i32>,
285        filter: &CallReportFilter,
286    ) -> Result<Page<serde_json::Value>, ManagerError> {
287        let cfg = self.cfg.get().await?;
288        let cfg = cfg.as_ref();
289        // Validate + convert exactly like the typed path, then mirror the generated client's wire
290        // format: plain values for strings/ints, JSON-encoded values for the oneOf enum params.
291        let (r#type, to_number, agent_id, state, finish_reason) = filter.to_params()?;
292        use crate::gen::manager::apis::urlencode as enc;
293        let mut q = vec![format!("page={page}")];
294        if let Some(max) = per_page {
295            q.push(format!("max={max}"));
296        }
297        if let Some(v) = &filter.from_number {
298            q.push(format!("fromNumber={}", enc(v)));
299        }
300        if let Some(v) = filter.time_start {
301            q.push(format!("time.start={v}"));
302        }
303        if let Some(v) = filter.time_end {
304            q.push(format!("time.end={v}"));
305        }
306        if let Some(v) = &filter.q {
307            q.push(format!("q={}", enc(v)));
308        }
309        if let Some(v) = &to_number {
310            let s = serde_json::to_string(v).unwrap_or_default();
311            q.push(format!("toNumber={}", enc(&s)));
312        }
313        if let Some(v) = &agent_id {
314            let s = serde_json::to_string(v).unwrap_or_default();
315            q.push(format!("agentId={}", enc(&s)));
316        }
317        if let Some(v) = &state {
318            let s = serde_json::to_string(v).unwrap_or_default();
319            q.push(format!("state={}", enc(&s)));
320        }
321        if let Some(v) = &r#type {
322            let s = serde_json::to_string(v).unwrap_or_default();
323            q.push(format!("type={}", enc(&s)));
324        }
325        if let Some(v) = &finish_reason {
326            let s = serde_json::to_string(v).unwrap_or_default();
327            q.push(format!("finishReason={}", enc(&s)));
328        }
329        let path = format!("/api/v2/calls/reporting?{}", q.join("&"));
330        fetch_page(
331            &self.retry,
332            |e| e,
333            || async {
334                let v = get_json(cfg, &path).await?;
335                Ok(page_parts(v, page))
336            },
337        )
338        .await
339    }
340
341    /// One page of the simple (timing-metrics) call report as **raw JSON rows** — same
342    /// stricter-than-reality rationale as [`Self::list_page_raw`].
343    pub async fn simple_page_raw(
344        &self,
345        page: i32,
346        per_page: Option<i32>,
347    ) -> Result<Page<serde_json::Value>, ManagerError> {
348        let cfg = self.cfg.get().await?;
349        let cfg = cfg.as_ref();
350        let mut q = vec![format!("page={page}")];
351        if let Some(max) = per_page {
352            q.push(format!("max={max}"));
353        }
354        let path = format!("/api/v2/calls/reporting/simple?{}", q.join("&"));
355        fetch_page(
356            &self.retry,
357            |e| e,
358            || async {
359                let v = get_json(cfg, &path).await?;
360                Ok(page_parts(v, page))
361            },
362        )
363        .await
364    }
365
366    /// One page of the detailed call report, narrowed by [`CallReportFilter`].
367    pub async fn list_page(
368        &self,
369        page: i32,
370        per_page: Option<i32>,
371        filter: &CallReportFilter,
372    ) -> Result<Page<models::Call>, ManagerError> {
373        let cfg = self.cfg.get().await?;
374        let cfg = cfg.as_ref();
375        let (r#type, to_number, agent_id, state, finish_reason) = filter.to_params()?;
376        fetch_page(&self.retry, map_manager_err, || {
377            let (r#type, to_number, agent_id, state, finish_reason) = (
378                r#type.clone(),
379                to_number.clone(),
380                agent_id.clone(),
381                state.clone(),
382                finish_reason.clone(),
383            );
384            async move {
385                let r = call_api::list_reporting_calls(
386                    cfg,
387                    Some(page),
388                    per_page,
389                    None,
390                    None,
391                    None,
392                    None,
393                    r#type,
394                    None,
395                    filter.from_number.as_deref(),
396                    None,
397                    to_number,
398                    filter.time_start,
399                    filter.time_end,
400                    agent_id,
401                    filter.q.as_deref(),
402                    state,
403                    None,
404                    None,
405                    finish_reason,
406                    None,
407                    None,
408                    None,
409                    None,
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                )
426                .await?;
427                Ok((
428                    r.items,
429                    r.pagination.pages,
430                    r.pagination.current,
431                    Some(r.pagination.total),
432                ))
433            }
434        })
435        .await
436    }
437
438    /// One page of the simple (timing-metrics) call report.
439    pub async fn simple_page(
440        &self,
441        page: i32,
442        per_page: Option<i32>,
443    ) -> Result<Page<models::ReportingCall>, ManagerError> {
444        let cfg = self.cfg.get().await?;
445        let cfg = cfg.as_ref();
446        fetch_page(&self.retry, map_manager_err, || async move {
447            let r = reporting_call_api::list_all_simple_reporting_calls(
448                cfg,
449                Some(page),
450                per_page,
451                None,
452                None,
453                None,
454                None,
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            )
483            .await?;
484            Ok((
485                r.items,
486                r.pagination.pages,
487                r.pagination.current,
488                Some(r.pagination.total),
489            ))
490        })
491        .await
492    }
493
494    /// The simple call report across all report types, collecting every call across pages.
495    pub async fn simple_all(&self) -> Result<Vec<models::ReportingCall>, ManagerError> {
496        let cfg = self.cfg.get().await?;
497        let cfg = cfg.as_ref();
498        collect_all(&self.retry, map_manager_err, |page| async move {
499            let r = reporting_call_api::list_all_simple_reporting_calls(
500                cfg,
501                Some(page),
502                None,
503                None,
504                None,
505                None,
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            )
535            .await?;
536            Ok((r.items, r.pagination.pages, r.pagination.current))
537        })
538        .await
539    }
540
541    /// The simple inbound call report, collecting every call across pages.
542    pub async fn inbound_simple_all(&self) -> Result<Vec<models::ReportingCall>, ManagerError> {
543        let cfg = self.cfg.get().await?;
544        let cfg = cfg.as_ref();
545        collect_all(&self.retry, map_manager_err, |page| async move {
546            let r = reporting_call_api::list_inbound_simple_reporting_calls(cfg, Some(page), None)
547                .await?;
548            Ok((r.items, r.pagination.pages, r.pagination.current))
549        })
550        .await
551    }
552}