1use crate::types::CategoryId;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Classification {
10 Income,
12 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
68pub struct Category {
69 pub id: CategoryId,
71 pub name: String,
73 pub classification: String,
75 pub color: String,
77 pub icon: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
84pub struct CategoryParent {
85 pub id: CategoryId,
87 pub name: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
94pub struct CategoryDetail {
95 pub id: CategoryId,
97 pub name: String,
99 pub classification: Classification,
101 pub color: String,
103 pub icon: String,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub parent: Option<CategoryParent>,
108 pub subcategories_count: u32,
110 pub created_at: DateTime<Utc>,
112 pub updated_at: DateTime<Utc>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
119pub struct CategoryCollection {
120 pub categories: Vec<CategoryDetail>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
127pub(crate) struct CreateCategoryRequest {
128 pub category: CreateCategoryData,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
135pub(crate) struct CreateCategoryData {
136 pub name: String,
138 pub classification: Classification,
140 pub color: String,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub lucide_icon: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub parent_id: Option<CategoryId>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
153pub(crate) struct UpdateCategoryRequest {
154 pub category: UpdateCategoryData,
156}
157
158#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
160#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
161pub(crate) struct UpdateCategoryData {
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub name: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub classification: Option<Classification>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub color: Option<String>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub lucide_icon: Option<String>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub parent_id: Option<CategoryId>,
177}