sure_client_rs/client/accounts.rs
1use crate::models::account::{
2 AccountCollection, AccountDetail, AccountableAttributes, CreateAccountData,
3 CreateAccountRequest, UpdateAccountData, UpdateAccountRequest,
4};
5use crate::models::{DeleteResponse, PaginatedResponse};
6use crate::types::AccountId;
7use crate::{ApiError, error::ApiResult};
8use bon::bon;
9use reqwest::Method;
10use rust_decimal::Decimal;
11use std::collections::HashMap;
12use url::Url;
13
14use super::SureClient;
15
16const MAX_PER_PAGE: u32 = 100;
17
18#[bon]
19impl SureClient {
20 /// List accounts
21 ///
22 /// Retrieves a paginated list of accounts.
23 ///
24 /// # Arguments
25 /// * `page` - Page number (default: 1)
26 /// * `per_page` - Items per page (default: 25, max: 100)
27 ///
28 /// # Returns
29 /// A paginated response containing accounts and pagination metadata.
30 ///
31 /// # Errors
32 /// Returns `ApiError::Unauthorized` if the API key is invalid.
33 /// Returns `ApiError::Network` if the request fails due to network issues.
34 ///
35 /// # Example
36 /// ```no_run
37 /// use sure_client_rs::{SureClient, BearerToken};
38 ///
39 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
40 /// // Use defaults (page 1, per_page 25)
41 /// let response = client.get_accounts().call().await?;
42 ///
43 /// for account in response.items.accounts {
44 /// println!("{}: {:?} {:?}", account.name, account.balance, account.currency);
45 /// }
46 ///
47 /// // Or customize parameters using the builder
48 /// let response = client.get_accounts().page(2).per_page(50).call().await?;
49 /// # Ok(())
50 /// # }
51 /// ```
52 #[builder]
53 pub async fn get_accounts(
54 &self,
55 #[builder(default = 1)] page: u32,
56 #[builder(default = 25)] per_page: u32,
57 ) -> ApiResult<PaginatedResponse<AccountCollection>> {
58 if per_page > MAX_PER_PAGE {
59 return Err(ApiError::InvalidParameter(format!(
60 "per_page cannot exceed {MAX_PER_PAGE}",
61 )));
62 }
63
64 let mut query_params = HashMap::new();
65
66 query_params.insert("page", page.to_string());
67 query_params.insert("per_page", per_page.to_string());
68
69 self.execute_request(Method::GET, "/api/v1/accounts", Some(&query_params), None)
70 .await
71 }
72
73 /// Get a specific account by ID
74 ///
75 /// Retrieves detailed information about a single account.
76 ///
77 /// # Arguments
78 /// * `id` - The account ID to retrieve
79 ///
80 /// # Returns
81 /// Detailed account information.
82 ///
83 /// # Errors
84 /// Returns `ApiError::NotFound` if the account doesn't exist.
85 /// Returns `ApiError::Unauthorized` if the API key is invalid.
86 /// Returns `ApiError::Network` if the request fails due to network issues.
87 ///
88 /// # Example
89 /// ```no_run
90 /// use sure_client_rs::{SureClient, BearerToken, AccountId};
91 /// use uuid::Uuid;
92 ///
93 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
94 /// let account_id = AccountId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
95 /// let account = client.get_account(&account_id).await?;
96 ///
97 /// println!("Account: {}", account.name);
98 /// println!("Balance: {}", account.balance);
99 /// # Ok(())
100 /// # }
101 /// ```
102 pub async fn get_account(&self, id: &AccountId) -> ApiResult<AccountDetail> {
103 self.execute_request(Method::GET, &format!("/api/v1/accounts/{}", id), None, None)
104 .await
105 }
106}
107
108#[bon]
109impl SureClient {
110 /// Create a new account
111 ///
112 /// Creates a new account with type-specific attributes. The account kind is
113 /// automatically derived from the attributes you provide.
114 ///
115 /// # Arguments
116 /// * `name` - The account name
117 /// * `balance` - The initial account balance
118 /// * `attributes` - Type-specific attributes that determine the account kind
119 /// * `currency` - Optional currency code (defaults to family currency if not provided)
120 /// * `institution_name` - Optional name of the financial institution
121 /// * `institution_domain` - Optional domain of the financial institution
122 /// * `notes` - Optional additional notes
123 ///
124 /// # Returns
125 /// The newly created account with full details.
126 ///
127 /// # Errors
128 /// Returns `ApiError::ValidationError` if required fields are missing or invalid.
129 /// Returns `ApiError::Unauthorized` if the API key is invalid.
130 /// Returns `ApiError::Network` if the request fails due to network issues.
131 ///
132 /// # Example
133 /// ```no_run
134 /// use sure_client_rs::{SureClient, BearerToken};
135 /// use sure_client_rs::models::account::{
136 /// AccountableAttributes, DepositoryAttributes, DepositorySubtype
137 /// };
138 /// use rust_decimal::Decimal;
139 ///
140 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
141 /// let account = client.create_account()
142 /// .name("Checking Account".to_string())
143 /// .balance(Decimal::new(100000, 2)) // $1,000.00
144 /// .attributes(AccountableAttributes::Depository(DepositoryAttributes {
145 /// subtype: Some(DepositorySubtype::Checking),
146 /// locked_attributes: None,
147 /// }))
148 /// .currency(iso_currency::Currency::USD)
149 /// .institution_name("Bank of Example".to_string())
150 /// .call()
151 /// .await?;
152 ///
153 /// println!("Created account: {}", account.name);
154 /// # Ok(())
155 /// # }
156 /// ```
157 #[builder]
158 pub async fn create_account(
159 &self,
160 name: String,
161 balance: Decimal,
162 attributes: AccountableAttributes,
163 currency: Option<iso_currency::Currency>,
164 institution_name: Option<String>,
165 institution_domain: Option<Url>,
166 notes: Option<String>,
167 ) -> ApiResult<AccountDetail> {
168 // Derive the account kind from the attributes
169 let kind = attributes.kind();
170
171 let request = CreateAccountRequest {
172 account: CreateAccountData {
173 name,
174 kind,
175 balance,
176 currency,
177 institution_name,
178 institution_domain,
179 notes,
180 accountable_attributes: attributes,
181 },
182 };
183
184 self.execute_request(
185 Method::POST,
186 "/api/v1/accounts",
187 None,
188 Some(serde_json::to_string(&request)?),
189 )
190 .await
191 }
192
193 /// Update an account
194 ///
195 /// Updates an existing account with new values. Only fields provided in the
196 /// request will be updated. If updating attributes, the entire attributes object
197 /// must be provided as it replaces the existing attributes.
198 ///
199 /// # Arguments
200 /// * `id` - The account ID to update
201 /// * `name` - Optional new account name
202 /// * `balance` - Optional new balance
203 /// * `institution_name` - Optional new institution name
204 /// * `institution_domain` - Optional new institution domain
205 /// * `notes` - Optional new notes
206 /// * `attributes` - Optional new account-specific attributes (replaces existing)
207 ///
208 /// # Returns
209 /// The updated account.
210 ///
211 /// # Errors
212 /// Returns `ApiError::NotFound` if the account doesn't exist.
213 /// Returns `ApiError::ValidationError` if the provided values are invalid.
214 /// Returns `ApiError::Unauthorized` if the API key is invalid.
215 /// Returns `ApiError::Network` if the request fails due to network issues.
216 ///
217 /// # Example
218 /// ```no_run
219 /// use sure_client_rs::{SureClient, BearerToken, AccountId};
220 /// use sure_client_rs::models::account::{
221 /// AccountableAttributes, DepositoryAttributes, DepositorySubtype
222 /// };
223 /// use rust_decimal::Decimal;
224 /// use uuid::Uuid;
225 ///
226 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
227 /// let account_id = AccountId::new(Uuid::new_v4());
228 ///
229 /// // Update just the name and balance
230 /// let account = client.update_account()
231 /// .id(&account_id)
232 /// .name("Updated Account Name".to_string())
233 /// .balance(Decimal::new(150000, 2)) // $1,500.00
234 /// .call()
235 /// .await?;
236 ///
237 /// // Update attributes
238 /// let updated = client.update_account()
239 /// .id(&account_id)
240 /// .attributes(AccountableAttributes::Depository(DepositoryAttributes {
241 /// subtype: Some(DepositorySubtype::Savings),
242 /// locked_attributes: None,
243 /// }))
244 /// .call()
245 /// .await?;
246 ///
247 /// println!("Updated account: {}", account.name);
248 /// # Ok(())
249 /// # }
250 /// ```
251 #[builder]
252 pub async fn update_account(
253 &self,
254 id: &AccountId,
255 name: Option<String>,
256 balance: Option<Decimal>,
257 institution_name: Option<String>,
258 institution_domain: Option<Url>,
259 notes: Option<String>,
260 attributes: Option<AccountableAttributes>,
261 ) -> ApiResult<AccountDetail> {
262 let request = UpdateAccountRequest {
263 account: UpdateAccountData {
264 name,
265 balance,
266 institution_name,
267 institution_domain,
268 notes,
269 accountable_attributes: attributes,
270 },
271 };
272
273 self.execute_request(
274 Method::PATCH,
275 &format!("/api/v1/accounts/{}", id),
276 None,
277 Some(serde_json::to_string(&request)?),
278 )
279 .await
280 }
281
282 /// Delete an account
283 ///
284 /// Permanently deletes an account.
285 ///
286 /// # Arguments
287 /// * `id` - The account ID to delete
288 ///
289 /// # Returns
290 /// A confirmation message.
291 ///
292 /// # Errors
293 /// Returns `ApiError::NotFound` if the account doesn't exist.
294 /// Returns `ApiError::ValidationError` if the account cannot be deleted (e.g., linked account).
295 /// Returns `ApiError::Unauthorized` if the API key is invalid.
296 /// Returns `ApiError::Network` if the request fails due to network issues.
297 ///
298 /// # Example
299 /// ```no_run
300 /// use sure_client_rs::{SureClient, BearerToken, AccountId};
301 /// use uuid::Uuid;
302 ///
303 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
304 /// let account_id = AccountId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
305 /// let response = client.delete_account(&account_id).await?;
306 ///
307 /// println!("Deleted: {}", response.message);
308 /// # Ok(())
309 /// # }
310 /// ```
311 pub async fn delete_account(&self, id: &AccountId) -> ApiResult<DeleteResponse> {
312 self.execute_request(
313 Method::DELETE,
314 &format!("/api/v1/accounts/{}", id),
315 None,
316 None,
317 )
318 .await
319 }
320}