Skip to content

Commit

Permalink
Merge pull request #24 from sebinsua/feat/list-counterparties
Browse files Browse the repository at this point in the history
Added `list counterparties` command
  • Loading branch information
sebinsua committed Dec 25, 2015
2 parents f3c120a + d580793 commit 8b4b58e
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
All notable changes to this project will be [documented](http://keepachangelog.com/) in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [v0.0.4](https://github.com/sebinsua/teller-cli/releases/tag/v0.0.4) - 2015-12-25

- `list counterparties` gives access to a list of outgoing transactions grouped by their counterparty.
- Outgoing values are now positive since the word outgoing already describes a negative flow of money.

## [v0.0.3](https://github.com/sebinsua/teller-cli/releases/tag/v0.0.3) - 2015-12-22

- `list totals` became `list balances|outgoing|incoming`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "teller_cli"
version = "0.0.3"
version = "0.0.4"
authors = ["Seb Insua <[email protected]>"]

[[bin]]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# teller-cli ![Build Status](https://img.shields.io/travis/sebinsua/teller-cli.svg)
> Banking for your command line
The purpose of this command line tool is to provide a human-interface for your bank and not merely to be a one-to-one match with the underlying API.
This tool provides useful ways of interrogating your bank through your command line, and is not merely meant to be a one-to-one match with underlying APIs.

It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK bank, so you will need to have an account there.

Expand All @@ -15,7 +15,7 @@ It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK ba

*e.g.*

![Instructions](http://i.imgur.com/cR3IMAN.png)
![Instructions](http://i.imgur.com/cvZRwev.png)

## Why?

Expand Down Expand Up @@ -87,7 +87,7 @@ fi
### From release

```
> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.3/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller
> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.4/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller
```

### From source
Expand Down
67 changes: 67 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use chrono::{Date, DateTime, UTC, Datelike};
use chrono::duration::Duration;
use itertools::Itertools;

use std::collections::HashMap;

use std::io::prelude::*; // Required for read_to_string use later.
use std::str::FromStr;

Expand Down Expand Up @@ -121,6 +123,21 @@ impl TransactionsWithCurrrency {
}
}

#[derive(Debug)]
pub struct CounterpartiesWithCurrrency {
pub counterparties: Vec<(String, String)>,
pub currency: String,
}

impl CounterpartiesWithCurrrency {
pub fn new<S: Into<String>>(counterparties: Vec<(String, String)>, currency: S) -> CounterpartiesWithCurrrency {
CounterpartiesWithCurrrency {
counterparties: counterparties,
currency: currency.into(),
}
}
}

fn get_auth_header(auth_token: &str) -> Authorization<Bearer> {
Authorization(
Bearer {
Expand Down Expand Up @@ -279,6 +296,56 @@ pub fn get_transactions_with_currency(config: &Config, account_id: &str, timefra
Ok(TransactionsWithCurrrency::new(transactions, currency))
}

fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec<Transaction>) -> HashMap<String, Vec<(String, String)>> {
let grouped_counterparties = transactions.iter().fold(HashMap::new(), |mut acc: HashMap<String, Vec<&'a Transaction>>, t: &'a Transaction| {
let counterparty = t.counterparty.to_owned();
if acc.contains_key(&counterparty) {
if let Some(txs) = acc.get_mut(&counterparty) {
txs.push(t);
}
} else {
let mut txs: Vec<&'a Transaction> = vec![];
txs.push(t);
acc.insert(counterparty, txs);
}

acc
});

grouped_counterparties.into_iter().fold(HashMap::new(), |mut acc, (counterparty, txs)| {
let date_amount_tuples = txs.into_iter().map(|tx| (tx.date.to_owned(), tx.amount.to_owned())).collect();
acc.insert(counterparty.to_string(), date_amount_tuples);
acc
})
}

pub fn get_counterparties(config: &Config, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult<CounterpartiesWithCurrrency> {
let transactions_with_currency = try!(get_transactions_with_currency(&config, &account_id, &timeframe));

let to_cent_integer = |amount: &str| {
(f64::from_str(&amount).unwrap() * 100f64).round() as i64
};
let from_cent_integer_to_float_string = |amount: &i64| {
format!("{:.2}", *amount as f64 / 100f64)
};

let transactions: Vec<Transaction> = transactions_with_currency.transactions.into_iter().filter(|tx| to_cent_integer(&tx.amount) < 0).collect();
let currency = transactions_with_currency.currency;

let counterparty_to_date_amount_list = convert_to_counterparty_to_date_amount_list(&transactions);
let sorted_counterparties = counterparty_to_date_amount_list.into_iter().map(|(counterparty, date_amount_tuples)| {
let amount = date_amount_tuples.iter().fold(0i64, |acc, dat| acc + to_cent_integer(&dat.1));
(counterparty, amount.abs())
}).sort_by(|&(_, amount_a), &(_, amount_b)| {
amount_a.cmp(&amount_b)
});
let counterparties = sorted_counterparties.into_iter().map(|(counterparty, amount)| {
(counterparty, from_cent_integer_to_float_string(&amount))
}).collect();

Ok(CounterpartiesWithCurrrency::new(counterparties, currency))
}

fn get_grouped_transaction_aggregates(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe, aggregate_txs: &Fn((String, Vec<Transaction>)) -> (String, i64)) -> ApiServiceResult<Vec<(String, i64)>> {
let transactions: Vec<Transaction> = get_transactions(&config, &account_id, &timeframe).unwrap_or(vec![]);

Expand Down
51 changes: 48 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod config;
mod client;
mod inquirer;

use client::{Account, Transaction, Money, HistoricalAmountsWithCurrency, Balances, Outgoings, Incomings, get_accounts, get_account_balance, get_transactions_with_currency, get_balances, get_outgoings, get_incomings, get_outgoing, get_incoming};
use client::{Account, Transaction, Money, HistoricalAmountsWithCurrency, Balances, Outgoings, Incomings, get_accounts, get_account_balance, get_transactions_with_currency, get_counterparties, get_balances, get_outgoings, get_incomings, get_outgoing, get_incoming};
use client::{Interval, Timeframe};

use std::path::PathBuf;
Expand All @@ -35,6 +35,7 @@ Usage:
teller init
teller [list] accounts
teller [list] transactions [<account> --timeframe=<tf> --show-description]
teller [list] counterparties [<account> --timeframe=<tf> --count=<n>]
teller [list] (balances|outgoings|incomings) [<account> --interval=<itv> --timeframe=<tf> --output=<of>]
teller [show] balance [<account> --hide-currency]
teller [show] outgoing [<account> --hide-currency]
Expand All @@ -45,6 +46,7 @@ Commands:
init Configure.
list accounts List accounts.
list transactions List transactions.
list counterparties List outgoing amounts grouped by counterparties.
list balances List balances during a timeframe.
list outgoings List outgoings during a timeframe.
list incomings List incomings during a timeframe.
Expand All @@ -57,8 +59,9 @@ Commands:
Options:
-h --help Show this screen.
-V --version Show version.
-i --interval=<itv> Group by an interval of time (default: monthly).
-t --timeframe=<tf> Operate upon a named period of time (default: 6-months).
-i --interval=<itv> Group by an interval of time [default: monthly].
-t --timeframe=<tf> Operate upon a named period of time [default: 6-months].
-c --count=<n> Only the top N elements [default: 10].
-d --show-description Show descriptions against transactions.
-c --hide-currency Show money without currency codes.
-o --output=<of> Output in a particular format (e.g. spark).
Expand All @@ -71,6 +74,7 @@ struct Args {
cmd_show: bool,
cmd_accounts: bool,
cmd_transactions: bool,
cmd_counterparties: bool,
cmd_balances: bool,
cmd_outgoings: bool,
cmd_incomings: bool,
Expand All @@ -80,6 +84,7 @@ struct Args {
arg_account: AccountType,
flag_interval: Interval,
flag_timeframe: Timeframe,
flag_count: i64,
flag_show_description: bool,
flag_hide_currency: bool,
flag_output: OutputFormat,
Expand Down Expand Up @@ -312,6 +317,15 @@ fn pick_command(arguments: Args) {
Some(config) => list_transactions(&config, &arg_account, &flag_timeframe, &flag_show_description),
}
},
Args { cmd_counterparties, ref arg_account, ref flag_timeframe, flag_count, .. } if cmd_counterparties == true => {
match get_config() {
None => {
error!("Configuration could not be found or created so command not executed");
exit(1)
}
Some(config) => list_counterparties(&config, &arg_account, &flag_timeframe, &flag_count),
}
},
Args { cmd_balances, ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } if cmd_balances == true => {
match get_config() {
None => {
Expand Down Expand Up @@ -471,6 +485,37 @@ fn list_transactions(config: &Config, account: &AccountType, timeframe: &Timefra
}
}

fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, count: &i64) {
let mut counterparties_table = String::new();

counterparties_table.push_str(&format!("row\tcounterparty\tamount ({})\n", currency));
let skip_n = counterparties.len() - (*count as usize);
for (idx, counterparty) in counterparties.iter().skip(skip_n).enumerate() {
let row_number = (idx + 1) as u32;
let new_counterparty_row = format!("{}\t{}\t{}\n", row_number, counterparty.0, counterparty.1);
counterparties_table = counterparties_table + &new_counterparty_row;
}

let mut tw = TabWriter::new(Vec::new());
write!(&mut tw, "{}", counterparties_table).unwrap();
tw.flush().unwrap();

let counterparties_str = String::from_utf8(tw.unwrap()).unwrap();

println!("{}", counterparties_str)
}

fn list_counterparties(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) {
let account_id = get_account_id(&config, &account);
match get_counterparties(&config, &account_id, &timeframe) {
Ok(counterparties_with_currency) => represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count),
Err(e) => {
error!("Unable to list counterparties: {}", e);
exit(1)
},
}
}

fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) {
match *output {
OutputFormat::Spark => {
Expand Down

0 comments on commit 8b4b58e

Please sign in to comment.