1use crate::contact::Contact;
3use crate::conversation::{
4 Conversation, ConversationId, ConversationItem, ConversationNotification,
5 SessionVariable, VoiceEvent, VoiceItem, WhatsAppItem, WhatsappRequest
6 ,
7};
8use crate::rest::common::{
9 ApiError, ApiResponse, ListRequest, PaginatedResponse,
10 PaginationParams, SortOrder, SortParams,
11};
12use crate::RecordReference;
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CreateConversationRequest {
21 pub from: String, pub account: RecordReference,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub contact: Option<Contact>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub initial_item: Option<ConversationItem>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct UpdateConversationRequest {
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub contact: Option<Contact>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub metadata: Option<serde_json::Value>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct AddConversationItemRequest {
45 pub channel: String, #[serde(skip_serializing_if = "Option::is_none")]
47 pub voice: Option<VoiceItem>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub whatsapp: Option<WhatsAppItem>,
50 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
51 pub meta: HashSet<SessionVariable>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct SendWhatsAppMessageRequest {
59 pub phone_number_id: String,
60 pub message: WhatsappRequest,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct UpdateVoiceEventsRequest {
68 pub item_id: String,
69 pub events: Vec<VoiceEvent>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct ConversationListRequest {
77 #[serde(flatten)]
78 pub common: ListRequest,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub account_id: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub from: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub channel: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub has_contact: Option<bool>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub has_parked_calls: Option<bool>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub within_whatsapp_period: Option<bool>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub created_after: Option<i64>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub created_before: Option<i64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub updated_after: Option<i64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub updated_before: Option<i64>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct ConversationEventsRequest {
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub item_id: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub event_type: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub since_timestamp: Option<i64>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub until_timestamp: Option<i64>,
114 #[serde(default = "default_limit")]
115 pub limit: u32,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct UpdateSessionVariablesRequest {
123 pub variables: Vec<SessionVariable>,
124 #[serde(default)]
125 pub merge: bool, }
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ConversationStats {
133 pub total_conversations: u64,
134 pub active_conversations: u64,
135 pub conversations_with_parked_calls: u64,
136 pub voice_conversations: u64,
137 pub whatsapp_conversations: u64,
138 pub average_items_per_conversation: f64,
139 pub conversations_by_channel: serde_json::Map<String, serde_json::Value>,
140}
141
142pub type ConversationResponse = ApiResponse<Conversation>;
145pub type ConversationListResponse = ApiResponse<PaginatedResponse<Conversation>>;
146pub type ConversationItemResponse = ApiResponse<ConversationItem>;
147pub type ConversationEventsResponse = ApiResponse<Vec<VoiceEvent>>;
148pub type ConversationStatsResponse = ApiResponse<ConversationStats>;
149pub type ConversationNotificationResponse = ApiResponse<ConversationNotification>;
150
151#[derive(Debug, Clone)]
154pub enum ConversationErrorCode {
155 NotFound,
156 InvalidPhoneNumber,
157 InvalidChannel,
158 MissingChannelData,
159 InvalidAccount,
160 ItemNotFound,
161 InvalidTimeRange,
162 WhatsAppPeriodExpired,
163}
164
165impl ConversationErrorCode {
166 pub fn as_str(&self) -> &'static str {
167 match self {
168 Self::NotFound => "CONVERSATION_NOT_FOUND",
169 Self::InvalidPhoneNumber => "INVALID_PHONE_NUMBER",
170 Self::InvalidChannel => "INVALID_CHANNEL",
171 Self::MissingChannelData => "MISSING_CHANNEL_DATA",
172 Self::InvalidAccount => "INVALID_ACCOUNT",
173 Self::ItemNotFound => "CONVERSATION_ITEM_NOT_FOUND",
174 Self::InvalidTimeRange => "INVALID_TIME_RANGE",
175 Self::WhatsAppPeriodExpired => "WHATSAPP_PERIOD_EXPIRED",
176 }
177 }
178}
179
180impl CreateConversationRequest {
183 pub fn into_conversation(self) -> Conversation {
184 let id = ConversationId {
185 from: self.from,
186 account: self.account,
187 };
188
189 let now = chrono::Utc::now().timestamp_millis();
190 let mut items = Vec::new();
191
192 if let Some(initial_item) = self.initial_item {
193 items.push(initial_item);
194 }
195
196 Conversation {
197 id,
198 contact: self.contact.map(|c| c.into()),
199 items,
200 created: now,
201 last_updated: now,
202 }
203 }
204
205 pub fn validate(&self) -> Result<(), ApiError> {
206 if !self.from.starts_with('+') || self.from.len() < 10 || self.from.len() > 15 {
208 return Err(ApiError::new(
209 ConversationErrorCode::InvalidPhoneNumber.as_str(),
210 "Phone number must be in E164 format",
211 )
212 .with_field("from"));
213 }
214
215 Ok(())
216 }
217}
218
219impl AddConversationItemRequest {
220 pub fn into_conversation_item(self, id: String) -> Result<ConversationItem, ApiError> {
221 match self.channel.as_str() {
223 "voice" => {
224 if self.voice.is_none() {
225 return Err(ApiError::new(
226 ConversationErrorCode::MissingChannelData.as_str(),
227 "Voice data is required for voice channel",
228 ));
229 }
230 }
231 "whatsapp" => {
232 if self.whatsapp.is_none() {
233 return Err(ApiError::new(
234 ConversationErrorCode::MissingChannelData.as_str(),
235 "WhatsApp data is required for whatsapp channel",
236 ));
237 }
238 }
239 _ => {
240 return Err(ApiError::new(
241 ConversationErrorCode::InvalidChannel.as_str(),
242 "Channel must be 'voice' or 'whatsapp'",
243 )
244 .with_field("channel"));
245 }
246 }
247
248 Ok(ConversationItem {
249 id,
250 channel: self.channel,
251 timestamp: chrono::Utc::now().timestamp_millis(),
252 voice: self.voice,
253 whatsapp: self.whatsapp,
254 meta: self.meta,
255 })
256 }
257}
258
259impl UpdateConversationRequest {
260 pub fn apply_to(self, conversation: &mut Conversation) {
261 if let Some(contact) = self.contact {
262 conversation.contact = Some(contact.into());
263 }
264 conversation.last_updated = chrono::Utc::now().timestamp_millis();
265 }
266
267 pub fn is_empty(&self) -> bool {
268 self.contact.is_none() && self.metadata.is_none()
269 }
270}
271
272impl Default for ConversationListRequest {
273 fn default() -> Self {
274 Self {
275 common: ListRequest {
276 pagination: PaginationParams::default(),
277 sort: SortParams::default(),
278 search: None,
279 time_range: None,
280 filters: None,
281 },
282 account_id: None,
283 from: None,
284 channel: None,
285 has_contact: None,
286 has_parked_calls: None,
287 within_whatsapp_period: None,
288 created_after: None,
289 created_before: None,
290 updated_after: None,
291 updated_before: None,
292 }
293 }
294}
295
296impl ConversationListRequest {
297 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn with_pagination(mut self, page: u32, page_size: u32) -> Self {
302 self.common.pagination = PaginationParams::new(page, page_size);
303 self
304 }
305
306 pub fn with_sorting(mut self, sort_by: String, sort_order: SortOrder) -> Self {
307 self.common.sort = SortParams {
308 sort_by: Some(sort_by),
309 sort_order,
310 };
311 self
312 }
313
314 pub fn with_account_id(mut self, account_id: String) -> Self {
315 self.account_id = Some(account_id);
316 self
317 }
318
319 pub fn with_channel(mut self, channel: String) -> Self {
320 self.channel = Some(channel);
321 self
322 }
323
324 pub fn with_time_range(mut self, created_after: i64, created_before: i64) -> Self {
325 self.created_after = Some(created_after);
326 self.created_before = Some(created_before);
327 self
328 }
329
330 pub fn only_with_parked_calls(mut self) -> Self {
331 self.has_parked_calls = Some(true);
332 self
333 }
334
335 pub fn only_within_whatsapp_period(mut self) -> Self {
336 self.within_whatsapp_period = Some(true);
337 self
338 }
339}
340
341fn default_limit() -> u32 {
342 100
343}
344
345impl UpdateSessionVariablesRequest {
346 pub fn apply_to(self, conversation: &mut ConversationItem) {
347 if self.merge {
348 for var in self.variables {
350 conversation.meta.replace(var);
351 }
352 } else {
353 conversation.meta = self.variables.into_iter().collect();
355 }
356 }
357}