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 color: String,
37 pub icon: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub classification: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
49pub struct Merchant {
50 pub id: MerchantId,
52 pub name: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
59pub struct Tag {
60 pub id: TagId,
62 pub name: String,
64 pub color: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
71pub struct Transfer {
72 pub id: TransactionId,
74 pub amount: String,
77 pub currency: iso_currency::Currency,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub other_account: Option<Account>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87pub enum TransactionNature {
88 #[serde(alias = "inflow")]
90 Income,
91 #[serde(alias = "outflow")]
93 Expense,
94}
95
96impl std::fmt::Display for TransactionNature {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 Self::Income => write!(f, "income"),
100 Self::Expense => write!(f, "expense"),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ParseTransactionNatureError(String);
108
109impl std::fmt::Display for ParseTransactionNatureError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 write!(f, "Invalid transaction nature: {}", self.0)
112 }
113}
114
115impl std::error::Error for ParseTransactionNatureError {}
116
117impl std::str::FromStr for TransactionNature {
118 type Err = ParseTransactionNatureError;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 match s {
122 "income" | "inflow" => Ok(Self::Income),
123 "expense" | "outflow" => Ok(Self::Expense),
124 _ => Err(ParseTransactionNatureError(s.to_string())),
125 }
126 }
127}
128
129impl TryFrom<&str> for TransactionNature {
130 type Error = ParseTransactionNatureError;
131
132 fn try_from(value: &str) -> Result<Self, Self::Error> {
133 value.parse()
134 }
135}
136
137impl TryFrom<String> for TransactionNature {
138 type Error = ParseTransactionNatureError;
139
140 fn try_from(value: String) -> Result<Self, Self::Error> {
141 value.parse()
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "lowercase")]
148pub enum TransactionType {
149 Income,
151 Expense,
153}
154
155impl std::fmt::Display for TransactionType {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 let s = match self {
158 Self::Income => "income",
159 Self::Expense => "expense",
160 };
161 write!(f, "{}", s)
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ParseTransactionTypeError(String);
168
169impl std::fmt::Display for ParseTransactionTypeError {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 write!(f, "Invalid transaction type: {}", self.0)
172 }
173}
174
175impl std::error::Error for ParseTransactionTypeError {}
176
177impl std::str::FromStr for TransactionType {
178 type Err = ParseTransactionTypeError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 match s {
182 "income" => Ok(Self::Income),
183 "expense" => Ok(Self::Expense),
184 _ => Err(ParseTransactionTypeError(s.to_string())),
185 }
186 }
187}
188
189impl TryFrom<&str> for TransactionType {
190 type Error = ParseTransactionTypeError;
191
192 fn try_from(value: &str) -> Result<Self, Self::Error> {
193 value.parse()
194 }
195}
196
197impl TryFrom<String> for TransactionType {
198 type Error = ParseTransactionTypeError;
199
200 fn try_from(value: String) -> Result<Self, Self::Error> {
201 value.parse()
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
208pub struct Transaction {
209 pub id: TransactionId,
211 #[serde(with = "crate::serde::naive_date")]
213 pub date: DateTime<Utc>,
214 pub amount: String,
216 pub amount_cents: i64,
218 pub signed_amount_cents: i64,
221 pub currency: iso_currency::Currency,
223 pub name: String,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub notes: Option<String>,
228 pub classification: String,
230 pub account: Account,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub category: Option<Category>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub merchant: Option<Merchant>,
238 pub tags: Vec<Tag>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub transfer: Option<Transfer>,
243 pub created_at: DateTime<Utc>,
245 pub updated_at: DateTime<Utc>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
252pub struct TransactionCollection {
253 pub transactions: Vec<Transaction>,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
260pub(crate) struct CreateTransactionRequest {
261 pub transaction: CreateTransactionData,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
268pub(crate) struct CreateTransactionData {
269 pub account_id: AccountId,
271 pub date: DateTime<Utc>,
273 pub amount: Decimal,
275 pub name: String,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub notes: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub currency: Option<iso_currency::Currency>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub category_id: Option<CategoryId>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub merchant_id: Option<MerchantId>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub nature: Option<TransactionNature>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub tag_ids: Option<Vec<TagId>>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
300pub(crate) struct UpdateTransactionRequest {
301 pub transaction: UpdateTransactionData,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
307#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
308pub(crate) struct UpdateTransactionData {
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub date: Option<DateTime<Utc>>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub amount: Option<Decimal>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub name: Option<String>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub notes: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub currency: Option<iso_currency::Currency>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub category_id: Option<CategoryId>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub merchant_id: Option<MerchantId>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub nature: Option<TransactionNature>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub tag_ids: Option<Vec<TagId>>,
336}