cal-core 0.2.158

Callable core lib
Documentation
// File: cal-core/src/rest/conversation.rs
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;

// ==================== Create Conversation Request ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateConversationRequest {
    pub from: String, // E164 phone number
    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>,
}

// ==================== Update Conversation Request ====================

#[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>,
}

// ==================== Add Conversation Item Request ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddConversationItemRequest {
    pub channel: String, // "voice" or "whatsapp"
    #[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>,
}

// ==================== Send WhatsApp Message Request ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendWhatsAppMessageRequest {
    pub phone_number_id: String,
    pub message: WhatsappRequest,
}

// ==================== Update Voice Events Request ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateVoiceEventsRequest {
    pub item_id: String,
    pub events: Vec<VoiceEvent>,
}

// ==================== List Conversations Request ====================

#[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>,
}

// ==================== Conversation Events Request ====================

#[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,
}

// ==================== Session Variables Request ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionVariablesRequest {
    pub variables: Vec<SessionVariable>,
    #[serde(default)]
    pub merge: bool, // If true, merge with existing; if false, replace
}

// ==================== Conversation Statistics ====================

#[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>,
}

// ==================== Error Codes ====================

#[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",
        }
    }
}

// ==================== Helper Implementations ====================

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> {
        // Validate E164 phone number format
        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> {
        // Validate channel data
        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 {
            // Merge new variables with existing ones
            for var in self.variables {
                conversation.meta.replace(var);
            }
        } else {
            // Replace all variables
            conversation.meta = self.variables.into_iter().collect();
        }
    }
}