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}