diff options
Diffstat (limited to 'src/ynab')
-rw-r--r-- | src/ynab/budget.rs | 216 | ||||
-rw-r--r-- | src/ynab/client.rs | 76 | ||||
-rw-r--r-- | src/ynab/transaction.rs | 101 | ||||
-rw-r--r-- | src/ynab/util.rs | 6 |
4 files changed, 0 insertions, 399 deletions
diff --git a/src/ynab/budget.rs b/src/ynab/budget.rs deleted file mode 100644 index e3275bb..0000000 --- a/src/ynab/budget.rs +++ /dev/null @@ -1,216 +0,0 @@ -use snafu::{OptionExt, ResultExt}; - -#[derive(Debug, snafu::Snafu)] -pub enum Error { - #[snafu(display("couldn't get default budget: {}", source))] - GetBudget { source: super::client::Error }, - - #[snafu(display("couldn't update transactions: {}", source))] - UpdateTransactions { source: super::client::Error }, - - #[snafu(display("couldn't find the reimbursables category"))] - FindReimbursablesCategory, -} - -pub type Result<T> = std::result::Result<T, Error>; - -pub struct Budget { - client: super::client::Client, - id: String, - name: String, - reimbursables: Vec<super::transaction::Transaction>, -} - -impl Budget { - pub fn new(key: &str) -> Result<Self> { - let client = super::client::Client::new(key); - let budget = client.default_budget().context(GetBudget)?; - let reimbursables = Self::get_reimbursables(&budget)?; - let budget = Self { - client, - id: budget.id.clone(), - name: budget.name.clone(), - reimbursables, - }; - budget.check(); - Ok(budget) - } - - #[must_use] - pub fn refresh(&mut self) -> Result<()> { - let budget = self.client.default_budget().context(GetBudget)?; - self.id = budget.id.clone(); - self.name = budget.name.clone(); - self.reimbursables = Self::get_reimbursables(&budget)?; - self.check(); - Ok(()) - } - - pub fn name(&self) -> String { - self.name.clone() - } - - pub fn id(&self) -> String { - self.id.clone() - } - - pub fn reimbursables(&self) -> &[super::transaction::Transaction] { - &self.reimbursables - } - - #[must_use] - pub fn reconcile_transactions( - &self, - txns: &[&super::transaction::Transaction], - ) -> Result<()> { - 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.id, to_update) - .context(UpdateTransactions)?; - Ok(()) - } - - fn get_reimbursables( - budget: &ynab_api::models::BudgetDetail, - ) -> Result<Vec<super::transaction::Transaction>> { - let reimbursables_id = budget - .categories - .as_ref() - .and_then(|categories| { - categories - .iter() - .find(|c| c.name == "Reimbursables") - .map(|c| c.id.clone()) - }) - .context(FindReimbursablesCategory)?; - - let mut payee_map = std::collections::HashMap::new(); - if let Some(payees) = &budget.payees { - for p in payees { - payee_map.insert(p.id.clone(), p.name.clone()); - } - } - let payee_map = payee_map; - - let mut account_map = std::collections::HashMap::new(); - if let Some(accounts) = &budget.accounts { - for a in accounts { - account_map.insert(a.id.clone(), a.name.clone()); - } - } - let account_map = account_map; - - let mut reimbursables = vec![]; - - let mut transaction_map = std::collections::HashMap::new(); - if let Some(transactions) = &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; - } - } else { - continue; - } - - let payee = t - .payee_id - .iter() - .map(|payee_id| payee_map.get(payee_id).cloned()) - .next() - .unwrap_or(None); - let account = account_map.get(&t.account_id).cloned(); - - let mut txn = - super::transaction::Transaction::from_transaction(t); - txn.payee = payee; - txn.account = account; - reimbursables.push(txn); - } - } - let transaction_map = transaction_map; - - if let Some(subtransactions) = &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() - .map(|payee_id| payee_map.get(payee_id).cloned()) - .next() - .unwrap_or_else(|| { - t.payee_id - .iter() - .map(|payee_id| payee_map.get(payee_id).cloned()) - .next() - .unwrap_or(None) - }); - let account = account_map.get(&t.account_id).cloned(); - - let mut txn = - super::transaction::Transaction::from_sub_transaction( - t, st, - ); - txn.payee = payee; - txn.account = account; - reimbursables.push(txn); - } - } - - reimbursables.sort_by_cached_key(|t| t.date.clone()); - Ok(reimbursables) - } - - fn check(&self) { - self.check_reconciled(); - self.check_has_inflows(); - } - - fn check_reconciled(&self) { - let reconciled_amount: i64 = self - .reimbursables() - .iter() - .filter(|t| t.reimbursed) - .map(|t| t.amount) - .sum(); - if reconciled_amount != 0 { - eprintln!( - "reconciled reimbursables don't sum to $0.00: ${}", - crate::ynab::format_amount(reconciled_amount) - ); - std::process::exit(1); - } - } - - fn check_has_inflows(&self) { - let txns = self - .reimbursables() - .iter() - .filter(|t| !t.reimbursed && t.amount > 0) - .count(); - if txns == 0 { - eprintln!("no transactions to reconcile"); - std::process::exit(1); - } - } -} diff --git a/src/ynab/client.rs b/src/ynab/client.rs deleted file mode 100644 index 19b456b..0000000 --- a/src/ynab/client.rs +++ /dev/null @@ -1,76 +0,0 @@ -#[derive(Debug, snafu::Snafu)] -pub enum Error { - // ynab-api error types don't implement Error, so can't use the - // auto-source behavior - #[snafu(display("failed to update transactions: {}", source_msg))] - UpdateTransactions { source_msg: String }, - - #[snafu(display("failed to get budgets: {}", source_msg))] - GetBudgets { source_msg: String }, - - #[snafu(display("failed to get budget {}: {}", id, source_msg))] - GetBudgetById { id: String, source_msg: String }, -} - -pub type Result<T> = std::result::Result<T, Error>; - -pub struct Client { - api: ynab_api::apis::client::APIClient, -} - -impl Client { - pub fn new(key: &str) -> Self { - let mut ynab_config = - ynab_api::apis::configuration::Configuration::new(); - ynab_config.api_key = Some(ynab_api::apis::configuration::ApiKey { - prefix: Some("Bearer".to_string()), - key: key.to_string(), - }); - Self { - api: ynab_api::apis::client::APIClient::new(ynab_config), - } - } - - pub fn default_budget(&self) -> Result<ynab_api::models::BudgetDetail> { - let budget_id = self - .api - .budgets_api() - .get_budgets() - .map_err(|e| Error::GetBudgets { - source_msg: format!("{:?}", e), - })? - .data - .budgets - .iter() - .next() - .ok_or_else(|| Error::GetBudgets { - source_msg: "no budgets found".to_string(), - })? - .id - .clone(); - Ok(self - .api - .budgets_api() - .get_budget_by_id(&budget_id, 0) - .map_err(|e| Error::GetBudgetById { - id: budget_id.clone(), - source_msg: format!("{:?}", e), - })? - .data - .budget) - } - - pub fn update_transactions( - &self, - budget_id: &str, - transactions: ynab_api::models::UpdateTransactionsWrapper, - ) -> Result<()> { - self.api - .transactions_api() - .update_transactions(budget_id, transactions) - .map(|_| ()) - .map_err(|e| Error::UpdateTransactions { - source_msg: format!("{:?}", e), - }) - } -} diff --git a/src/ynab/transaction.rs b/src/ynab/transaction.rs deleted file mode 100644 index be31019..0000000 --- a/src/ynab/transaction.rs +++ /dev/null @@ -1,101 +0,0 @@ -#[derive(Clone, Debug)] -pub struct Transaction { - pub id: String, - pub date: String, - pub amount: i64, - pub memo: Option<String>, - pub cleared: String, - pub approved: bool, - pub flag_color: Option<String>, - pub account_id: String, - pub payee_id: Option<String>, - pub category_id: Option<String>, - pub import_id: Option<String>, - - pub account: Option<String>, - pub payee: Option<String>, - 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(), - - account: None, - 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(), - - account: None, - 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 - } -} diff --git a/src/ynab/util.rs b/src/ynab/util.rs deleted file mode 100644 index af8a8d1..0000000 --- a/src/ynab/util.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub fn format_amount(amount: i64) -> String { - let dollars = amount.abs() / 1000; - let cents = (amount.abs() % 1000) / 10; - let sign = if amount < 0 { "-" } else { "" }; - format!("${}{}.{:02}", sign, dollars, cents) -} |