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}