From 430b17c92ee8e15bacce2c058d3e56f187da5fc3 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 11 Aug 2019 19:47:39 -0400 Subject: validate reconciled amounts, and also handle split transactions --- src/main.rs | 36 +++++++++++++++-- src/ynab/budget.rs | 103 ++++++++++++++++++++++++++++++++---------------- src/ynab/client.rs | 2 +- src/ynab/transaction.rs | 1 + 4 files changed, 103 insertions(+), 39 deletions(-) diff --git a/src/main.rs b/src/main.rs index 46f8042..0bba75a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,41 @@ fn main() { let key = std::env::args().nth(1).unwrap(); let client = ynab::Client::new(&key); let budget = client.default_budget(); + let reimbursables = budget.reimbursables(); println!("using budget {} ({})", budget.name(), budget.id()); - for t in budget.reimbursables() { - if t.reimbursed { - continue; - } + let reconciled_amount: i64 = reimbursables + .iter() + .filter(|t| t.reimbursed) + .map(|t| t.amount) + .sum(); + if reconciled_amount != 0 { + eprintln!( + "reconciled reimbursables don't sum to $0.00: ${}", + ynab::format_amount(reconciled_amount) + ); + std::process::exit(1); + } + println!("reconciled reimbursables correctly sum to $0.00"); + + println!("ready for reconciliation:"); + for t in reimbursables + .iter() + .filter(|t| !t.reimbursed && t.amount > 0) + { + println!( + "{} | {} | {}", + t.date, + t.payee, + ynab::format_amount(t.amount) + ) + } + println!("match against reconcilable:"); + for t in reimbursables + .iter() + .filter(|t| !t.reimbursed && t.amount <= 0) + { println!( "{} | {} | {}", t.date, diff --git a/src/ynab/budget.rs b/src/ynab/budget.rs index 85eab22..d31b803 100644 --- a/src/ynab/budget.rs +++ b/src/ynab/budget.rs @@ -1,14 +1,10 @@ -pub struct Budget<'a> { - api: &'a ynab_api::apis::client::APIClient, +pub struct Budget { budget: ynab_api::models::BudgetDetail, } -impl<'a> Budget<'a> { - pub fn new( - api: &'a ynab_api::apis::client::APIClient, - budget: ynab_api::models::BudgetDetail, - ) -> Self { - Self { api, budget } +impl Budget { + pub fn new(budget: ynab_api::models::BudgetDetail) -> Self { + Self { budget } } pub fn name(&self) -> String { @@ -20,35 +16,30 @@ impl<'a> Budget<'a> { } pub fn reimbursables(&self) -> Vec { - let reimbursables_id = self - .api - .categories_api() - .get_categories(&self.budget.id, 0) - .unwrap() - .data - .category_groups - .iter() - .map(|group| { - group - .categories + let reimbursables_id = + if let Some(categories) = &self.budget.categories { + categories .iter() - .map(|c| (c.id.clone(), c.name.clone())) - }) - .flat_map(|cs| cs) - .find(|(_, name)| name == "Reimbursables") - .map(|(id, _)| id) - .unwrap(); + .find(|c| c.name == "Reimbursables") + .map(|c| c.id.clone()) + .unwrap() + } else { + panic!("no categories found") + }; let mut reimbursables = vec![]; - if let Some(transactions) = &self.budget.transactions { - if let Some(payees) = &self.budget.payees { - let mut payee_map = std::collections::HashMap::new(); - for p in payees { - payee_map.insert(p.id.clone(), p.name.clone()); - } - let payee_map = payee_map; + if let Some(payees) = &self.budget.payees { + let mut payee_map = std::collections::HashMap::new(); + for p in payees { + payee_map.insert(p.id.clone(), p.name.clone()); + } + let payee_map = payee_map; + let mut transaction_map = std::collections::HashMap::new(); + if let Some(transactions) = &self.budget.transactions { for t in transactions { + transaction_map.insert(t.id.clone(), t); + if let Some(category_id) = &t.category_id { if category_id != &reimbursables_id { continue; @@ -73,14 +64,58 @@ impl<'a> Budget<'a> { date: t.date.clone(), payee, amount: t.amount, + total_amount: t.amount, + reimbursed, + }) + } + } + let transaction_map = transaction_map; + + if let Some(subtransactions) = &self.budget.subtransactions { + for st in subtransactions { + if let Some(category_id) = &st.category_id { + if category_id != &reimbursables_id { + continue; + } + } else { + continue; + } + + let t = transaction_map[&st.transaction_id]; + let payee = st + .payee_id + .iter() + .flat_map(|payee_id| payee_map.get(payee_id).cloned()) + .next() + .unwrap_or_else(|| { + t.payee_id + .iter() + .flat_map(|payee_id| { + payee_map.get(payee_id).cloned() + }) + .next() + .unwrap_or_else(|| "(none)".to_string()) + }); + let reimbursed = if let Some(color) = &t.flag_color { + color == "green" + } else { + false + }; + + reimbursables.push(super::transaction::Transaction { + date: t.date.clone(), + payee, + amount: st.amount, + total_amount: t.amount, reimbursed, }) } - } else { - panic!("no payees?"); } + } else { + panic!("no payees?"); } + reimbursables.sort_by_cached_key(|t| t.date.clone()); reimbursables } } diff --git a/src/ynab/client.rs b/src/ynab/client.rs index a2fbdb3..852481a 100644 --- a/src/ynab/client.rs +++ b/src/ynab/client.rs @@ -26,6 +26,6 @@ impl Client { .unwrap() .data .budget; - super::budget::Budget::new(&self.api, full_budget) + super::budget::Budget::new(full_budget) } } diff --git a/src/ynab/transaction.rs b/src/ynab/transaction.rs index 79209b5..d7903dc 100644 --- a/src/ynab/transaction.rs +++ b/src/ynab/transaction.rs @@ -3,5 +3,6 @@ pub struct Transaction { pub date: String, pub payee: String, pub amount: i64, + pub total_amount: i64, pub reimbursed: bool, } -- cgit v1.2.3