sure_client_rs/client/transactions.rs
1use crate::ApiError;
2use crate::error::ApiResult;
3use crate::models::transaction::{
4 CreateTransactionData, CreateTransactionRequest, Transaction, TransactionCollection,
5 TransactionNature, TransactionType, UpdateTransactionData, UpdateTransactionRequest,
6};
7use crate::models::{DeleteResponse, PaginatedResponse};
8use crate::types::{AccountId, CategoryId, MerchantId, TagId, TransactionId};
9use bon::bon;
10use chrono::{DateTime, Utc};
11use reqwest::Method;
12use rust_decimal::Decimal;
13use std::collections::HashMap;
14
15use super::SureClient;
16
17const MAX_PER_PAGE: u32 = 100;
18
19#[bon]
20impl SureClient {
21 /// List transactions with optional filters
22 ///
23 /// Retrieves a paginated list of transactions. Results can be filtered by various criteria
24 /// including date range, amount, account, category, merchant, tags, and search text.
25 ///
26 /// # Arguments
27 /// * `page` - Page number (default: 1)
28 /// * `per_page` - Items per page (default: 25, max: 100)
29 /// * `account_id` - Filter by single account ID
30 /// * `account_ids` - Filter by multiple account IDs
31 /// * `category_id` - Filter by single category ID
32 /// * `category_ids` - Filter by multiple category IDs
33 /// * `merchant_id` - Filter by single merchant ID
34 /// * `merchant_ids` - Filter by multiple merchant IDs
35 /// * `tag_ids` - Filter by tag IDs
36 /// * `start_date` - Filter transactions from this date (inclusive)
37 /// * `end_date` - Filter transactions until this date (inclusive)
38 /// * `min_amount` - Filter by minimum amount
39 /// * `max_amount` - Filter by maximum amount
40 /// * `transaction_type` - Filter by transaction type (income or expense)
41 /// * `search` - Search by name, notes, or merchant name
42 ///
43 /// # Returns
44 /// A paginated response containing transactions and pagination metadata.
45 ///
46 /// # Errors
47 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
48 /// Returns `ApiError::Network` if the request fails due to network issues.
49 ///
50 /// # Example
51 /// ```no_run
52 /// use sure_client_rs::{SureClient, BearerToken};
53 /// use chrono::{DateTime, Utc};
54 ///
55 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
56 /// // Use defaults (page 1, per_page 25, no filters)
57 /// let response = client.get_transactions().call().await?;
58 ///
59 /// // Or customize with builder
60 /// use chrono::TimeZone;
61 /// let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
62 /// let end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
63 /// let response = client.get_transactions()
64 /// .page(2)
65 /// .per_page(50)
66 /// .start_date(&start)
67 /// .end_date(&end)
68 /// .search("coffee")
69 /// .call()
70 /// .await?;
71 /// # Ok(())
72 /// # }
73 /// ```
74 #[builder]
75 pub async fn get_transactions(
76 &self,
77 #[builder(default = 1)] page: u32,
78 #[builder(default = 25)] per_page: u32,
79 account_id: Option<&AccountId>,
80 account_ids: Option<&[AccountId]>,
81 category_id: Option<&CategoryId>,
82 category_ids: Option<&[CategoryId]>,
83 merchant_id: Option<&MerchantId>,
84 merchant_ids: Option<&[MerchantId]>,
85 tag_ids: Option<&[TagId]>,
86 start_date: Option<&DateTime<Utc>>,
87 end_date: Option<&DateTime<Utc>>,
88 min_amount: Option<Decimal>,
89 max_amount: Option<Decimal>,
90 transaction_type: Option<TransactionType>,
91 search: Option<&str>,
92 ) -> ApiResult<PaginatedResponse<TransactionCollection>> {
93 if per_page > MAX_PER_PAGE {
94 return Err(ApiError::InvalidParameter(format!(
95 "per_page cannot exceed {MAX_PER_PAGE}",
96 )));
97 }
98
99 let mut query_params = HashMap::new();
100
101 query_params.insert("page", page.to_string());
102 query_params.insert("per_page", per_page.to_string());
103
104 if let Some(account_id) = account_id {
105 query_params.insert("account_id", account_id.to_string());
106 }
107
108 if let Some(account_ids) = account_ids {
109 for id in account_ids {
110 query_params.insert("account_ids[]", id.to_string());
111 }
112 }
113
114 if let Some(category_id) = category_id {
115 query_params.insert("category_id", category_id.to_string());
116 }
117
118 if let Some(category_ids) = category_ids {
119 for id in category_ids {
120 query_params.insert("category_ids[]", id.to_string());
121 }
122 }
123
124 if let Some(merchant_id) = merchant_id {
125 query_params.insert("merchant_id", merchant_id.to_string());
126 }
127
128 if let Some(merchant_ids) = merchant_ids {
129 for id in merchant_ids {
130 query_params.insert("merchant_ids[]", id.to_string());
131 }
132 }
133
134 if let Some(tag_ids) = tag_ids {
135 for id in tag_ids {
136 query_params.insert("tag_ids[]", id.to_string());
137 }
138 }
139
140 if let Some(start_date) = start_date {
141 query_params.insert("start_date", start_date.format("%Y-%m-%d").to_string());
142 }
143
144 if let Some(end_date) = end_date {
145 query_params.insert("end_date", end_date.format("%Y-%m-%d").to_string());
146 }
147
148 if let Some(min_amount) = min_amount {
149 query_params.insert("min_amount", min_amount.to_string());
150 }
151
152 if let Some(max_amount) = max_amount {
153 query_params.insert("max_amount", max_amount.to_string());
154 }
155
156 if let Some(transaction_type) = transaction_type {
157 query_params.insert("type", transaction_type.to_string());
158 }
159
160 if let Some(search) = search {
161 query_params.insert("search", search.to_string());
162 }
163
164 self.execute_request(
165 Method::GET,
166 "/api/v1/transactions",
167 Some(&query_params),
168 None,
169 )
170 .await
171 }
172
173 /// Create a new transaction
174 ///
175 /// Creates a new transaction with the specified details.
176 ///
177 /// # Arguments
178 /// * `account_id` - Account ID (required)
179 /// * `date` - Transaction date (required)
180 /// * `amount` - Transaction amount (required)
181 /// * `name` - Transaction name/description (required)
182 /// * `notes` - Additional notes
183 /// * `currency` - Currency code (defaults to family currency)
184 /// * `category_id` - Category ID
185 /// * `merchant_id` - Merchant ID
186 /// * `nature` - Transaction nature (determines sign)
187 /// * `tag_ids` - Tag IDs
188 ///
189 /// # Returns
190 /// The newly created transaction.
191 ///
192 /// # Errors
193 /// Returns `ApiError::ValidationError` if required fields are missing or invalid.
194 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
195 /// Returns `ApiError::Network` if the request fails due to network issues.
196 ///
197 /// # Example
198 /// ```no_run
199 /// use sure_client_rs::{SureClient, BearerToken, AccountId};
200 /// use chrono::{DateTime, TimeZone, Utc};
201 /// use rust_decimal::Decimal;
202 /// use uuid::Uuid;
203 ///
204 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
205 /// let transaction = client.create_transaction()
206 /// .account_id(AccountId::new(Uuid::new_v4()))
207 /// .date(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap())
208 /// .amount(Decimal::new(4250, 2)) // $42.50
209 /// .name("Grocery Store".to_string())
210 /// .currency(iso_currency::Currency::USD)
211 /// .call()
212 /// .await?;
213 ///
214 /// println!("Created transaction: {}", transaction.id);
215 /// # Ok(())
216 /// # }
217 /// ```
218 ///
219 #[builder]
220 pub async fn create_transaction(
221 &self,
222 account_id: AccountId,
223 date: DateTime<Utc>,
224 amount: Decimal,
225 name: String,
226 notes: Option<String>,
227 currency: Option<iso_currency::Currency>,
228 category_id: Option<CategoryId>,
229 merchant_id: Option<MerchantId>,
230 nature: Option<TransactionNature>,
231 tag_ids: Option<Vec<TagId>>,
232 ) -> ApiResult<Transaction> {
233 let request = CreateTransactionRequest {
234 transaction: CreateTransactionData {
235 account_id,
236 date,
237 amount,
238 name,
239 notes,
240 currency,
241 category_id,
242 merchant_id,
243 nature,
244 tag_ids,
245 },
246 };
247
248 self.execute_request(
249 Method::POST,
250 "/api/v1/transactions",
251 None,
252 Some(serde_json::to_string(&request)?),
253 )
254 .await
255 }
256
257 /// Get a specific transaction by ID
258 ///
259 /// Retrieves detailed information about a single transaction.
260 ///
261 /// # Arguments
262 /// * `id` - The transaction ID to retrieve
263 ///
264 /// # Returns
265 /// Detailed transaction information.
266 ///
267 /// # Errors
268 /// Returns `ApiError::NotFound` if the transaction doesn't exist.
269 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
270 /// Returns `ApiError::Network` if the request fails due to network issues.
271 ///
272 /// # Example
273 /// ```no_run
274 /// use sure_client_rs::{SureClient, BearerToken, TransactionId};
275 /// use uuid::Uuid;
276 ///
277 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
278 /// let transaction_id = TransactionId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
279 /// let transaction = client.get_transaction(&transaction_id).await?;
280 ///
281 /// println!("Transaction: {}", transaction.name);
282 /// println!("Amount: {} {}", transaction.amount, transaction.currency);
283 /// # Ok(())
284 /// # }
285 /// ```
286 ///
287 pub async fn get_transaction(&self, id: &TransactionId) -> ApiResult<Transaction> {
288 self.execute_request(
289 Method::GET,
290 &format!("/api/v1/transactions/{}", id),
291 None,
292 None,
293 )
294 .await
295 }
296
297 /// Update a transaction
298 ///
299 /// Updates an existing transaction with new values. Only fields provided will be updated.
300 ///
301 /// # Arguments
302 /// * `id` - The transaction ID to update
303 /// * `date` - Transaction date
304 /// * `amount` - Transaction amount
305 /// * `name` - Transaction name/description
306 /// * `notes` - Additional notes
307 /// * `currency` - Currency code
308 /// * `category_id` - Category ID
309 /// * `merchant_id` - Merchant ID
310 /// * `nature` - Transaction nature
311 /// * `tag_ids` - Tag IDs
312 ///
313 /// # Returns
314 /// The updated transaction.
315 ///
316 /// # Errors
317 /// Returns `ApiError::NotFound` if the transaction doesn't exist.
318 /// Returns `ApiError::ValidationError` if the provided values are invalid.
319 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
320 /// Returns `ApiError::Network` if the request fails due to network issues.
321 ///
322 /// # Example
323 /// ```no_run
324 /// use sure_client_rs::{SureClient, BearerToken, TransactionId};
325 /// use rust_decimal::Decimal;
326 /// use uuid::Uuid;
327 ///
328 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
329 /// let transaction_id = TransactionId::new(Uuid::new_v4());
330 ///
331 /// let transaction = client.update_transaction()
332 /// .id(&transaction_id)
333 /// .amount(Decimal::new(5000, 2)) // Update to $50.00
334 /// .notes("Updated notes".to_string())
335 /// .call()
336 /// .await?;
337 ///
338 /// println!("Updated transaction: {}", transaction.id);
339 /// # Ok(())
340 /// # }
341 /// ```
342 ///
343 #[builder]
344 pub async fn update_transaction(
345 &self,
346 id: &TransactionId,
347 date: Option<DateTime<Utc>>,
348 amount: Option<Decimal>,
349 name: Option<String>,
350 notes: Option<String>,
351 currency: Option<iso_currency::Currency>,
352 category_id: Option<CategoryId>,
353 merchant_id: Option<MerchantId>,
354 nature: Option<TransactionNature>,
355 tag_ids: Option<Vec<TagId>>,
356 ) -> ApiResult<Transaction> {
357 let request = UpdateTransactionRequest {
358 transaction: UpdateTransactionData {
359 date,
360 amount,
361 name,
362 notes,
363 currency,
364 category_id,
365 merchant_id,
366 nature,
367 tag_ids,
368 },
369 };
370
371 self.execute_request(
372 Method::PATCH,
373 &format!("/api/v1/transactions/{}", id),
374 None,
375 Some(serde_json::to_string(&request)?),
376 )
377 .await
378 }
379
380 /// Delete a transaction
381 ///
382 /// Permanently deletes a transaction.
383 ///
384 /// # Arguments
385 /// * `id` - The transaction ID to delete
386 ///
387 /// # Returns
388 /// A confirmation message.
389 ///
390 /// # Errors
391 /// Returns `ApiError::NotFound` if the transaction doesn't exist.
392 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
393 /// Returns `ApiError::Network` if the request fails due to network issues.
394 ///
395 /// # Example
396 /// ```no_run
397 /// use sure_client_rs::{SureClient, BearerToken, TransactionId};
398 /// use uuid::Uuid;
399 ///
400 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
401 /// let transaction_id = TransactionId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
402 /// let response = client.delete_transaction(&transaction_id).await?;
403 ///
404 /// println!("Deleted: {}", response.message);
405 /// # Ok(())
406 /// # }
407 /// ```
408 ///
409 pub async fn delete_transaction(&self, id: &TransactionId) -> ApiResult<DeleteResponse> {
410 self.execute_request(
411 Method::DELETE,
412 &format!("/api/v1/transactions/{}", id),
413 None,
414 None,
415 )
416 .await
417 }
418}