sure_client_rs/client/
categories.rs

1use bon::bon;
2use reqwest::Method;
3
4use crate::ApiError;
5use crate::error::ApiResult;
6use crate::models::category::{
7    CategoryCollection, CategoryDetail, Classification, CreateCategoryData, CreateCategoryRequest,
8    UpdateCategoryData, UpdateCategoryRequest,
9};
10use crate::models::{DeleteResponse, PaginatedResponse};
11use crate::types::CategoryId;
12use std::collections::HashMap;
13
14use super::SureClient;
15
16const MAX_PER_PAGE: u32 = 100;
17
18#[bon]
19impl SureClient {
20    /// List categories with optional filters
21    ///
22    /// Retrieves a paginated list of categories. Results can be filtered by classification,
23    /// parent category, or limited to root categories only.
24    ///
25    /// # Arguments
26    /// * `page` - Page number (default: 1)
27    /// * `per_page` - Items per page (default: 25, max: 100)
28    /// * `classification` - Filter by classification (income or expense)
29    /// * `roots_only` - Return only root categories (default: false)
30    /// * `parent_id` - Filter by parent category ID
31    ///
32    /// # Returns
33    /// A paginated response containing categories and pagination metadata.
34    ///
35    /// # Errors
36    /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
37    /// Returns `ApiError::Network` if the request fails due to network issues.
38    #[builder]
39    pub async fn get_categories(
40        &self,
41        #[builder(default = 1)] page: u32,
42        #[builder(default = 25)] per_page: u32,
43        #[builder(default = false)] roots_only: bool,
44        classification: Option<Classification>,
45        parent_id: Option<&CategoryId>,
46    ) -> ApiResult<PaginatedResponse<CategoryCollection>> {
47        let mut query_params = HashMap::new();
48
49        if per_page > MAX_PER_PAGE {
50            return Err(ApiError::InvalidParameter(format!(
51                "per_page cannot exceed {MAX_PER_PAGE}",
52            )));
53        }
54
55        query_params.insert("page", page.to_string());
56        query_params.insert("per_page", per_page.to_string());
57        query_params.insert("roots_only", roots_only.to_string());
58
59        if let Some(classification) = classification {
60            query_params.insert("classification", classification.to_string());
61        }
62
63        if let Some(parent_id) = parent_id {
64            query_params.insert("parent_id", parent_id.to_string());
65        }
66
67        self.execute_request(Method::GET, "/api/v1/categories", Some(&query_params), None)
68            .await
69    }
70
71    /// Get a specific category by ID
72    ///
73    /// Retrieves detailed information about a single category, including parent
74    /// and subcategory information.
75    ///
76    /// # Arguments
77    /// * `id` - The category ID to retrieve
78    ///
79    /// # Returns
80    /// Detailed category information including parent and subcategory count.
81    ///
82    /// # Errors
83    /// Returns `ApiError::NotFound` if the category doesn't exist.
84    /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
85    /// Returns `ApiError::Network` if the request fails due to network issues.
86    pub async fn get_category(&self, id: &CategoryId) -> ApiResult<CategoryDetail> {
87        self.execute_request(
88            Method::GET,
89            &format!("/api/v1/categories/{}", id),
90            None,
91            None,
92        )
93        .await
94    }
95}
96
97#[bon]
98impl SureClient {
99    /// Create a new category
100    ///
101    /// Creates a new category with the specified details.
102    ///
103    /// # Arguments
104    /// * `request` - The category creation request containing all required fields
105    ///
106    /// # Returns
107    /// The newly created category with full details.
108    ///
109    /// # Errors
110    /// Returns `ApiError::ValidationError` if required fields are missing or invalid.
111    /// Returns `ApiError::Unauthorized` if the API key is invalid.
112    /// Returns `ApiError::Network` if the request fails due to network issues.
113    ///
114    /// # Example
115    /// ```no_run
116    /// use sure_client_rs::{SureClient, BearerToken};
117    /// use sure_client_rs::models::category::Classification;
118    ///
119    /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
120    /// let category = client.create_category()
121    ///     .name("Groceries".to_string())
122    ///     .classification(Classification::Expense)
123    ///     .color("#FF5733".to_string())
124    ///     .lucide_icon("shopping-cart".to_string())
125    ///     .call()
126    ///     .await?;
127    ///
128    /// println!("Created category: {}", category.name);
129    /// # Ok(())
130    /// # }
131    /// ```
132    #[builder]
133    pub async fn create_category(
134        &self,
135        name: String,
136        classification: Classification,
137        color: String,
138        lucide_icon: Option<String>,
139        parent_id: Option<CategoryId>,
140    ) -> ApiResult<CategoryDetail> {
141        let request = CreateCategoryRequest {
142            category: CreateCategoryData {
143                name,
144                classification,
145                color,
146                lucide_icon,
147                parent_id,
148            },
149        };
150
151        self.execute_request(
152            Method::POST,
153            "/api/v1/categories",
154            None,
155            Some(serde_json::to_string(&request)?),
156        )
157        .await
158    }
159
160    /// Update a category
161    ///
162    /// Updates an existing category with new values. Only fields provided in the
163    /// request will be updated.
164    ///
165    /// # Arguments
166    /// * `id` - The category ID to update
167    /// * `request` - The category update request containing fields to update
168    ///
169    /// # Returns
170    /// The updated category.
171    ///
172    /// # Errors
173    /// Returns `ApiError::NotFound` if the category doesn't exist.
174    /// Returns `ApiError::ValidationError` if the provided values are invalid.
175    /// Returns `ApiError::Unauthorized` if the API key is invalid.
176    /// Returns `ApiError::Network` if the request fails due to network issues.
177    ///
178    /// # Example
179    /// ```no_run
180    /// use sure_client_rs::{SureClient, BearerToken, CategoryId};
181    /// use uuid::Uuid;
182    ///
183    /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
184    /// let category_id = CategoryId::new(Uuid::new_v4());
185    ///
186    /// let category = client.update_category()
187    ///     .id(&category_id)
188    ///     .name("Updated Category Name".to_string())
189    ///     .color("#00FF00".to_string())
190    ///     .call()
191    ///     .await?;
192    ///
193    /// println!("Updated category: {}", category.name);
194    /// # Ok(())
195    /// # }
196    /// ```
197    #[builder]
198    pub async fn update_category(
199        &self,
200        id: &CategoryId,
201        name: Option<String>,
202        classification: Option<Classification>,
203        color: Option<String>,
204        lucide_icon: Option<String>,
205        parent_id: Option<CategoryId>,
206    ) -> ApiResult<CategoryDetail> {
207        let request = UpdateCategoryRequest {
208            category: UpdateCategoryData {
209                name,
210                classification,
211                color,
212                lucide_icon,
213                parent_id,
214            },
215        };
216
217        self.execute_request(
218            Method::PATCH,
219            &format!("/api/v1/categories/{}", id),
220            None,
221            Some(serde_json::to_string(&request)?),
222        )
223        .await
224    }
225
226    /// Delete a category
227    ///
228    /// Permanently deletes a category.
229    ///
230    /// # Arguments
231    /// * `id` - The category ID to delete
232    ///
233    /// # Returns
234    /// A confirmation message.
235    ///
236    /// # Errors
237    /// Returns `ApiError::NotFound` if the category doesn't exist.
238    /// Returns `ApiError::Unauthorized` if the API key is invalid.
239    /// Returns `ApiError::Network` if the request fails due to network issues.
240    ///
241    /// # Example
242    /// ```no_run
243    /// use sure_client_rs::{SureClient, BearerToken, CategoryId};
244    /// use uuid::Uuid;
245    ///
246    /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
247    /// let category_id = CategoryId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
248    /// let response = client.delete_category(&category_id).await?;
249    ///
250    /// println!("Deleted: {}", response.message);
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub async fn delete_category(&self, id: &CategoryId) -> ApiResult<DeleteResponse> {
255        self.execute_request(
256            Method::DELETE,
257            &format!("/api/v1/categories/{}", id),
258            None,
259            None,
260        )
261        .await
262    }
263}