1use crate::types::{AccountId, CategoryId, MerchantId, TagId, TransactionId};
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
9pub struct Account {
10 pub id: AccountId,
12 pub name: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub balance: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub currency: Option<iso_currency::Currency>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub classification: Option<String>,
23 pub account_type: String,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
30pub struct Category {
31 pub id: CategoryId,
33 pub name: String,
35 pub classification: String,
37 pub color: String,
39 pub icon: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
46pub struct Merchant {
47 pub id: MerchantId,
49 pub name: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
56pub struct Tag {
57 pub id: TagId,
59 pub name: String,
61 pub color: String,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
68pub struct Transfer {
69 pub id: TransactionId,
71 pub amount: String,
74 pub currency: iso_currency::Currency,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub other_account: Option<Account>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "lowercase")]
84pub enum TransactionNature {
85 #[serde(alias = "inflow")]
87 Income,
88 #[serde(alias = "outflow")]
90 Expense,
91}
92
93impl std::fmt::Display for TransactionNature {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Income => write!(f, "income"),
97 Self::Expense => write!(f, "expense"),
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ParseTransactionNatureError(String);
105
106impl std::fmt::Display for ParseTransactionNatureError {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "Invalid transaction nature: {}", self.0)
109 }
110}
111
112impl std::error::Error for ParseTransactionNatureError {}
113
114impl std::str::FromStr for TransactionNature {
115 type Err = ParseTransactionNatureError;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s {
119 "income" | "inflow" => Ok(Self::Income),
120 "expense" | "outflow" => Ok(Self::Expense),
121 _ => Err(ParseTransactionNatureError(s.to_string())),
122 }
123 }
124}
125
126impl TryFrom<&str> for TransactionNature {
127 type Error = ParseTransactionNatureError;
128
129 fn try_from(value: &str) -> Result<Self, Self::Error> {
130 value.parse()
131 }
132}
133
134impl TryFrom<String> for TransactionNature {
135 type Error = ParseTransactionNatureError;
136
137 fn try_from(value: String) -> Result<Self, Self::Error> {
138 value.parse()
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum TransactionType {
146 Income,
148 Expense,
150}
151
152impl std::fmt::Display for TransactionType {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 let s = match self {
155 Self::Income => "income",
156 Self::Expense => "expense",
157 };
158 write!(f, "{}", s)
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ParseTransactionTypeError(String);
165
166impl std::fmt::Display for ParseTransactionTypeError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 write!(f, "Invalid transaction type: {}", self.0)
169 }
170}
171
172impl std::error::Error for ParseTransactionTypeError {}
173
174impl std::str::FromStr for TransactionType {
175 type Err = ParseTransactionTypeError;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 match s {
179 "income" => Ok(Self::Income),
180 "expense" => Ok(Self::Expense),
181 _ => Err(ParseTransactionTypeError(s.to_string())),
182 }
183 }
184}
185
186impl TryFrom<&str> for TransactionType {
187 type Error = ParseTransactionTypeError;
188
189 fn try_from(value: &str) -> Result<Self, Self::Error> {
190 value.parse()
191 }
192}
193
194impl TryFrom<String> for TransactionType {
195 type Error = ParseTransactionTypeError;
196
197 fn try_from(value: String) -> Result<Self, Self::Error> {
198 value.parse()
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
205pub struct Transaction {
206 pub id: TransactionId,
208 #[serde(with = "crate::serde::naive_date")]
210 pub date: DateTime<Utc>,
211 pub amount: String,
214 pub currency: iso_currency::Currency,
216 pub name: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub notes: Option<String>,
221 pub classification: String,
223 pub account: Account,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub category: Option<Category>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub merchant: Option<Merchant>,
231 pub tags: Vec<Tag>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub transfer: Option<Transfer>,
236 pub created_at: DateTime<Utc>,
238 pub updated_at: DateTime<Utc>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
245pub struct TransactionCollection {
246 pub transactions: Vec<Transaction>,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
253pub(crate) struct CreateTransactionRequest {
254 pub transaction: CreateTransactionData,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
261pub(crate) struct CreateTransactionData {
262 pub account_id: AccountId,
264 pub date: DateTime<Utc>,
266 pub amount: Decimal,
268 pub name: String,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub notes: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub currency: Option<iso_currency::Currency>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub category_id: Option<CategoryId>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub merchant_id: Option<MerchantId>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub nature: Option<TransactionNature>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub tag_ids: Option<Vec<TagId>>,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
293pub(crate) struct UpdateTransactionRequest {
294 pub transaction: UpdateTransactionData,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
300#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
301pub(crate) struct UpdateTransactionData {
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub date: Option<DateTime<Utc>>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub amount: Option<Decimal>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub name: Option<String>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub notes: Option<String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub currency: Option<iso_currency::Currency>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub category_id: Option<CategoryId>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub merchant_id: Option<MerchantId>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub nature: Option<TransactionNature>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub tag_ids: Option<Vec<TagId>>,
329}