From e801bc38e6ef8746ea61dd2bc7fce59e3204049f Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sat, 17 Aug 2019 02:36:43 -0400 Subject: add ability to actually update transactions --- src/main.rs | 3 +- src/views/txn_table.rs | 44 ++++++++++++++++-------- src/ynab/budget.rs | 71 +++++++++++++++++++++----------------- src/ynab/client.rs | 20 +++++++++-- src/ynab/transaction.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 179 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index fbb078b..8ee7bc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ fn main() { .read_to_string(&mut key) .unwrap(); let client = ynab::Client::new(&key.trim()); - let budget = client.default_budget(); + let budget = client.into_default_budget(); checks::run_checks(&budget); @@ -32,6 +32,7 @@ fn main() { layout.add_child(views::txn_tables(&budget)); + app.set_user_data(budget); app.add_fullscreen_layer(layout); app.run(); } diff --git a/src/views/txn_table.rs b/src/views/txn_table.rs index 8cc927c..007b4e2 100644 --- a/src/views/txn_table.rs +++ b/src/views/txn_table.rs @@ -52,7 +52,9 @@ impl cursive_table_view::TableViewItem } } TxnColumn::Date => self.date.clone(), - TxnColumn::Payee => self.payee.clone(), + TxnColumn::Payee => { + self.payee.clone().unwrap_or_else(|| "".to_string()) + } TxnColumn::Amount => crate::ynab::format_amount(self.amount), TxnColumn::TotalAmount => { if self.amount == self.total_amount { @@ -114,33 +116,47 @@ fn txn_table( }) .default_column(TxnColumn::Date) .on_submit(move |s, _, _| { - let total_outflow = s - .call_on_id("outflows_table", |v: &mut TxnTableView| -> i64 { + let outflows: Vec<_> = s + .call_on_id("outflows_table", |v: &mut TxnTableView| { v.borrow_items() .iter() .filter(|t| t.selected) - .map(|t| t.amount) - .sum() + .cloned() + .collect() }) .unwrap(); - let total_inflow = s - .call_on_id("inflows_table", |v: &mut TxnTableView| -> i64 { + let inflows: Vec<_> = s + .call_on_id("inflows_table", |v: &mut TxnTableView| { v.borrow_items() .iter() .filter(|t| t.selected) - .map(|t| t.amount) - .sum() + .cloned() + .collect() }) .unwrap(); + let total_outflow: i64 = outflows.iter().map(|t| t.amount).sum(); + let total_inflow: i64 = inflows.iter().map(|t| t.amount).sum(); let total_amount = total_outflow + total_inflow; if total_amount == 0 { - s.add_layer(cursive::views::Dialog::info( - "success, sum is zero!", - )) + let budget: &mut crate::ynab::Budget = s.user_data().unwrap(); + let txns: Vec<_> = + outflows.iter().chain(inflows.iter()).collect(); + let err = budget.reconcile_transactions(&txns); + if let Some(err) = err { + s.add_layer(cursive::views::Dialog::info(format!( + "Error: {}", + err + ))) + } else { + s.add_layer(cursive::views::Dialog::info(format!( + "Successfully updated {} transactions", + txns.len() + ))) + } } else { s.add_layer(cursive::views::Dialog::info(format!( - "failed, sum is {}", - total_amount + "Selected amount is {}, must be 0", + crate::ynab::format_amount(total_amount) ))) } }) diff --git a/src/ynab/budget.rs b/src/ynab/budget.rs index 5d2741b..dfaaf81 100644 --- a/src/ynab/budget.rs +++ b/src/ynab/budget.rs @@ -1,12 +1,17 @@ pub struct Budget { + client: super::client::Client, budget: ynab_api::models::BudgetDetail, reimbursables: Vec, } impl Budget { - pub fn new(budget: ynab_api::models::BudgetDetail) -> Self { + pub fn new( + client: super::client::Client, + budget: ynab_api::models::BudgetDetail, + ) -> Self { let reimbursables = Self::get_reimbursables(&budget); Self { + client, budget, reimbursables, } @@ -24,6 +29,24 @@ impl Budget { &self.reimbursables } + pub fn reconcile_transactions( + &self, + txns: &[&super::transaction::Transaction], + ) -> Option { + let mut to_update = + ynab_api::models::UpdateTransactionsWrapper::new(); + to_update.transactions = Some( + txns.iter() + .map(|t| { + let mut ut = t.to_update_transaction(); + ut.flag_color = Some("green".to_string()); + ut + }) + .collect(), + ); + self.client.update_transactions(&self.budget.id, to_update) + } + fn get_reimbursables( budget: &ynab_api::models::BudgetDetail, ) -> Vec { @@ -61,23 +84,14 @@ impl Budget { let payee = t .payee_id .iter() - .flat_map(|payee_id| payee_map.get(payee_id).cloned()) + .map(|payee_id| payee_map.get(payee_id).cloned()) .next() - .unwrap_or_else(|| "".to_string()); - let reimbursed = if let Some(color) = &t.flag_color { - color == "green" - } else { - false - }; + .unwrap_or(None); - reimbursables.push(super::transaction::Transaction { - date: t.date.clone(), - payee, - amount: t.amount, - total_amount: t.amount, - reimbursed, - selected: false, - }) + let mut txn = + super::transaction::Transaction::from_transaction(t); + txn.payee = payee; + reimbursables.push(txn); } } let transaction_map = transaction_map; @@ -96,31 +110,24 @@ impl Budget { let payee = st .payee_id .iter() - .flat_map(|payee_id| payee_map.get(payee_id).cloned()) + .map(|payee_id| payee_map.get(payee_id).cloned()) .next() .unwrap_or_else(|| { t.payee_id .iter() - .flat_map(|payee_id| { + .map(|payee_id| { payee_map.get(payee_id).cloned() }) .next() - .unwrap_or_else(|| "".to_string()) + .unwrap_or(None) }); - 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, - selected: false, - }) + let mut txn = + super::transaction::Transaction::from_sub_transaction( + t, st, + ); + txn.payee = payee; + reimbursables.push(txn); } } } else { diff --git a/src/ynab/client.rs b/src/ynab/client.rs index 852481a..9467750 100644 --- a/src/ynab/client.rs +++ b/src/ynab/client.rs @@ -15,7 +15,7 @@ impl Client { } } - pub fn default_budget(&self) -> super::budget::Budget { + pub fn into_default_budget(self) -> super::budget::Budget { let budgets = self.api.budgets_api().get_budgets().unwrap().data.budgets; let budget = budgets.iter().next().unwrap(); @@ -26,6 +26,22 @@ impl Client { .unwrap() .data .budget; - super::budget::Budget::new(full_budget) + super::budget::Budget::new(self, full_budget) + } + + pub fn update_transactions( + &self, + budget_id: &str, + transactions: ynab_api::models::UpdateTransactionsWrapper, + ) -> Option { + let res = self + .api + .transactions_api() + .update_transactions(budget_id, transactions); + if let Err(e) = res { + Some(format!("{:?}", e)) + } else { + None + } } } diff --git a/src/ynab/transaction.rs b/src/ynab/transaction.rs index 326ee83..38e5d54 100644 --- a/src/ynab/transaction.rs +++ b/src/ynab/transaction.rs @@ -1,9 +1,98 @@ #[derive(Clone, Debug)] pub struct Transaction { + pub id: String, pub date: String, - pub payee: String, pub amount: i64, + pub memo: Option, + pub cleared: String, + pub approved: bool, + pub flag_color: Option, + pub account_id: String, + pub payee_id: Option, + pub category_id: Option, + pub import_id: Option, + + pub payee: Option, pub total_amount: i64, pub reimbursed: bool, pub selected: bool, } + +impl Transaction { + pub fn from_transaction( + t: &ynab_api::models::TransactionSummary, + ) -> Self { + let reimbursed = if let Some(color) = &t.flag_color { + color == "green" + } else { + false + }; + Self { + id: t.id.clone(), + date: t.date.clone(), + amount: t.amount, + memo: t.memo.clone(), + cleared: t.cleared.clone(), + approved: t.approved, + flag_color: t.flag_color.clone(), + account_id: t.account_id.clone(), + payee_id: t.payee_id.clone(), + category_id: t.category_id.clone(), + import_id: t.import_id.clone(), + + payee: None, + total_amount: t.amount, + reimbursed, + selected: false, + } + } + + pub fn from_sub_transaction( + t: &ynab_api::models::TransactionSummary, + st: &ynab_api::models::SubTransaction, + ) -> Self { + let reimbursed = if let Some(color) = &t.flag_color { + color == "green" + } else { + false + }; + Self { + id: t.id.clone(), + date: t.date.clone(), + amount: st.amount, + memo: t.memo.clone(), + cleared: t.cleared.clone(), + approved: t.approved, + flag_color: t.flag_color.clone(), + account_id: t.account_id.clone(), + payee_id: t.payee_id.clone(), + category_id: t.category_id.clone(), + import_id: t.import_id.clone(), + + payee: None, + total_amount: t.amount, + reimbursed, + selected: false, + } + } + + pub fn to_update_transaction( + &self, + ) -> ynab_api::models::UpdateTransaction { + let mut ut = ynab_api::models::UpdateTransaction::new( + self.account_id.clone(), + self.date.clone(), + self.amount, + ); + ut.id = Some(self.id.clone()); + ut.payee_id = self.payee_id.clone(); + ut.category_id = self.category_id.clone(); + ut.memo = self.memo.clone(); + ut.cleared = Some(self.cleared.clone()); + ut.approved = Some(self.approved); + ut.flag_color = self.flag_color.clone(); + ut.import_id = self.import_id.clone(); + + ut + } +} -- cgit v1.2.3