use crate::contact::Contact;
use crate::conversation::{
Conversation, ConversationId, ConversationItem, ConversationNotification,
SessionVariable, VoiceEvent, VoiceItem, WhatsAppItem, WhatsappRequest
,
};
use crate::rest::common::{
ApiError, ListRequest,
PaginationParams, SortOrder, SortParams,
};
use crate::RecordReference;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateConversationRequest {
pub from: String, pub account: RecordReference,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_item: Option<ConversationItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateConversationRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddConversationItemRequest {
pub channel: String, #[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub whatsapp: Option<WhatsAppItem>,
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub meta: HashSet<SessionVariable>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendWhatsAppMessageRequest {
pub phone_number_id: String,
pub message: WhatsappRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateVoiceEventsRequest {
pub item_id: String,
pub events: Vec<VoiceEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationListRequest {
#[serde(flatten)]
pub common: ListRequest,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_contact: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_parked_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub within_whatsapp_period: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_after: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_before: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_after: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_before: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationEventsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub item_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub until_timestamp: Option<i64>,
#[serde(default = "default_limit")]
pub limit: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionVariablesRequest {
pub variables: Vec<SessionVariable>,
#[serde(default)]
pub merge: bool, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationStats {
pub total_conversations: u64,
pub active_conversations: u64,
pub conversations_with_parked_calls: u64,
pub voice_conversations: u64,
pub whatsapp_conversations: u64,
pub average_items_per_conversation: f64,
pub conversations_by_channel: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub enum ConversationErrorCode {
NotFound,
InvalidPhoneNumber,
InvalidChannel,
MissingChannelData,
InvalidAccount,
ItemNotFound,
InvalidTimeRange,
WhatsAppPeriodExpired,
}
impl ConversationErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
Self::NotFound => "CONVERSATION_NOT_FOUND",
Self::InvalidPhoneNumber => "INVALID_PHONE_NUMBER",
Self::InvalidChannel => "INVALID_CHANNEL",
Self::MissingChannelData => "MISSING_CHANNEL_DATA",
Self::InvalidAccount => "INVALID_ACCOUNT",
Self::ItemNotFound => "CONVERSATION_ITEM_NOT_FOUND",
Self::InvalidTimeRange => "INVALID_TIME_RANGE",
Self::WhatsAppPeriodExpired => "WHATSAPP_PERIOD_EXPIRED",
}
}
}
impl CreateConversationRequest {
pub fn into_conversation(self) -> Conversation {
let id = ConversationId {
from: self.from,
account: self.account,
};
let now = chrono::Utc::now().timestamp_millis();
let mut items = Vec::new();
if let Some(initial_item) = self.initial_item {
items.push(initial_item);
}
Conversation {
id,
contact: self.contact.map(|c| c.into()),
items,
created: now,
last_updated: now,
}
}
pub fn validate(&self) -> Result<(), ApiError> {
if !self.from.starts_with('+') || self.from.len() < 10 || self.from.len() > 15 {
return Err(ApiError::new(
ConversationErrorCode::InvalidPhoneNumber.as_str(),
"Phone number must be in E164 format",
)
.with_field("from"));
}
Ok(())
}
}
impl AddConversationItemRequest {
pub fn into_conversation_item(self, id: String) -> Result<ConversationItem, ApiError> {
match self.channel.as_str() {
"voice" => {
if self.voice.is_none() {
return Err(ApiError::new(
ConversationErrorCode::MissingChannelData.as_str(),
"Voice data is required for voice channel",
));
}
}
"whatsapp" => {
if self.whatsapp.is_none() {
return Err(ApiError::new(
ConversationErrorCode::MissingChannelData.as_str(),
"WhatsApp data is required for whatsapp channel",
));
}
}
_ => {
return Err(ApiError::new(
ConversationErrorCode::InvalidChannel.as_str(),
"Channel must be 'voice' or 'whatsapp'",
)
.with_field("channel"));
}
}
Ok(ConversationItem {
id,
channel: self.channel,
timestamp: chrono::Utc::now().timestamp_millis(),
voice: self.voice,
whatsapp: self.whatsapp,
meta: self.meta,
})
}
}
impl UpdateConversationRequest {
pub fn apply_to(self, conversation: &mut Conversation) {
if let Some(contact) = self.contact {
conversation.contact = Some(contact.into());
}
conversation.last_updated = chrono::Utc::now().timestamp_millis();
}
pub fn is_empty(&self) -> bool {
self.contact.is_none() && self.metadata.is_none()
}
}
impl Default for ConversationListRequest {
fn default() -> Self {
Self {
common: ListRequest {
pagination: PaginationParams::default(),
sort: SortParams::default(),
search: None,
time_range: None,
filters: None,
},
account_id: None,
from: None,
channel: None,
has_contact: None,
has_parked_calls: None,
within_whatsapp_period: None,
created_after: None,
created_before: None,
updated_after: None,
updated_before: None,
}
}
}
impl ConversationListRequest {
pub fn new() -> Self {
Self::default()
}
pub fn with_pagination(mut self, page: u32, page_size: u32) -> Self {
self.common.pagination = PaginationParams::new(page, page_size);
self
}
pub fn with_sorting(mut self, sort_by: String, sort_order: SortOrder) -> Self {
self.common.sort = SortParams {
sort_by: Some(sort_by),
sort_order,
};
self
}
pub fn with_account_id(mut self, account_id: String) -> Self {
self.account_id = Some(account_id);
self
}
pub fn with_channel(mut self, channel: String) -> Self {
self.channel = Some(channel);
self
}
pub fn with_time_range(mut self, created_after: i64, created_before: i64) -> Self {
self.created_after = Some(created_after);
self.created_before = Some(created_before);
self
}
pub fn only_with_parked_calls(mut self) -> Self {
self.has_parked_calls = Some(true);
self
}
pub fn only_within_whatsapp_period(mut self) -> Self {
self.within_whatsapp_period = Some(true);
self
}
}
fn default_limit() -> u32 {
100
}
impl UpdateSessionVariablesRequest {
pub fn apply_to(self, conversation: &mut ConversationItem) {
if self.merge {
for var in self.variables {
conversation.meta.replace(var);
}
} else {
conversation.meta = self.variables.into_iter().collect();
}
}
}