sure_client_rs/models/
category.rs

1use crate::types::CategoryId;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Classification of a category
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Classification {
10    /// Income category
11    Income,
12    /// Expense category
13    Expense,
14}
15
16impl fmt::Display for Classification {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Income => write!(f, "income"),
20            Self::Expense => write!(f, "expense"),
21        }
22    }
23}
24
25/// Error returned when parsing a `Classification` from a string fails.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ParseClassificationError(String);
28
29impl fmt::Display for ParseClassificationError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "Invalid classification: {}", self.0)
32    }
33}
34
35impl std::error::Error for ParseClassificationError {}
36
37impl std::str::FromStr for Classification {
38    type Err = ParseClassificationError;
39
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        match s {
42            "income" => Ok(Self::Income),
43            "expense" => Ok(Self::Expense),
44            _ => Err(ParseClassificationError(s.to_string())),
45        }
46    }
47}
48
49impl TryFrom<&str> for Classification {
50    type Error = ParseClassificationError;
51
52    fn try_from(value: &str) -> Result<Self, Self::Error> {
53        value.parse()
54    }
55}
56
57impl TryFrom<String> for Classification {
58    type Error = ParseClassificationError;
59
60    fn try_from(value: String) -> Result<Self, Self::Error> {
61        value.parse()
62    }
63}
64
65/// Basic category information
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
68pub struct Category {
69    /// Unique identifier
70    pub id: CategoryId,
71    /// Category name
72    pub name: String,
73    /// Classification (income or expense)
74    pub classification: String,
75    /// Color for UI display (hex code)
76    pub color: String,
77    /// Icon identifier
78    pub icon: String,
79}
80
81/// Parent category reference
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
84pub struct CategoryParent {
85    /// Parent category ID
86    pub id: CategoryId,
87    /// Parent category name
88    pub name: String,
89}
90
91/// Detailed category information
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
94pub struct CategoryDetail {
95    /// Unique identifier
96    pub id: CategoryId,
97    /// Category name
98    pub name: String,
99    /// Classification (income or expense)
100    pub classification: Classification,
101    /// Color for UI display (hex code)
102    pub color: String,
103    /// Icon identifier
104    pub icon: String,
105    /// Parent category (if this is a subcategory)
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub parent: Option<CategoryParent>,
108    /// Number of subcategories
109    pub subcategories_count: u32,
110    /// Creation timestamp
111    pub created_at: DateTime<Utc>,
112    /// Last update timestamp
113    pub updated_at: DateTime<Utc>,
114}
115
116/// Collection of categories with pagination
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
119pub struct CategoryCollection {
120    /// List of categories
121    pub categories: Vec<CategoryDetail>,
122}
123
124/// Request to create a new category
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
127pub(crate) struct CreateCategoryRequest {
128    /// Category data
129    pub category: CreateCategoryData,
130}
131
132/// Data for creating a new category
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
135pub(crate) struct CreateCategoryData {
136    /// Category name
137    pub name: String,
138    /// Classification (income or expense)
139    pub classification: Classification,
140    /// Color for UI display (hex code)
141    pub color: String,
142    /// Lucide icon name
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub lucide_icon: Option<String>,
145    /// Parent category ID for subcategories
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub parent_id: Option<CategoryId>,
148}
149
150/// Request to update an existing category
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
153pub(crate) struct UpdateCategoryRequest {
154    /// Category data
155    pub category: UpdateCategoryData,
156}
157
158/// Data for updating a category
159#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
160#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
161pub(crate) struct UpdateCategoryData {
162    /// Category name
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub name: Option<String>,
165    /// Classification (income or expense)
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub classification: Option<Classification>,
168    /// Color for UI display (hex code)
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub color: Option<String>,
171    /// Lucide icon name
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub lucide_icon: Option<String>,
174    /// Parent category ID for subcategories
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub parent_id: Option<CategoryId>,
177}