cal_core/rest/
contact.rs

1// File: cal-core/src/rest/contact.rs
2
3use crate::contact::{Contact, ContactDevice};
4use crate::rest::common::{
5    ApiError, ApiResponse, BatchResult, Id, ListRequest, PaginatedResponse, PaginationParams,
6    SearchParams, SortOrder, SortParams,
7};
8use crate::RecordReference;
9use serde::{Deserialize, Serialize};
10#[cfg(feature = "openapi")]
11use utoipa::ToSchema;
12
13// Create Contact Request
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[cfg_attr(feature = "openapi", derive(ToSchema))]
17pub struct CreateContactRequest {
18    pub organisation: RecordReference,
19    pub account: RecordReference,
20    pub primary: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub secondary: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub tertiary: Option<String>,
25    pub name: String,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub nickname: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub email: Option<String>,
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub capabilities: Vec<String>,
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub groups: Vec<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub device: Option<ContactDevice>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub group: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub opt_out: Option<bool>,
40}
41
42// Update Contact Request
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "openapi", derive(ToSchema))]
45#[serde(rename_all = "camelCase")]
46pub struct UpdateContactRequest {
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub organisation: Option<RecordReference>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub account: Option<RecordReference>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub primary: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub secondary: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub tertiary: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub name: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub nickname: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub email: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub capabilities: Option<Vec<String>>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub groups: Option<Vec<String>>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub device: Option<ContactDevice>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub group: Option<String>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub opt_out: Option<bool>,
73}
74
75// Contact-specific query parameters (extends common list request)
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[cfg_attr(feature = "openapi", derive(ToSchema))]
78#[serde(rename_all = "camelCase")]
79pub struct ContactListRequest {
80    #[serde(flatten)]
81    pub common: ListRequest,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub account_id: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub organisation_id: Option<String>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub name: Option<String>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub primary: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub email: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub group: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub capability: Option<String>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub device_id: Option<String>,
98}
99
100// Batch operations
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(ToSchema))]
103#[serde(rename_all = "camelCase")]
104pub struct BatchCreateContactsRequest {
105    pub contacts: Vec<CreateContactRequest>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[cfg_attr(feature = "openapi", derive(ToSchema))]
110#[serde(rename_all = "camelCase")]
111pub struct BatchUpdateContactsRequest {
112    pub updates: Vec<ContactUpdate>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(ToSchema))]
117#[serde(rename_all = "camelCase")]
118pub struct ContactUpdate {
119    pub id: String,
120    #[serde(flatten)]
121    pub update: UpdateContactRequest,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[cfg_attr(feature = "openapi", derive(ToSchema))]
126#[serde(rename_all = "camelCase")]
127pub struct BatchDeleteContactsRequest {
128    pub ids: Vec<String>,
129}
130
131// Type aliases using common types
132pub type ContactResponse = ApiResponse<Contact>;
133pub type ContactListResponse = ApiResponse<PaginatedResponse<Contact>>;
134pub type BatchCreateContactsResponse = ApiResponse<BatchResult<Contact, ApiError>>;
135pub type BatchUpdateContactsResponse = ApiResponse<BatchResult<Contact, ApiError>>;
136pub type BatchDeleteContactsResponse = ApiResponse<BatchResult<Id, ApiError>>;
137
138// Contact-specific error codes
139#[derive(Debug, Clone)]
140pub enum ContactErrorCode {
141    NotFound,
142    DuplicatePrimary,
143    InvalidEmail,
144    InvalidPhone,
145    MissingRequiredField,
146    InvalidOrganisation,
147    InvalidAccount,
148}
149
150impl ContactErrorCode {
151    pub fn as_str(&self) -> &'static str {
152        match self {
153            Self::NotFound => "CONTACT_NOT_FOUND",
154            Self::DuplicatePrimary => "DUPLICATE_PRIMARY_NUMBER",
155            Self::InvalidEmail => "INVALID_EMAIL_FORMAT",
156            Self::InvalidPhone => "INVALID_PHONE_FORMAT",
157            Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
158            Self::InvalidOrganisation => "INVALID_ORGANISATION",
159            Self::InvalidAccount => "INVALID_ACCOUNT",
160        }
161    }
162}
163
164// Helper implementations
165impl CreateContactRequest {
166    pub fn validate(&self) -> Result<(), ApiError> {
167        if self.name.trim().is_empty() {
168            return Err(ApiError::new(
169                ContactErrorCode::MissingRequiredField.as_str(),
170                "Contact name is required",
171            )
172            .with_field("name"));
173        }
174
175        if self.primary.trim().is_empty() {
176            return Err(ApiError::new(
177                ContactErrorCode::MissingRequiredField.as_str(),
178                "Primary contact number is required",
179            )
180            .with_field("primary"));
181        }
182
183        if let Some(email) = &self.email {
184            if !email.is_empty() && !email.contains('@') {
185                return Err(ApiError::new(
186                    ContactErrorCode::InvalidEmail.as_str(),
187                    "Invalid email format",
188                )
189                .with_field("email"));
190            }
191        }
192
193        Ok(())
194    }
195}
196
197impl UpdateContactRequest {
198    pub fn apply_to(self, contact: &mut Contact) {
199        contact.organisation = self.organisation;
200
201        if let Some(account) = self.account {
202            contact.account = account;
203        }
204        if let Some(primary) = self.primary {
205            contact.primary = primary;
206        }
207        if let Some(secondary) = self.secondary {
208            contact.secondary = Some(secondary);
209        }
210        if let Some(tertiary) = self.tertiary {
211            contact.tertiary = Some(tertiary);
212        }
213        if let Some(name) = self.name {
214            contact.name = name;
215        }
216        if let Some(nickname) = self.nickname {
217            contact.nickname = Some(nickname);
218        }
219        if let Some(email) = self.email {
220            contact.email = Some(email);
221        }
222        if let Some(capabilities) = self.capabilities {
223            contact.capabilities = capabilities;
224        }
225        if let Some(groups) = self.groups {
226            contact.groups = groups;
227        }
228        if let Some(device) = self.device {
229            contact.device = Some(device);
230        }
231        if let Some(opt_out) = self.opt_out {
232            contact.opt_out = Some(opt_out);
233        }
234        if let Some(group) = self.group {
235            contact.group = Some(group);
236        }
237    }
238
239    pub fn is_empty(&self) -> bool {
240        self.organisation.is_none()
241            && self.account.is_none()
242            && self.primary.is_none()
243            && self.secondary.is_none()
244            && self.tertiary.is_none()
245            && self.name.is_none()
246            && self.nickname.is_none()
247            && self.email.is_none()
248            && self.capabilities.is_none()
249            && self.groups.is_none()
250            && self.device.is_none()
251            && self.group.is_none()
252            && self.opt_out.is_none()
253    }
254}
255
256impl Default for ContactListRequest {
257    fn default() -> Self {
258        Self {
259            common: ListRequest {
260                pagination: PaginationParams::default(),
261                sort: SortParams::default(),
262                search: None,
263                time_range: None,
264                filters: None,
265            },
266            account_id: None,
267            organisation_id: None,
268            name: None,
269            primary: None,
270            email: None,
271            group: None,
272            capability: None,
273            device_id: None,
274        }
275    }
276}
277
278// Convenience builders
279impl ContactListRequest {
280    pub fn new() -> Self {
281        Self::default()
282    }
283
284    pub fn with_pagination(mut self, page: u32, page_size: u32) -> Self {
285        self.common.pagination = PaginationParams::new(page, page_size);
286        self
287    }
288
289    pub fn with_sorting(mut self, sort_by: String, sort_order: SortOrder) -> Self {
290        self.common.sort = SortParams {
291            sort_by: Some(sort_by),
292            sort_order,
293        };
294        self
295    }
296
297    pub fn with_search(mut self, query: String) -> Self {
298        self.common.search = Some(SearchParams {
299            query,
300            fields: None,
301            fuzzy: false,
302            highlight: None,
303        });
304        self
305    }
306
307    pub fn with_account_id(mut self, account_id: String) -> Self {
308        self.account_id = Some(account_id);
309        self
310    }
311
312    pub fn with_organisation_id(mut self, organisation_id: String) -> Self {
313        self.organisation_id = Some(organisation_id);
314        self
315    }
316}