1use 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#[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 pub organisation: RecordReference,
38
39 pub account: RecordReference,
41
42 #[cfg_attr(
44 feature = "openapi",
45 schema(example = "+1234567890", pattern = r"^\+?[1-9]\d{1,14}$")
46 )]
47 pub primary: String,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 #[cfg_attr(feature = "openapi", schema(example = "+0987654321"))]
52 pub secondary: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 #[cfg_attr(feature = "openapi", schema(example = "+1122334455"))]
57 pub tertiary: Option<String>,
58
59 #[cfg_attr(feature = "openapi", schema(example = "John Doe", min_length = 1))]
61 pub name: String,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 #[cfg_attr(feature = "openapi", schema(example = "Johnny"))]
66 pub nickname: Option<String>,
67
68 #[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 #[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 #[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 #[serde(skip_serializing_if = "Option::is_none")]
88 pub device: Option<ContactDevice>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub group: Option<String>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 #[cfg_attr(feature = "openapi", schema(example = false))]
97 pub opt_out: Option<bool>,
98}
99
100#[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 #[serde(skip_serializing_if = "Option::is_none")]
111 pub organisation: Option<RecordReference>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub account: Option<RecordReference>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 #[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
120 pub primary: Option<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub secondary: Option<String>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub tertiary: Option<String>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 #[cfg_attr(feature = "openapi", schema(example = "Jane Doe"))]
133 pub name: Option<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub nickname: Option<String>,
138
139 #[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 #[serde(skip_serializing_if = "Option::is_none")]
146 pub capabilities: Option<Vec<String>>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub groups: Option<Vec<String>>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub device: Option<ContactDevice>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub group: Option<String>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub opt_out: Option<bool>,
163}
164
165#[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 #[serde(flatten)]
176 pub common: ListRequest,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
181 pub account_id: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439012"))]
186 pub organisation_id: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 #[cfg_attr(feature = "openapi", schema(example = "John"))]
191 pub name: Option<String>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
195 #[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
196 pub primary: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 #[cfg_attr(feature = "openapi", schema(example = "john@example.com"))]
201 pub email: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 #[cfg_attr(feature = "openapi", schema(example = "sales"))]
206 pub group: Option<String>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 #[cfg_attr(feature = "openapi", schema(example = "sms"))]
211 pub capability: Option<String>,
212
213 #[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#[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 #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
247 pub contacts: Vec<CreateContactRequest>,
248}
249
250#[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 #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
261 pub updates: Vec<ContactUpdate>,
262}
263
264#[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 #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
275 pub id: String,
276
277 #[serde(flatten)]
279 pub update: UpdateContactRequest,
280}
281
282#[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 #[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
299 pub ids: Vec<String>,
300}
301
302#[derive(Debug, Clone)]
304#[cfg_attr(feature = "openapi", derive(ToSchema))]
305pub enum ContactErrorCode {
306 NotFound,
308 DuplicatePrimary,
310 InvalidEmail,
312 InvalidPhone,
314 MissingRequiredField,
316 InvalidOrganisation,
318 InvalidAccount,
320}
321
322impl ContactErrorCode {
323 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
337impl CreateContactRequest {
339 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 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 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
455impl ContactListRequest {
457 pub fn new() -> Self {
459 Self::default()
460 }
461
462 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 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 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 pub fn with_account_id(mut self, account_id: String) -> Self {
490 self.account_id = Some(account_id);
491 self
492 }
493
494 pub fn with_organisation_id(mut self, organisation_id: String) -> Self {
496 self.organisation_id = Some(organisation_id);
497 self
498 }
499}