Skip to main content

ynab_mcp/ynab/
models.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize, Serializer};
4use uuid::Uuid;
5
6// Custom serialization for amounts - converts milliunits to decimal currency amounts
7pub fn serialize_amount<S>(amount: &i64, serializer: S) -> Result<S::Ok, S::Error>
8where
9    S: Serializer,
10{
11    let decimal_amount = Decimal::from(*amount) / Decimal::from(1000);
12    serializer.serialize_str(&decimal_amount.to_string())
13}
14
15// Custom serialization for optional amounts
16pub fn serialize_amount_option<S>(amount: &Option<i64>, serializer: S) -> Result<S::Ok, S::Error>
17where
18    S: Serializer,
19{
20    match amount {
21        Some(amt) => {
22            let decimal_amount = Decimal::from(*amt) / Decimal::from(1000);
23            serializer.serialize_some(&decimal_amount.to_string())
24        }
25        None => serializer.serialize_none(),
26    }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ApiResponse<T> {
31    pub data: T,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct BudgetsResponse {
36    pub budgets: Vec<Budget>,
37    pub default_budget: Option<Budget>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BudgetResponse {
42    pub budget: Budget,
43    pub server_knowledge: i64,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct UserResponse {
48    pub user: User,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AccountsResponse {
53    pub accounts: Vec<Account>,
54    pub server_knowledge: i64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct AccountResponse {
59    pub account: Account,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CategoriesResponse {
64    pub category_groups: Vec<CategoryGroup>,
65    pub server_knowledge: i64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CategoryResponse {
70    pub category: Category,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct PayeesResponse {
75    pub payees: Vec<Payee>,
76    pub server_knowledge: i64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PayeeResponse {
81    pub payee: Payee,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct TransactionsResponse {
86    pub transactions: Vec<Transaction>,
87    pub server_knowledge: i64,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct TransactionResponse {
92    pub transaction: Transaction,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ScheduledTransactionsResponse {
97    pub scheduled_transactions: Vec<ScheduledTransaction>,
98    pub server_knowledge: i64,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ScheduledTransactionResponse {
103    pub scheduled_transaction: ScheduledTransaction,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct MonthsResponse {
108    pub months: Vec<Month>,
109    pub server_knowledge: i64,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct MonthResponse {
114    pub month: Month,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Budget {
119    pub id: Uuid,
120    pub name: String,
121    pub last_modified_on: Option<DateTime<Utc>>, // Optional in API
122    pub first_month: Option<NaiveDate>,          // Optional in API
123    pub last_month: Option<NaiveDate>,           // Optional in API
124    pub date_format: Option<DateFormat>,         // Optional and nullable in API
125    pub currency_format: Option<CurrencyFormat>, // Optional and nullable in API
126    pub accounts: Option<Vec<Account>>,
127    pub payees: Option<Vec<Payee>>,
128    pub payee_locations: Option<Vec<PayeeLocation>>,
129    pub category_groups: Option<Vec<CategoryGroup>>,
130    pub categories: Option<Vec<Category>>,
131    pub months: Option<Vec<Month>>,
132    pub transactions: Option<Vec<Transaction>>,
133    pub subtransactions: Option<Vec<SubTransaction>>,
134    pub scheduled_transactions: Option<Vec<ScheduledTransaction>>,
135    pub scheduled_subtransactions: Option<Vec<ScheduledSubTransaction>>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct DateFormat {
140    pub format: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct CurrencyFormat {
145    pub iso_code: String,
146    pub example_format: String,
147    pub decimal_digits: u32,
148    pub decimal_separator: String,
149    pub symbol_first: bool,
150    pub group_separator: String,
151    pub currency_symbol: String,
152    pub display_symbol: bool,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Account {
157    pub id: Uuid,
158    pub name: String,
159    #[serde(rename = "type")]
160    pub account_type: AccountType,
161    pub on_budget: bool,
162    pub closed: bool,
163    pub note: Option<String>,
164    #[serde(serialize_with = "serialize_amount")]
165    pub balance: i64, // milliunits
166    #[serde(serialize_with = "serialize_amount")]
167    pub cleared_balance: i64, // milliunits
168    #[serde(serialize_with = "serialize_amount")]
169    pub uncleared_balance: i64, // milliunits
170    pub transfer_payee_id: Option<Uuid>, // Can be null according to API
171    pub direct_import_linked: Option<bool>, // Optional field
172    pub direct_import_in_error: Option<bool>, // Optional field
173    pub last_reconciled_at: Option<DateTime<Utc>>,
174    #[serde(serialize_with = "serialize_amount_option")]
175    pub debt_original_balance: Option<i64>,
176    pub debt_interest_rates: Option<serde_json::Value>, // HashMap in TS
177    pub debt_minimum_payments: Option<serde_json::Value>, // HashMap in TS
178    pub debt_escrow_amounts: Option<serde_json::Value>, // HashMap in TS
179    pub deleted: bool,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub enum AccountType {
184    #[serde(rename = "checking")]
185    Checking,
186    #[serde(rename = "savings")]
187    Savings,
188    #[serde(rename = "creditCard")]
189    CreditCard,
190    #[serde(rename = "cash")]
191    Cash,
192    #[serde(rename = "lineOfCredit")]
193    LineOfCredit,
194    #[serde(rename = "otherAsset")]
195    OtherAsset,
196    #[serde(rename = "otherLiability")]
197    OtherLiability,
198    #[serde(rename = "mortgage")]
199    Mortgage,
200    #[serde(rename = "autoLoan")]
201    AutoLoan,
202    #[serde(rename = "studentLoan")]
203    StudentLoan,
204    #[serde(rename = "personalLoan")]
205    PersonalLoan,
206    #[serde(rename = "medicalDebt")]
207    MedicalDebt,
208    #[serde(rename = "otherDebt")]
209    OtherDebt,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct Payee {
214    pub id: Uuid,
215    pub name: String,
216    pub transfer_account_id: Option<Uuid>,
217    pub deleted: bool,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct PayeeLocation {
222    pub id: Uuid,
223    pub payee_id: Uuid,
224    pub latitude: String,  // Required field according to API
225    pub longitude: String, // Required field according to API
226    pub deleted: bool,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct CategoryGroup {
231    pub id: Uuid,
232    pub name: String,
233    pub hidden: bool,
234    pub deleted: bool,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Category {
239    pub id: Uuid,
240    pub category_group_id: Uuid,
241    pub category_group_name: Option<String>,
242    pub name: String,
243    pub hidden: bool,
244    pub original_category_group_id: Option<Uuid>,
245    pub note: Option<String>,
246    #[serde(serialize_with = "serialize_amount")]
247    pub budgeted: i64, // milliunits
248    #[serde(serialize_with = "serialize_amount")]
249    pub activity: i64, // milliunits
250    #[serde(serialize_with = "serialize_amount")]
251    pub balance: i64, // milliunits
252    pub goal_type: Option<String>, // CategoryGoalTypeEnum in TS
253    pub goal_needs_whole_amount: Option<bool>,
254    pub goal_day: Option<u32>,
255    pub goal_cadence: Option<u32>,
256    pub goal_cadence_frequency: Option<u32>,
257    pub goal_creation_month: Option<String>, // String in API, not NaiveDate
258    pub goal_target: Option<i64>,
259    pub goal_target_month: Option<String>, // String in API, not NaiveDate
260    pub goal_percentage_complete: Option<u32>,
261    pub goal_months_to_budget: Option<u32>,
262    pub goal_under_funded: Option<i64>,
263    pub goal_overall_funded: Option<i64>,
264    pub goal_overall_left: Option<i64>,
265    pub goal_snoozed_at: Option<String>, // Missing field from API
266    pub deleted: bool,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Month {
271    pub month: NaiveDate,
272    pub note: Option<String>,
273    #[serde(serialize_with = "serialize_amount")]
274    pub income: i64, // milliunits
275    #[serde(serialize_with = "serialize_amount")]
276    pub budgeted: i64, // milliunits
277    #[serde(serialize_with = "serialize_amount")]
278    pub activity: i64, // milliunits
279    #[serde(serialize_with = "serialize_amount")]
280    pub to_be_budgeted: i64, // milliunits
281    pub age_of_money: Option<u32>,
282    pub deleted: bool,
283    pub categories: Option<Vec<Category>>, // Not always present in all endpoints
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Transaction {
288    pub id: Uuid,
289    pub date: NaiveDate,
290    #[serde(serialize_with = "serialize_amount")]
291    pub amount: i64, // milliunits
292    pub memo: Option<String>,
293    pub cleared: ClearState,
294    pub approved: bool,
295    pub flag_color: Option<String>,
296    pub flag_name: Option<String>,
297    pub account_id: Uuid,
298    pub payee_id: Option<Uuid>,
299    pub category_id: Option<Uuid>,
300    pub transfer_account_id: Option<Uuid>,
301    pub transfer_transaction_id: Option<Uuid>,
302    pub matched_transaction_id: Option<Uuid>,
303    pub import_id: Option<String>,
304    pub import_payee_name: Option<String>,
305    pub import_payee_name_original: Option<String>,
306    pub debt_transaction_type: Option<String>,
307    pub deleted: bool,
308    pub account_name: String,
309    pub payee_name: Option<String>,
310    pub category_name: Option<String>,
311    pub subtransactions: Option<Vec<SubTransaction>>, // Present in TransactionDetail only
312    // HybridTransaction specific fields
313    #[serde(rename = "type")]
314    pub transaction_type: Option<String>,
315    pub parent_transaction_id: Option<Uuid>,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319pub enum ClearState {
320    #[serde(rename = "cleared")]
321    Cleared,
322    #[serde(rename = "uncleared")]
323    Uncleared,
324    #[serde(rename = "reconciled")]
325    Reconciled,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct SubTransaction {
330    pub id: Uuid,
331    pub transaction_id: Uuid,
332    #[serde(serialize_with = "serialize_amount")]
333    pub amount: i64, // milliunits
334    pub memo: Option<String>,
335    pub payee_id: Option<Uuid>,
336    pub payee_name: Option<String>,
337    pub category_id: Option<Uuid>,
338    pub category_name: Option<String>,
339    pub transfer_account_id: Option<Uuid>,
340    pub transfer_transaction_id: Option<Uuid>,
341    pub deleted: bool,
342}
343
344impl Transaction {
345    /// Fix amount inconsistencies by checking the import_id for the correct amount
346    pub fn fix_amount_if_needed(&mut self) {
347        if let Some(import_id) = &self.import_id {
348            // Parse import_id format: "YNAB:{amount}:{date}:{sequence}"
349            let parts: Vec<&str> = import_id.split(':').collect();
350            if parts.len() >= 2 && parts[0] == "YNAB" {
351                if let Ok(expected_milliunits) = parts[1].parse::<i64>() {
352                    // Check if the current amount seems wrong (too small compared to import_id)
353                    let current_abs = self.amount.abs();
354                    let expected_abs = expected_milliunits.abs();
355
356                    // If the expected amount is significantly larger, assume API returned wrong format
357                    if expected_abs >= 1000 && current_abs < 100 {
358                        // Likely the API returned currency units instead of milliunits
359                        // Use the import_id amount as the source of truth
360                        self.amount = expected_milliunits;
361                    }
362                }
363            }
364        }
365    }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct ScheduledTransaction {
370    pub id: Uuid,
371    pub date_first: NaiveDate,
372    pub date_next: NaiveDate, // Missing from original model
373    pub frequency: String,    // ScheduledTransactionSummaryFrequencyEnum in TS
374    #[serde(serialize_with = "serialize_amount")]
375    pub amount: i64, // milliunits
376    pub memo: Option<String>,
377    pub flag_color: Option<String>,
378    pub flag_name: Option<String>,
379    pub account_id: Uuid,
380    pub payee_id: Option<Uuid>,
381    pub category_id: Option<Uuid>,
382    pub transfer_account_id: Option<Uuid>,
383    pub deleted: bool,
384    pub account_name: String, // Not in API spec - likely added in detailed responses
385    pub payee_name: Option<String>, // Not in API spec - likely added in detailed responses
386    pub category_name: Option<String>, // Not in API spec - likely added in detailed responses
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ScheduledSubTransaction {
391    pub id: Uuid,
392    pub scheduled_transaction_id: Uuid,
393    #[serde(serialize_with = "serialize_amount")]
394    pub amount: i64, // milliunits
395    pub memo: Option<String>,
396    pub payee_id: Option<Uuid>,
397    pub category_id: Option<Uuid>,
398    pub transfer_account_id: Option<Uuid>,
399    pub deleted: bool,
400    pub payee_name: Option<String>,
401    pub category_name: Option<String>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct User {
406    pub id: Uuid,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct TransactionDetail {
411    pub account_id: Uuid,
412    pub payee_id: Option<Uuid>,
413    pub category_id: Option<Uuid>,
414    pub amount: i64, // milliunits (YNAB API expects raw integer milliunits)
415    pub memo: Option<String>,
416    pub cleared: Option<ClearState>,
417    pub approved: Option<bool>,
418    pub flag_color: Option<String>,
419    pub flag_name: Option<String>,
420    pub date: NaiveDate,
421    pub import_id: Option<String>,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct NewTransaction {
426    pub transactions: Vec<TransactionDetail>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct UpdateTransaction {
431    pub transaction: TransactionDetail,
432}