sure_client_rs/models/
transaction.rs

1use crate::types::{AccountId, CategoryId, MerchantId, TagId, TransactionId};
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6/// Account information
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
9pub struct Account {
10    /// Unique identifier
11    pub id: AccountId,
12    /// Account name
13    pub name: String,
14    /// Formatted balance (e.g. "$1,234.56")
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub balance: Option<String>,
17    /// Currency code (e.g. "USD")
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub currency: Option<iso_currency::Currency>,
20    /// Account classification (e.g. "asset", "liability")
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub classification: Option<String>,
23    /// Accountable type (e.g. "depository", "investment", "credit_card")
24    pub account_type: String,
25}
26
27/// Category information (basic version for transaction references)
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
30pub struct Category {
31    /// Unique identifier
32    pub id: CategoryId,
33    /// Category name
34    pub name: String,
35    /// Classification (income or expense)
36    pub classification: String,
37    /// Color for UI display (hex code)
38    pub color: String,
39    /// Icon identifier
40    pub icon: String,
41}
42
43/// Merchant information
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
46pub struct Merchant {
47    /// Unique identifier
48    pub id: MerchantId,
49    /// Merchant name
50    pub name: String,
51}
52
53/// Tag information
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
56pub struct Tag {
57    /// Unique identifier
58    pub id: TagId,
59    /// Tag name
60    pub name: String,
61    /// Color for UI display (hex code)
62    pub color: String,
63}
64
65/// Transfer information (for money transfers between accounts)
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
68pub struct Transfer {
69    /// Unique identifier
70    pub id: TransactionId,
71    /// Transfer amount
72    // #[serde(with = "rust_decimal::serde::arbitrary_precision")]
73    pub amount: String,
74    /// Currency code (e.g., "USD", "EUR")
75    pub currency: iso_currency::Currency,
76    /// The other account involved in the transfer
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub other_account: Option<Account>,
79}
80
81/// Transaction nature/type
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "lowercase")]
84pub enum TransactionNature {
85    /// Income transaction
86    #[serde(alias = "inflow")]
87    Income,
88    /// Expense transaction
89    #[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/// Error returned when parsing a `TransactionNature` from a string fails.
103#[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/// Transaction filter type
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum TransactionType {
146    /// Income transactions
147    Income,
148    /// Expense transactions
149    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/// Error returned when parsing a `TransactionType` from a string fails.
163#[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/// Complete transaction information
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
205pub struct Transaction {
206    /// Unique identifier
207    pub id: TransactionId,
208    /// Transaction date
209    #[serde(with = "crate::serde::naive_date")]
210    pub date: DateTime<Utc>,
211    /// Transaction amount
212    // #[serde(with = "rust_decimal::serde::arbitrary_precision")]
213    pub amount: String,
214    /// Currency code (e.g., "USD", "EUR")
215    pub currency: iso_currency::Currency,
216    /// Transaction name/description
217    pub name: String,
218    /// Additional notes
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub notes: Option<String>,
221    /// Classification (income/expense)
222    pub classification: String,
223    /// Associated account
224    pub account: Account,
225    /// Associated category
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub category: Option<Category>,
228    /// Associated merchant
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub merchant: Option<Merchant>,
231    /// Associated tags
232    pub tags: Vec<Tag>,
233    /// Associated transfer (if this is a transfer)
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub transfer: Option<Transfer>,
236    /// Creation timestamp
237    pub created_at: DateTime<Utc>,
238    /// Last update timestamp
239    pub updated_at: DateTime<Utc>,
240}
241
242/// Collection of transactions
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
245pub struct TransactionCollection {
246    /// List of transactions
247    pub transactions: Vec<Transaction>,
248}
249
250/// Request body for creating a transaction
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
253pub(crate) struct CreateTransactionRequest {
254    /// Transaction data
255    pub transaction: CreateTransactionData,
256}
257
258/// Transaction data for creation
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
261pub(crate) struct CreateTransactionData {
262    /// Account ID (required)
263    pub account_id: AccountId,
264    /// Transaction date (required)
265    pub date: DateTime<Utc>,
266    /// Transaction amount (required)
267    pub amount: Decimal,
268    /// Transaction name/description (required)
269    pub name: String,
270    /// Additional notes
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub notes: Option<String>,
273    /// Currency code
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub currency: Option<iso_currency::Currency>,
276    /// Category ID
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub category_id: Option<CategoryId>,
279    /// Merchant ID
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub merchant_id: Option<MerchantId>,
282    /// Transaction nature (determines sign)
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub nature: Option<TransactionNature>,
285    /// Tag IDs
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub tag_ids: Option<Vec<TagId>>,
288}
289
290/// Request body for updating a transaction
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
293pub(crate) struct UpdateTransactionRequest {
294    /// Transaction data
295    pub transaction: UpdateTransactionData,
296}
297
298/// Transaction data for updates
299#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
300#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
301pub(crate) struct UpdateTransactionData {
302    /// Transaction date
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub date: Option<DateTime<Utc>>,
305    /// Transaction amount
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub amount: Option<Decimal>,
308    /// Transaction name/description
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub name: Option<String>,
311    /// Additional notes
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub notes: Option<String>,
314    /// Currency code
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub currency: Option<iso_currency::Currency>,
317    /// Category ID
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub category_id: Option<CategoryId>,
320    /// Merchant ID
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub merchant_id: Option<MerchantId>,
323    /// Transaction nature
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub nature: Option<TransactionNature>,
326    /// Tag IDs
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub tag_ids: Option<Vec<TagId>>,
329}