aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2019-08-11 19:47:39 -0400
committerJesse Luehrs <doy@tozt.net>2019-08-11 19:47:39 -0400
commit430b17c92ee8e15bacce2c058d3e56f187da5fc3 (patch)
treee4059834d85cc8202fed56840a9d2778c4b53866
parent909badee565c8e5164890b71da6a03b9efe16b28 (diff)
downloadynab-reimbursements-430b17c92ee8e15bacce2c058d3e56f187da5fc3.tar.gz
ynab-reimbursements-430b17c92ee8e15bacce2c058d3e56f187da5fc3.zip
validate reconciled amounts, and also handle split transactions
-rw-r--r--src/main.rs36
-rw-r--r--src/ynab/budget.rs103
-rw-r--r--src/ynab/client.rs2
-rw-r--r--src/ynab/transaction.rs1
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<super::transaction::Transaction> {
- 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,
}