cal_core/rest/
account.rs

1// File: cal-core/src/rest/account.rs
2
3use crate::account::*;
4use crate::accounting::Address;
5use crate::device::device::DeviceStruct;
6use crate::rest::common::{
7    ApiError, ListRequest,
8};
9use crate::RecordReference;
10use serde::{Deserialize, Serialize};
11
12#[cfg(feature = "openapi")]
13use utoipa::{IntoParams, ToSchema};
14
15/// Query parameters for account-specific operations
16#[derive(Debug, Deserialize)]
17#[cfg_attr(feature = "openapi", derive(ToSchema, IntoParams))]
18#[cfg_attr(feature = "openapi", schema(
19    title ="Query parameters containing account ID"
20))]
21pub struct AccountIdQuery {
22    /// Account ID for the operation
23    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
24    pub account_id: String,
25}
26
27/// Request to create a new account
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[cfg_attr(feature = "openapi", derive(ToSchema))]
30#[cfg_attr(feature = "openapi", schema(
31    title ="Request payload for creating a new account",
32    example = json!({
33        "name": "Acme Corporation",
34        "mbn": "12345",
35        "domain": "acme.com",
36        "organisation": {
37            "id": "507f1f77bcf86cd799439011",
38            "name": "Acme Org"
39        },
40        "clusterSettings": {
41            "region": "us-east-1",
42            "zone": "a"
43        }
44    })
45))]
46#[serde(rename_all = "camelCase")]
47pub struct CreateAccountRequest {
48    /// Account display name
49    #[cfg_attr(feature = "openapi", schema(example = "Acme Corporation", min_length = 1))]
50    pub name: String,
51
52    /// Main Business Number (MBN) - must be unique
53    #[cfg_attr(feature = "openapi", schema(example = "12345", min_length = 1))]
54    pub mbn: String,
55
56    /// Domain name for the account - must be unique
57    #[cfg_attr(feature = "openapi", schema(example = "acme.com", min_length = 1))]
58    pub domain: String,
59
60    /// Organisation this account belongs to
61    pub organisation: RecordReference,
62
63    /// Physical address of the account
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub address: Option<Address>,
66
67    /// Microsoft Teams integration settings
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub teams: Option<TeamsAccount>,
70
71    /// Environment configuration (e.g., production, staging)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub environment: Option<Environment>,
74
75    /// Spending limits configuration
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub spend_cap: Option<SpendCap>,
78
79    /// Concurrent call limits
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub concurrency: Option<Concurrency>,
82
83    /// Service tier configuration
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub class_of_service: Option<ClassOfService>,
86
87    /// Cluster deployment settings
88    pub cluster_settings: Cluster,
89}
90
91/// Request to update an existing account
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[cfg_attr(feature = "openapi", derive(ToSchema))]
94#[cfg_attr(feature = "openapi", schema(
95    title ="Request payload for updating an account. All fields are optional."
96))]
97#[serde(rename_all = "camelCase")]
98pub struct UpdateAccountRequest {
99    /// New account display name
100    #[serde(skip_serializing_if = "Option::is_none")]
101    #[cfg_attr(feature = "openapi", schema(example = "Acme Corp Updated"))]
102    pub name: Option<String>,
103
104    /// New MBN (must be unique if provided)
105    #[serde(skip_serializing_if = "Option::is_none")]
106    #[cfg_attr(feature = "openapi", schema(example = "54321"))]
107    pub mbn: Option<String>,
108
109    /// New domain (must be unique if provided)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    #[cfg_attr(feature = "openapi", schema(example = "new-acme.com"))]
112    pub domain: Option<String>,
113
114    /// Update organisation reference
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub organisation: Option<RecordReference>,
117
118    /// Update physical address
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub address: Option<Address>,
121
122    /// Update Teams settings
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub teams: Option<TeamsAccount>,
125
126    /// Update environment
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub environment: Option<Environment>,
129
130    /// Update spending limits
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub spend_cap: Option<SpendCap>,
133
134    /// Update concurrency limits
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub concurrency: Option<Concurrency>,
137
138    /// Update service tier
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub class_of_service: Option<ClassOfService>,
141
142    /// Update cluster settings
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub cluster_settings: Option<Cluster>,
145}
146
147/// Request parameters for listing accounts
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[cfg_attr(feature = "openapi", derive(ToSchema))]
150#[cfg_attr(feature = "openapi", schema(
151    title ="Query parameters for listing accounts with filtering and pagination"
152))]
153#[serde(rename_all = "camelCase")]
154pub struct AccountListRequest {
155    /// Common list parameters (pagination, sorting, search)
156    #[serde(flatten)]
157    pub common: ListRequest,
158
159    /// Filter by organisation ID
160    #[serde(skip_serializing_if = "Option::is_none")]
161    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
162    pub organisation_id: Option<String>,
163
164    /// Filter by account name (partial match)
165    #[serde(skip_serializing_if = "Option::is_none")]
166    #[cfg_attr(feature = "openapi", schema(example = "Acme"))]
167    pub name: Option<String>,
168
169    /// Filter by exact MBN
170    #[serde(skip_serializing_if = "Option::is_none")]
171    #[cfg_attr(feature = "openapi", schema(example = "12345"))]
172    pub mbn: Option<String>,
173
174    /// Filter by exact domain
175    #[serde(skip_serializing_if = "Option::is_none")]
176    #[cfg_attr(feature = "openapi", schema(example = "acme.com"))]
177    pub domain: Option<String>,
178}
179
180/// Account-specific error codes
181#[derive(Debug, Clone)]
182#[cfg_attr(feature = "openapi", derive(ToSchema))]
183pub enum AccountErrorCode {
184    /// Account not found
185    NotFound,
186    /// MBN already exists
187    DuplicateMbn,
188    /// Domain already exists
189    DuplicateDomain,
190    /// Invalid organisation reference
191    InvalidOrganisation,
192    /// Invalid cluster configuration
193    InvalidCluster,
194    /// Required field is missing
195    MissingRequiredField,
196}
197
198impl AccountErrorCode {
199    /// Convert error code to string representation
200    pub fn as_str(&self) -> &'static str {
201        match self {
202            Self::NotFound => "ACCOUNT_NOT_FOUND",
203            Self::DuplicateMbn => "DUPLICATE_MBN",
204            Self::DuplicateDomain => "DUPLICATE_DOMAIN",
205            Self::InvalidOrganisation => "INVALID_ORGANISATION",
206            Self::InvalidCluster => "INVALID_CLUSTER",
207            Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
208        }
209    }
210}
211
212// Helper implementations
213impl CreateAccountRequest {
214    /// Validate the create account request
215    pub fn validate(&self) -> Result<(), ApiError> {
216        if self.name.trim().is_empty() {
217            return Err(ApiError::new(
218                AccountErrorCode::MissingRequiredField.as_str(),
219                "Account name is required",
220            )
221                .with_field("name"));
222        }
223
224        if self.mbn.trim().is_empty() {
225            return Err(ApiError::new(
226                AccountErrorCode::MissingRequiredField.as_str(),
227                "MBN is required",
228            )
229                .with_field("mbn"));
230        }
231
232        if self.domain.trim().is_empty() {
233            return Err(ApiError::new(
234                AccountErrorCode::MissingRequiredField.as_str(),
235                "Domain is required",
236            )
237                .with_field("domain"));
238        }
239
240        Ok(())
241    }
242}
243
244impl UpdateAccountRequest {
245    /// Check if the update request has any fields to update
246    pub fn is_empty(&self) -> bool {
247        self.name.is_none()
248            && self.mbn.is_none()
249            && self.domain.is_none()
250            && self.organisation.is_none()
251            && self.address.is_none()
252            && self.teams.is_none()
253            && self.environment.is_none()
254            && self.spend_cap.is_none()
255            && self.concurrency.is_none()
256            && self.class_of_service.is_none()
257            && self.cluster_settings.is_none()
258    }
259}
260
261impl Default for AccountListRequest {
262    fn default() -> Self {
263        Self {
264            common: ListRequest {
265                pagination: crate::rest::common::PaginationParams::default(),
266                sort: crate::rest::common::SortParams::default(),
267                search: None,
268                time_range: None,
269                filters: None,
270            },
271            organisation_id: None,
272            name: None,
273            mbn: None,
274            domain: None,
275        }
276    }
277}
278
279impl AccountListRequest {
280    /// Create a new account list request with defaults
281    pub fn new() -> Self {
282        Self::default()
283    }
284
285    /// Set pagination parameters
286    pub fn with_pagination(mut self, page: u32, page_size: u32) -> Self {
287        self.common.pagination = crate::rest::common::PaginationParams::new(page, page_size);
288        self
289    }
290
291    /// Filter by organisation ID
292    pub fn with_organisation_id(mut self, organisation_id: String) -> Self {
293        self.organisation_id = Some(organisation_id);
294        self
295    }
296}