cal_core/rest/
contact.rs

1// File: cal-core/src/rest/contact.rs
2
3use crate::RecordReference;
4use crate::contact::{Contact, ContactDevice};
5use crate::rest::common::{
6    ApiError, Id, ListRequest, PaginationParams, SearchParams, SortOrder, SortParams,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap};
10#[cfg(feature = "openapi")]
11use utoipa::ToSchema;
12
13/// Request to create a new contact
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[cfg_attr(feature = "openapi", derive(ToSchema))]
17#[cfg_attr(feature = "openapi", schema(
18    title ="Request payload for creating a new contact",
19    example = json!({
20        "organisation": {
21            "id": "507f1f77bcf86cd799439011",
22            "name": "Acme Org"
23        },
24        "account": {
25            "id": "507f1f77bcf86cd799439012",
26            "name": "Acme Account"
27        },
28        "primary": "+1234567890",
29        "name": "John Doe",
30        "email": "john.doe@example.com",
31        "capabilities": ["sms", "voice"],
32        "groups": ["sales", "support"]
33    })
34))]
35pub struct CreateContactRequest {
36    /// Organisation this contact belongs to
37    pub organisation: RecordReference,
38
39    /// Account this contact belongs to
40    pub account: RecordReference,
41
42    /// Primary phone number (required)
43    #[cfg_attr(
44        feature = "openapi",
45        schema(example = "+1234567890", pattern = r"^\+?[1-9]\d{1,14}$")
46    )]
47    pub primary: String,
48
49    /// Secondary phone number
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[cfg_attr(feature = "openapi", schema(example = "+0987654321"))]
52    pub secondary: Option<String>,
53
54    /// Tertiary phone number
55    #[serde(skip_serializing_if = "Option::is_none")]
56    #[cfg_attr(feature = "openapi", schema(example = "+1122334455"))]
57    pub tertiary: Option<String>,
58
59    /// Contact's full name
60    #[cfg_attr(feature = "openapi", schema(example = "John Doe", min_length = 1))]
61    pub name: String,
62
63    /// Contact's nickname or preferred name
64    #[serde(skip_serializing_if = "Option::is_none")]
65    #[cfg_attr(feature = "openapi", schema(example = "Johnny"))]
66    pub nickname: Option<String>,
67
68    /// Contact's email address
69    #[serde(skip_serializing_if = "Option::is_none")]
70    #[cfg_attr(
71        feature = "openapi",
72        schema(example = "john.doe@example.com", format = "email")
73    )]
74    pub email: Option<String>,
75
76    /// Communication capabilities (e.g., sms, voice, video)
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    #[cfg_attr(feature = "openapi", schema(example = json!(["sms", "voice", "email"])))]
79    pub capabilities: Vec<String>,
80
81    /// Groups this contact belongs to
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    #[cfg_attr(feature = "openapi", schema(example = json!(["sales", "vip", "support"])))]
84    pub groups: Vec<String>,
85
86    /// Associated device information
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub device: Option<ContactDevice>,
89
90    /// Primary group (deprecated - use groups instead)
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub group: Option<String>,
93
94    /// Whether the contact has opted out of communications
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[cfg_attr(feature = "openapi", schema(example = false))]
97    pub opt_out: Option<bool>,
98}
99
100/// Request to update an existing contact
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(ToSchema))]
103#[cfg_attr(
104    feature = "openapi",
105    schema(title = "Request payload for updating a contact. All fields are optional.")
106)]
107#[serde(rename_all = "camelCase")]
108pub struct UpdateContactRequest {
109    /// Update organisation reference
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub organisation: Option<RecordReference>,
112
113    /// Update account reference
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub account: Option<RecordReference>,
116
117    /// Update primary phone number
118    #[serde(skip_serializing_if = "Option::is_none")]
119    #[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
120    pub primary: Option<String>,
121
122    /// Update secondary phone number
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub secondary: Option<String>,
125
126    /// Update tertiary phone number
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub tertiary: Option<String>,
129
130    /// Update contact's name
131    #[serde(skip_serializing_if = "Option::is_none")]
132    #[cfg_attr(feature = "openapi", schema(example = "Jane Doe"))]
133    pub name: Option<String>,
134
135    /// Update nickname
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub nickname: Option<String>,
138
139    /// Update email address
140    #[serde(skip_serializing_if = "Option::is_none")]
141    #[cfg_attr(feature = "openapi", schema(example = "jane.doe@example.com"))]
142    pub email: Option<String>,
143
144    /// Update capabilities
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub capabilities: Option<Vec<String>>,
147
148    /// Update groups
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub groups: Option<Vec<String>>,
151
152    /// Update device information
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub device: Option<ContactDevice>,
155
156    /// Update primary group
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub group: Option<String>,
159
160    /// Update opt-out status
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub opt_out: Option<bool>,
163}
164
165/// Request parameters for listing contacts
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "openapi", derive(ToSchema))]
168#[cfg_attr(
169    feature = "openapi",
170    schema(title = "Query parameters for listing contacts with filtering and pagination")
171)]
172#[serde(rename_all = "camelCase")]
173pub struct ContactListRequest {
174    /// Common list parameters (pagination, sorting, search)
175    #[serde(flatten)]
176    pub common: ListRequest,
177
178    /// Filter by account ID
179    #[serde(skip_serializing_if = "Option::is_none")]
180    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
181    pub account_id: Option<String>,
182
183    /// Filter by organisation ID
184    #[serde(skip_serializing_if = "Option::is_none")]
185    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439012"))]
186    pub organisation_id: Option<String>,
187
188    /// Filter by name (partial match)
189    #[serde(skip_serializing_if = "Option::is_none")]
190    #[cfg_attr(feature = "openapi", schema(example = "John"))]
191    pub name: Option<String>,
192
193    /// Filter by primary phone number
194    #[serde(skip_serializing_if = "Option::is_none")]
195    #[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
196    pub primary: Option<String>,
197
198    /// Filter by email address
199    #[serde(skip_serializing_if = "Option::is_none")]
200    #[cfg_attr(feature = "openapi", schema(example = "john@example.com"))]
201    pub email: Option<String>,
202
203    /// Filter by group membership
204    #[serde(skip_serializing_if = "Option::is_none")]
205    #[cfg_attr(feature = "openapi", schema(example = "sales"))]
206    pub group: Option<String>,
207
208    /// Filter by capability
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[cfg_attr(feature = "openapi", schema(example = "sms"))]
211    pub capability: Option<String>,
212
213    /// Filter by device ID
214    #[serde(skip_serializing_if = "Option::is_none")]
215    #[cfg_attr(feature = "openapi", schema(example = "dev_123456"))]
216    pub device_id: Option<String>,
217}
218
219/// Request to create multiple contacts in a single operation
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "openapi", derive(ToSchema))]
222#[cfg_attr(feature = "openapi", schema(
223    title ="Request to create multiple contacts in batch",
224    example = json!({
225        "contacts": [
226            {
227                "organisation": {"id": "org_1", "name": "Org 1"},
228                "account": {"id": "acc_1", "name": "Account 1"},
229                "primary": "+1234567890",
230                "name": "John Doe",
231                "email": "john@example.com"
232            },
233            {
234                "organisation": {"id": "org_1", "name": "Org 1"},
235                "account": {"id": "acc_1", "name": "Account 1"},
236                "primary": "+0987654321",
237                "name": "Jane Doe",
238                "email": "jane@example.com"
239            }
240        ]
241    })
242))]
243#[serde(rename_all = "camelCase")]
244pub struct BatchCreateContactsRequest {
245    /// Array of contacts to create
246    #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
247    pub contacts: Vec<CreateContactRequest>,
248}
249
250/// Request to update multiple contacts in a single operation
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[cfg_attr(feature = "openapi", derive(ToSchema))]
253#[cfg_attr(
254    feature = "openapi",
255    schema(title = "Request to update multiple contacts in batch")
256)]
257#[serde(rename_all = "camelCase")]
258pub struct BatchUpdateContactsRequest {
259    /// Array of contact updates
260    #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
261    pub updates: Vec<ContactUpdate>,
262}
263
264/// Individual contact update in a batch operation
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[cfg_attr(feature = "openapi", derive(ToSchema))]
267#[cfg_attr(
268    feature = "openapi",
269    schema(title = "Update operation for a single contact in a batch")
270)]
271#[serde(rename_all = "camelCase")]
272pub struct ContactUpdate {
273    /// ID of the contact to update
274    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
275    pub id: String,
276
277    /// Update fields
278    #[serde(flatten)]
279    pub update: UpdateContactRequest,
280}
281
282/// Request to delete multiple contacts in a single operation
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "openapi", derive(ToSchema))]
285#[cfg_attr(feature = "openapi", schema(
286    title ="Request to delete multiple contacts in batch",
287    example = json!({
288        "ids": [
289            "507f1f77bcf86cd799439011",
290            "507f1f77bcf86cd799439012",
291            "507f1f77bcf86cd799439013"
292        ]
293    })
294))]
295#[serde(rename_all = "camelCase")]
296pub struct BatchDeleteContactsRequest {
297    /// Array of contact IDs to delete
298    #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
299    pub ids: Vec<String>,
300}
301
302/// Contact-specific error codes
303#[derive(Debug, Clone)]
304#[cfg_attr(feature = "openapi", derive(ToSchema))]
305pub enum ContactErrorCode {
306    /// Contact not found
307    NotFound,
308    /// Primary number already exists
309    DuplicatePrimary,
310    /// Invalid email format
311    InvalidEmail,
312    /// Invalid phone number format
313    InvalidPhone,
314    /// Required field is missing
315    MissingRequiredField,
316    /// Invalid organisation reference
317    InvalidOrganisation,
318    /// Invalid account reference
319    InvalidAccount,
320}
321
322impl ContactErrorCode {
323    /// Convert error code to string representation
324    pub fn as_str(&self) -> &'static str {
325        match self {
326            Self::NotFound => "CONTACT_NOT_FOUND",
327            Self::DuplicatePrimary => "DUPLICATE_PRIMARY_NUMBER",
328            Self::InvalidEmail => "INVALID_EMAIL_FORMAT",
329            Self::InvalidPhone => "INVALID_PHONE_FORMAT",
330            Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
331            Self::InvalidOrganisation => "INVALID_ORGANISATION",
332            Self::InvalidAccount => "INVALID_ACCOUNT",
333        }
334    }
335}
336
337// Helper implementations
338impl CreateContactRequest {
339    /// Validate the create contact request
340    pub fn validate(&self) -> Result<(), ApiError> {
341        if self.name.trim().is_empty() {
342            return Err(ApiError::new(
343                ContactErrorCode::MissingRequiredField.as_str(),
344                "Contact name is required",
345            )
346            .with_field("name"));
347        }
348
349        if self.primary.trim().is_empty() {
350            return Err(ApiError::new(
351                ContactErrorCode::MissingRequiredField.as_str(),
352                "Primary contact number is required",
353            )
354            .with_field("primary"));
355        }
356
357        if let Some(email) = &self.email {
358            if !email.is_empty() && !email.contains('@') {
359                return Err(ApiError::new(
360                    ContactErrorCode::InvalidEmail.as_str(),
361                    "Invalid email format",
362                )
363                .with_field("email"));
364            }
365        }
366
367        Ok(())
368    }
369}
370
371impl UpdateContactRequest {
372    /// Apply updates to an existing contact
373    pub fn apply_to(self, contact: &mut Contact) {
374        if let Some(organisation) = self.organisation {
375            contact.organisation = Some(organisation);
376        }
377        if let Some(account) = self.account {
378            contact.account = account;
379        }
380        if let Some(primary) = self.primary {
381            contact.primary = primary;
382        }
383        if let Some(secondary) = self.secondary {
384            contact.secondary = Some(secondary);
385        }
386        if let Some(tertiary) = self.tertiary {
387            contact.tertiary = Some(tertiary);
388        }
389        if let Some(name) = self.name {
390            contact.name = name;
391        }
392        if let Some(nickname) = self.nickname {
393            contact.nickname = Some(nickname);
394        }
395        if let Some(email) = self.email {
396            contact.email = Some(email);
397        }
398        if let Some(capabilities) = self.capabilities {
399            contact.capabilities = capabilities;
400        }
401        if let Some(groups) = self.groups {
402            contact.groups = groups;
403        }
404        if let Some(device) = self.device {
405            contact.device = Some(device);
406        }
407        if let Some(opt_out) = self.opt_out {
408            contact.opt_out = Some(opt_out);
409        }
410        if let Some(group) = self.group {
411            contact.group = Some(group);
412        }
413    }
414
415    /// Check if the update request has any fields to update
416    pub fn is_empty(&self) -> bool {
417        self.organisation.is_none()
418            && self.account.is_none()
419            && self.primary.is_none()
420            && self.secondary.is_none()
421            && self.tertiary.is_none()
422            && self.name.is_none()
423            && self.nickname.is_none()
424            && self.email.is_none()
425            && self.capabilities.is_none()
426            && self.groups.is_none()
427            && self.device.is_none()
428            && self.group.is_none()
429            && self.opt_out.is_none()
430    }
431}
432
433impl Default for ContactListRequest {
434    fn default() -> Self {
435        Self {
436            common: ListRequest {
437                pagination: PaginationParams::default(),
438                sort: SortParams::default(),
439                search: None,
440                time_range: None,
441                filters: None,
442            },
443            account_id: None,
444            organisation_id: None,
445            name: None,
446            primary: None,
447            email: None,
448            group: None,
449            capability: None,
450            device_id: None,
451        }
452    }
453}
454
455// Convenience builders
456impl ContactListRequest {
457    /// Create a new contact list request with defaults
458    pub fn new() -> Self {
459        Self::default()
460    }
461
462    /// Set pagination parameters
463    pub fn with_pagination(mut self, page: u32, page_size: u32) -> Self {
464        self.common.pagination = PaginationParams::new(page, page_size);
465        self
466    }
467
468    /// Set sorting parameters
469    pub fn with_sorting(mut self, sort_by: String, sort_order: SortOrder) -> Self {
470        self.common.sort = SortParams {
471            sort_by: Some(sort_by),
472            sort_order,
473        };
474        self
475    }
476
477    /// Set search parameters
478    pub fn with_search(mut self, query: String) -> Self {
479        self.common.search = Some(SearchParams {
480            query,
481            fields: None,
482            fuzzy: false,
483            highlight: None,
484        });
485        self
486    }
487
488    /// Filter by account ID
489    pub fn with_account_id(mut self, account_id: String) -> Self {
490        self.account_id = Some(account_id);
491        self
492    }
493
494    /// Filter by organisation ID
495    pub fn with_organisation_id(mut self, organisation_id: String) -> Self {
496        self.organisation_id = Some(organisation_id);
497        self
498    }
499}