Skip to main content

babelforce_manager_sdk/resources/
calls.rs

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