fi_cli/
lib.rs

1#[macro_use]
2extern crate diesel;
3
4mod account;
5pub mod cli;
6mod currency;
7mod schema;
8mod snapshot;
9
10use account::Account;
11use chrono::NaiveDate;
12use cli::Cli;
13use currency::Currency;
14use diesel::dsl::*;
15use diesel::prelude::*;
16use diesel::PgConnection;
17use reqwest;
18use schema::*;
19use serde_json::value::Value;
20use snapshot::Snapshot;
21use std::env;
22use std::error::Error;
23use termion::color;
24use textplots::{Chart, Plot, Shape};
25
26pub fn run(database_url: &str, command: Cli) -> Result<(), Box<dyn Error>> {
27    let conn = PgConnection::establish(&database_url).expect("Error connecting to the database");
28    match command {
29        Cli::Pull { currency } => {
30            if currency == "all".to_string() {
31                sync_all(&conn).expect("Error occurred while synching");
32            }
33            let given_currency = Currency::from_str(&currency);
34            let notion_api_url = get_notion_api_url(&given_currency);
35            let res = reqwest::blocking::get(&notion_api_url)?.json::<Value>()?;
36            Account::sync(&conn, res);
37        }
38        Cli::History { currency } => {
39            let given_currency = Currency::from_str(&currency);
40            display_history(&conn, &given_currency);
41        }
42        Cli::Sum { currency } => {
43            let given_currency = Currency::from_str(&currency);
44            display_latest_sum(&conn, &given_currency);
45        }
46        Cli::NetWorth { currency } => {
47            let given_currency = Currency::from_str(&currency);
48            display_net_worth(&conn, &given_currency);
49        }
50        Cli::Delete => {
51            delete_data(&conn);
52        }
53    }
54    Ok(())
55}
56
57fn get_notion_api_url(currency: &Currency) -> String {
58    let mut notion_api_url = String::from("NOTION_API_URL_");
59    notion_api_url.push_str(currency.as_str());
60    match env::var(notion_api_url) {
61        Ok(url) => url,
62        Err(err) => panic!("Failed to get notion api url. Error: {}", err),
63    }
64}
65
66fn sync_all(conn: &PgConnection) -> Result<(), Box<dyn Error>> {
67    use std::process::exit;
68    let currencies = [Currency::EUR, Currency::JPY, Currency::USD];
69    for cur in &currencies {
70        let notion_api_url = get_notion_api_url(cur);
71        let res = reqwest::blocking::get(&notion_api_url)?.json::<Value>()?;
72        Account::sync(&conn, res);
73    }
74    println!("Synching all accounts and snapshots completed. \n");
75    exit(0);
76}
77
78pub fn display_latest_sum(conn: &PgConnection, currency: &Currency) {
79    let table: Vec<(Snapshot, Account)> = snapshots::table
80        .distinct_on(snapshots::account_id)
81        .inner_join(accounts::table)
82        .filter(accounts::currency.eq(&currency.as_str()))
83        .order((snapshots::account_id, snapshots::date.desc()))
84        .load(conn)
85        .expect("Error loading latest sum");
86
87    let mut sum = 0;
88    println!("\n{} - latest", currency.as_str());
89    println!("---");
90    for (snapshot, account) in table {
91        println!("{}: {} {}", snapshot.date, account.name, snapshot.amount);
92        sum += snapshot.amount;
93    }
94    println!("---");
95    println!("Total: {}\n", sum);
96}
97
98pub fn display_history(conn: &PgConnection, currency: &Currency) {
99    let table: Vec<(NaiveDate, Option<i64>)> = snapshots::table
100        .inner_join(accounts::table)
101        .select((snapshots::date, sql("sum(amount)")))
102        .filter(accounts::currency.eq(&currency.as_str()))
103        .group_by(snapshots::date)
104        .order(snapshots::date.asc())
105        .load(conn)
106        .expect("Error loading table");
107
108    println!("\n{} - history", &currency.as_str());
109    println!("---");
110
111    let mut prev: Option<i64> = None;
112    for (date, sum) in &table {
113        if let Some(prev_sum) = prev {
114            let is_going_well = sum.unwrap() as f64 >= prev_sum as f64;
115            let diff = sum.unwrap() as f64 - prev_sum as f64;
116            let diff_percent = sum.unwrap() as f64 / prev_sum as f64;
117            if is_going_well {
118                println!(
119                    "{}: {} {} +{} / {:.2}%{}",
120                    date,
121                    sum.unwrap(),
122                    color::Fg(color::Cyan),
123                    diff,
124                    diff_percent * f64::from(100),
125                    color::Fg(color::Reset)
126                );
127            } else {
128                println!(
129                    "{}: {} {} {} / {:.2}%{}",
130                    date,
131                    sum.unwrap(),
132                    color::Fg(color::Red),
133                    diff,
134                    diff_percent * f64::from(100),
135                    color::Fg(color::Reset)
136                );
137            }
138        } else {
139            // First row of record without prev value
140            println!("{}: {}", date, sum.unwrap());
141        }
142        prev = Some(sum.unwrap());
143    }
144
145    let points: Vec<(f32, f32)> = table
146        .to_owned()
147        .into_iter()
148        .enumerate()
149        .map(|(idx, (_, sum))| (idx as f32, sum.unwrap() as f32))
150        .collect();
151
152    Chart::new(100, 40, 0.0, *&table.len() as f32 - 1.0)
153        .lineplot(&Shape::Lines(&points))
154        .nice();
155    println!("\n");
156}
157
158pub fn display_net_worth(conn: &PgConnection, currency: &Currency) {
159    let join = snapshots::table
160        .distinct_on(snapshots::account_id)
161        .inner_join(accounts::table)
162        .order((snapshots::account_id, snapshots::date.desc()));
163
164    let base_currency_table: Vec<(Snapshot, Account)> = join
165        .filter(accounts::currency.eq(currency.as_str()))
166        .load(conn)
167        .expect("Error loading latest sum");
168
169    let mut sum = 0;
170
171    println!("\nNet worth in {}\n===", currency.as_str());
172    println!("{} accounts", currency.as_str());
173    for (snapshot, account) in base_currency_table {
174        println!("{}: {} {}", snapshot.date, account.name, snapshot.amount);
175        sum += snapshot.amount;
176    }
177    let mut exchange_rate_api_url = String::from("https://api.exchangeratesapi.io/latest?base=");
178    exchange_rate_api_url.push_str(&currency.as_str());
179    let req = reqwest::blocking::get(&exchange_rate_api_url);
180
181    match req {
182        Ok(res) => {
183            let res_json = res.json::<Value>().unwrap();
184            for cur in &Currency::the_others(currency) {
185                display_latest_converted(conn, &res_json, cur, &mut sum);
186            }
187        }
188        Err(err) => panic!(err),
189    }
190
191    println!("===");
192    println!("Total: {} {}", sum, &currency.as_str());
193    println!("\n");
194}
195
196fn display_latest_converted(
197    conn: &PgConnection,
198    exchange_rate_json: &Value,
199    currency: &Currency,
200    sum: &mut i32,
201) {
202    if let Value::Number(rate) = &exchange_rate_json["rates"][currency.as_str()] {
203        if let Some(rate) = rate.as_f64() {
204            let table: Vec<(Snapshot, Account)> = snapshots::table
205                .distinct_on(snapshots::account_id)
206                .inner_join(accounts::table)
207                .order((snapshots::account_id, snapshots::date.desc()))
208                .filter(accounts::currency.eq(currency.as_str()))
209                .load(conn)
210                .expect("Error loading latest converted sum");
211
212            if let Value::String(base) = &exchange_rate_json["base"] {
213                println!(
214                    "---\n{} accounts (1.00 {} = {} {})",
215                    currency.as_str(),
216                    base,
217                    rate,
218                    currency.as_str(),
219                );
220            } else {
221                panic!("Error resolving base currency");
222            }
223
224            for (snapshot, account) in table {
225                let converted_amount = snapshot.amount as f64 / rate;
226                println!(
227                    "{}: {} {:.0}",
228                    snapshot.date, account.name, converted_amount
229                );
230                *sum += converted_amount as i32;
231            }
232        }
233    }
234}
235
236pub fn delete_data(conn: &PgConnection) {
237    Snapshot::delete_snapshots(conn);
238    Account::delete_accounts(conn);
239    println!("Deleted all rows in tables");
240}