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(¤cy);
34 let notion_api_url = get_notion_api_url(&given_currency);
35 let res = reqwest::blocking::get(¬ion_api_url)?.json::<Value>()?;
36 Account::sync(&conn, res);
37 }
38 Cli::History { currency } => {
39 let given_currency = Currency::from_str(¤cy);
40 display_history(&conn, &given_currency);
41 }
42 Cli::Sum { currency } => {
43 let given_currency = Currency::from_str(¤cy);
44 display_latest_sum(&conn, &given_currency);
45 }
46 Cli::NetWorth { currency } => {
47 let given_currency = Currency::from_str(¤cy);
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 ¤cies {
70 let notion_api_url = get_notion_api_url(cur);
71 let res = reqwest::blocking::get(¬ion_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(¤cy.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(¤cy.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", ¤cy.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 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(¤cy.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, ¤cy.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}