use crate::RecordReference;
use crate::contact::{Contact, ContactDevice};
use crate::rest::common::{
ApiError, Id, ListRequest, PaginationParams, SearchParams, SortOrder, SortParams,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Request payload for creating a new contact",
example = json!({
"organisation": {
"id": "507f1f77bcf86cd799439011",
"name": "Acme Org"
},
"account": {
"id": "507f1f77bcf86cd799439012",
"name": "Acme Account"
},
"primary": "+1234567890",
"name": "John Doe",
"email": "john.doe@example.com",
"capabilities": ["sms", "voice"],
"groups": ["sales", "support"]
})
))]
pub struct CreateContactRequest {
pub organisation: RecordReference,
pub account: RecordReference,
#[cfg_attr(
feature = "openapi",
schema(example = "+1234567890", pattern = r"^\+?[1-9]\d{1,14}$")
)]
pub primary: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "+0987654321"))]
pub secondary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "+1122334455"))]
pub tertiary: Option<String>,
#[cfg_attr(feature = "openapi", schema(example = "John Doe", min_length = 1))]
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "Johnny"))]
pub nickname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "openapi",
schema(example = "john.doe@example.com", format = "email")
)]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[cfg_attr(feature = "openapi", schema(example = json!(["sms", "voice", "email"])))]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[cfg_attr(feature = "openapi", schema(example = json!(["sales", "vip", "support"])))]
pub groups: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<ContactDevice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = false))]
pub opt_out: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(
feature = "openapi",
schema(title = "Request payload for updating a contact. All fields are optional.")
)]
#[serde(rename_all = "camelCase")]
pub struct UpdateContactRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub organisation: Option<RecordReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account: Option<RecordReference>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
pub primary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secondary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tertiary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "Jane Doe"))]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "jane.doe@example.com"))]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<ContactDevice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub opt_out: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(
feature = "openapi",
schema(title = "Query parameters for listing contacts with filtering and pagination")
)]
#[serde(rename_all = "camelCase")]
pub struct ContactListRequest {
#[serde(flatten)]
pub common: ListRequest,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
pub account_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439012"))]
pub organisation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "John"))]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "+1234567890"))]
pub primary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "john@example.com"))]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "sales"))]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "sms"))]
pub capability: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "dev_123456"))]
pub device_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Request to create multiple contacts in batch",
example = json!({
"contacts": [
{
"organisation": {"id": "org_1", "name": "Org 1"},
"account": {"id": "acc_1", "name": "Account 1"},
"primary": "+1234567890",
"name": "John Doe",
"email": "john@example.com"
},
{
"organisation": {"id": "org_1", "name": "Org 1"},
"account": {"id": "acc_1", "name": "Account 1"},
"primary": "+0987654321",
"name": "Jane Doe",
"email": "jane@example.com"
}
]
})
))]
#[serde(rename_all = "camelCase")]
pub struct BatchCreateContactsRequest {
#[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
pub contacts: Vec<CreateContactRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(
feature = "openapi",
schema(title = "Request to update multiple contacts in batch")
)]
#[serde(rename_all = "camelCase")]
pub struct BatchUpdateContactsRequest {
#[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
pub updates: Vec<ContactUpdate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(
feature = "openapi",
schema(title = "Update operation for a single contact in a batch")
)]
#[serde(rename_all = "camelCase")]
pub struct ContactUpdate {
#[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
pub id: String,
#[serde(flatten)]
pub update: UpdateContactRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Request to delete multiple contacts in batch",
example = json!({
"ids": [
"507f1f77bcf86cd799439011",
"507f1f77bcf86cd799439012",
"507f1f77bcf86cd799439013"
]
})
))]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteContactsRequest {
#[cfg_attr(feature = "openapi", schema(min_items = 1, max_items = 1000))]
pub ids: Vec<String>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub enum ContactErrorCode {
NotFound,
DuplicatePrimary,
InvalidEmail,
InvalidPhone,
MissingRequiredField,
InvalidOrganisation,
InvalidAccount,
}
impl ContactErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
Self::NotFound => "CONTACT_NOT_FOUND",
Self::DuplicatePrimary => "DUPLICATE_PRIMARY_NUMBER",
Self::InvalidEmail => "INVALID_EMAIL_FORMAT",
Self::InvalidPhone => "INVALID_PHONE_FORMAT",
Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
Self::InvalidOrganisation => "INVALID_ORGANISATION",
Self::InvalidAccount => "INVALID_ACCOUNT",
}
}
}
impl CreateContactRequest {
pub fn validate(&self) -> Result<(), ApiError> {
if self.name.trim().is_empty() {
return Err(ApiError::new(
ContactErrorCode::MissingRequiredField.as_str(),
"Contact name is required",
)
.with_field("name"));
}
if self.primary.trim().is_empty() {
return Err(ApiError::new(
ContactErrorCode::MissingRequiredField.as_str(),
"Primary contact number is required",
)
.with_field("primary"));
}
if let Some(email) = &self.email {
if !email.is_empty() && !email.contains('@') {
return Err(ApiError::new(
ContactErrorCode::InvalidEmail.as_str(),
"Invalid email format",
)
.with_field("email"));
}
}
Ok(())
}
}
impl UpdateContactRequest {
pub fn apply_to(self, contact: &mut Contact) {
if let Some(organisation) = self.organisation {
contact.organisation = Some(organisation);
}
if let Some(account) = self.account {
contact.account = account;
}
if let Some(primary) = self.primary {
contact.primary = primary;
}
if let Some(secondary) = self.secondary {
contact.secondary = Some(secondary);
}
if let Some(tertiary) = self.tertiary {
contact.tertiary = Some(tertiary);
}
if let Some(name) = self.name {
contact.name = name;
}
if let Some(nickname) = self.nickname {
contact.nickname = Some(nickname);
}
if let Some(email) = self.email {
contact.email = Some(email);
}
if let Some(capabilities) = self.capabilities {
contact.capabilities = capabilities;
}
if let Some(groups) = self.groups {
contact.groups = groups;
}
if let Some(device) = self.device {
contact.device = Some(device);
}
if let Some(opt_out) = self.opt_out {
contact.opt_out = Some(opt_out);
}
if let Some(group) = self.group {
contact.group = Some(group);
}
}
pub fn is_empty(&self) -> bool {
self.organisation.is_none()
&& self.account.is_none()
&& self.primary.is_none()
&& self.secondary.is_none()
&& self.tertiary.is_none()
&& self.name.is_none()
&& self.nickname.is_none()
&& self.email.is_none()
&& self.capabilities.is_none()
&& self.groups.is_none()
&& self.device.is_none()
&& self.group.is_none()
&& self.opt_out.is_none()
}
}
impl Default for ContactListRequest {
fn default() -> Self {
Self {
common: ListRequest {
pagination: PaginationParams::default(),
sort: SortParams::default(),
search: None,
time_range: None,
filters: None,
},
account_id: None,
organisation_id: None,
name: None,
primary: None,
email: None,
group: None,
capability: None,
device_id: None,
}
}
}
impl ContactListRequest {
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_search(mut self, query: String) -> Self {
self.common.search = Some(SearchParams {
query,
fields: None,
fuzzy: false,
highlight: None,
});
self
}
pub fn with_account_id(mut self, account_id: String) -> Self {
self.account_id = Some(account_id);
self
}
pub fn with_organisation_id(mut self, organisation_id: String) -> Self {
self.organisation_id = Some(organisation_id);
self
}
}