From 039689315511e9a8a76ddf6bd358bba0c03b68ce Mon Sep 17 00:00:00 2001 From: Richard Lyon Date: Tue, 6 Jun 2023 13:57:07 +0100 Subject: [PATCH] reinstate `transactions` --- ...0220101_000001_create_transaction_table.rs | 2 + src/bin/cli.rs | 12 +- src/commands/admin.rs | 6 +- src/commands/transactions.rs | 8 +- src/db/transaction.rs | 267 ++++++++---------- src/entities/account.rs | 1 + src/entities/transaction.rs | 1 + 7 files changed, 147 insertions(+), 150 deletions(-) diff --git a/migration/src/m20220101_000001_create_transaction_table.rs b/migration/src/m20220101_000001_create_transaction_table.rs index be59bfc..5393c94 100644 --- a/migration/src/m20220101_000001_create_transaction_table.rs +++ b/migration/src/m20220101_000001_create_transaction_table.rs @@ -22,6 +22,7 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(Transaction::Uid).string().not_null()) + .col(ColumnDef::new(Transaction::AccountUid).string().not_null()) .col( ColumnDef::new(Transaction::TransactionTime) .timestamp() @@ -60,6 +61,7 @@ pub enum Transaction { Table, Id, Uid, + AccountUid, TransactionTime, CounterpartyID, Amount, diff --git a/src/bin/cli.rs b/src/bin/cli.rs index cffa804..ca2c762 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -38,7 +38,7 @@ fn cli() -> Command { .subcommand( Command::new("transactions") .about("get transactions") - .arg(arg!(-d [DAYS] "The days to get").default_value("31")), + .arg(arg!(days: [DAYS] "The days to get").default_value("31")), ) } @@ -79,6 +79,7 @@ async fn main() -> Result<()> { ("list", _) => { commands::account::list().await?; } + ("balance", _) => { commands::account::balance().await?; } @@ -91,7 +92,14 @@ async fn main() -> Result<()> { Some(("transactions", sub_matches)) => { println!("Processing transactions"); - let days = *sub_matches.get_one::("DAYS").expect("required"); + let days = sub_matches + .get_one::("days") + .map(|s| s.as_str()) + .unwrap(); + let days: i64 = days.parse().unwrap(); + + println!("Getting {} days", days); + if let Err(e) = commands::transactions::update(days).await { println!("Application error: {}", e); process::exit(1); diff --git a/src/commands/admin.rs b/src/commands/admin.rs index 2783db6..c2e1377 100644 --- a/src/commands/admin.rs +++ b/src/commands/admin.rs @@ -1,5 +1,7 @@ -//! Command Line Interface `Admin` commands -//! +/*! +Command Line Interface `Admin` commands + +*/ use crate::config::Config; use crate::db::{self}; diff --git a/src/commands/transactions.rs b/src/commands/transactions.rs index ac0eb03..a0e9205 100644 --- a/src/commands/transactions.rs +++ b/src/commands/transactions.rs @@ -1,12 +1,14 @@ -//! Command Line Interface `Transaction` commands -//! +/*! +Command Line Interface `Transaction` commands + +*/ use crate::db; use anyhow::Result; /// Fetch transactions for the specified number of days and save to the database pub async fn update(days: i64) -> Result<()> { - db::transaction::insert_or_update(days).await; + db::transaction::insert_or_update(days).await?; Ok(()) } diff --git a/src/db/transaction.rs b/src/db/transaction.rs index f6c41bd..1f6ccc4 100644 --- a/src/db/transaction.rs +++ b/src/db/transaction.rs @@ -2,119 +2,98 @@ //! use super::get_database; +use crate::db; +use crate::entities::counterparty; +use crate::starling::client::{StarlingApiClient, StarlingClient}; use crate::{ - config::Config, entities::{prelude::*, transaction}, - starling::{ - transaction::StarlingTransaction, - }, + starling::transaction::StarlingTransaction, }; +use anyhow::Result; +use chrono::Duration; use sea_orm::*; /// Insert or update a list of Starling transactions for the specified account and number of days. /// /// If the transaction doesn't exist, insert it. If it exists and its status has changed, update it. -pub async fn insert_or_update(_days: i64) { - let _db = get_database().await.unwrap(); - let config = Config::new(); - config.load(); - - // for item in config.token.unwrap().iter() { - // for token in item.values() { - // let client = StarlingApiClient::new(token); - // for account in client.accounts().await.iter() { - // // fetch latest transactions - // let transactions = client - // .transactions_since( - // &account.uid, - // &account.default_category, - // chrono::Duration::days(days), - // ) - // .await; - - // // insert or update tables - // for transaction in transactions { - // println!("{:#?}", transaction); - // match transaction_exists(&db, &transaction.uid).await { - // Some(record) => { - // if transaction_changed(&record, &transaction) { - // // update the transaction - // } - // } - // None => { - // // insert the counterparty - // // insert the transaction - // } - // } - // } - // } - // } - // } - - // for transaction in transactions { - // println!("{:#?}", transaction); - // } - - // for item in client - // .transactions_since( - // &account.account_uid, - // &account.default_category, - // chrono::Duration::days(days), - // ) - // .await - // { - // match feeditem_exists(&db, &item.uid).await { - // // if the feed item doesn't already exist - // None => { - // // insert or get the counterparty id - // let item_counterparty_uid = item.counterparty_uid.clone().unwrap_or_default(); - // let counterparty_id = match counterparty_exists(&db, &item_counterparty_uid).await { - // Some(counterparty) => counterparty.id, - // None => { - // let counterparty = counterparty_from_starling_feed_item(&item); - // let record = Counterparty::insert(counterparty) - // .exec(&db) - // .await - // .expect("inserting counterparty"); - // record.last_insert_id - // } - // }; - - // // insert the new feed item - // let record = record_from_starling_feed_item(&item, counterparty_id); - // Transaction::insert(record) - // .exec(&db) - // .await - // .expect("inserting feed item"); - // } - // // if the feed item does exist - // Some(record) => { - // // if the feed item status has changed - // if feeditem_has_changed(&record, &item) { - // // update the feed item status - // // TODO : refactor this to update status field only - // let new_status = item.status.to_string(); - // let new_spending_category = item.spending_category; - // let new_user_note = item.user_note.clone().unwrap_or_default(); - - // let record = transaction::ActiveModel { - // status: ActiveValue::set(new_status.to_owned()), - // spending_category: ActiveValue::set(new_spending_category.to_owned()), - // user_note: ActiveValue::set(new_user_note.to_owned()), - // id: ActiveValue::Set(record.id.to_owned()), - // feed_uid: ActiveValue::Set(record.feed_uid.to_owned()), - // transaction_time: ActiveValue::set(record.transaction_time.to_owned()), - // counterparty_id: ActiveValue::set(record.counterparty_id.to_owned()), - // amount: ActiveValue::set(record.amount).to_owned(), - // currency: ActiveValue::set(record.currency.to_owned()), - // reference: ActiveValue::set(record.reference.to_owned()), - // }; - // record.update(&db).await.expect("updating feed item"); - // } - // } - // } - // } +pub async fn insert_or_update(days: i64) -> Result<()> { + let db = get_database().await.unwrap(); + for account in db::account::list().await? { + // fetch the latest transactions + + let client = StarlingApiClient::new(&account.token); + let transactions = client + .transactions_since( + &account.uid, + &account.default_category, + Duration::days(days), + ) + .await; + + for transaction in transactions { + match transaction_exists(&db, &transaction.uid).await { + None => { + // insert or get the counterparty id + + let item_counterparty_uid = + transaction.counterparty_uid.clone().unwrap_or_default(); + + let counterparty_id = match counterparty_exists(&db, &item_counterparty_uid) + .await + { + Some(counterparty) => counterparty.id, + + None => { + let counterparty = counterparty_from_starling_feed_item(&transaction); + let record = Counterparty::insert(counterparty) + .exec(&db) + .await + .expect("inserting counterparty"); + record.last_insert_id + } + }; + + // insert the new transaction + + let record = + record_from_starling_feed_item(&transaction, counterparty_id, &account.uid); + Transaction::insert(record) + .exec(&db) + .await + .expect("inserting feed item"); + } + + Some(record) => { + if transaction_changed(&record, &transaction) { + // update the feed item status + // TODO : refactor this to update status field only + + let new_status = transaction.status.to_string(); + let new_spending_category = transaction.spending_category; + let new_user_note = transaction.user_note.clone().unwrap_or_default(); + + let record = transaction::ActiveModel { + account_uid: ActiveValue::Set(record.account_uid.to_owned()), + status: ActiveValue::set(new_status.to_owned()), + spending_category: ActiveValue::set(new_spending_category.to_owned()), + user_note: ActiveValue::set(new_user_note.to_owned()), + id: ActiveValue::Set(record.id.to_owned()), + uid: ActiveValue::Set(record.uid.to_owned()), + transaction_time: ActiveValue::set(record.transaction_time.to_owned()), + counterparty_id: ActiveValue::set(record.counterparty_id.to_owned()), + amount: ActiveValue::set(record.amount).to_owned(), + currency: ActiveValue::set(record.currency.to_owned()), + reference: ActiveValue::set(record.reference.to_owned()), + }; + record.update(&db).await.expect("updating feed item"); + } + } + } + } + } + + Ok(()) } /// Return true if a feed item with the given feed uid exists in the database. @@ -132,46 +111,48 @@ async fn transaction_exists( // Return true if status or spending category has changed fn transaction_changed(record: &transaction::Model, newitem: &StarlingTransaction) -> bool { (record.status != newitem.status.to_string()) - || (record.spending_category != newitem.spending_category.to_string()) + || (record.spending_category != newitem.spending_category) || (record.user_note != newitem.user_note.clone().unwrap_or_default().to_string()) } // Return true if a counterparty with the given counterparty uid exists in the database. -// async fn counterparty_exists( -// db: &DatabaseConnection, -// counterparty_uid: &String, -// ) -> Option { -// Counterparty::find() -// .filter(counterparty::Column::Uid.eq(counterparty_uid)) -// .one(db) -// .await -// .expect("getting counterparty id") -// } - -// fn record_from_starling_feed_item( -// item: &StarlingTransaction, -// counterparty_id: i32, -// ) -> transaction::ActiveModel { -// transaction::ActiveModel { -// uid: ActiveValue::Set(item.uid.to_owned()), -// transaction_time: ActiveValue::Set(item.transaction_time.to_owned()), -// counterparty_id: ActiveValue::Set(counterparty_id), -// amount: ActiveValue::set(item.amount()).to_owned(), -// spending_category: ActiveValue::set(item.spending_category.to_owned()), -// currency: ActiveValue::set(item.currency().to_owned()), -// reference: ActiveValue::set(item.reference.clone().unwrap_or_default().to_owned()), -// user_note: ActiveValue::set(item.user_note.clone().unwrap_or_default().to_owned()), -// status: ActiveValue::set(item.status.to_string()), -// ..Default::default() -// } -// } - -// fn counterparty_from_starling_feed_item(item: &StarlingTransaction) -> counterparty::ActiveModel { -// let item_counterparty_uid = item.counterparty_uid.clone().unwrap_or_default(); -// counterparty::ActiveModel { -// uid: ActiveValue::Set(item_counterparty_uid.to_owned()), -// name: ActiveValue::Set(item.counterparty_name.to_owned()), -// r#type: ActiveValue::Set(item.counterparty_type.to_owned()), -// ..Default::default() -// } -// } +async fn counterparty_exists( + db: &DatabaseConnection, + counterparty_uid: &String, +) -> Option { + Counterparty::find() + .filter(counterparty::Column::Uid.eq(counterparty_uid)) + .one(db) + .await + .expect("getting counterparty id") +} + +fn record_from_starling_feed_item( + item: &StarlingTransaction, + counterparty_id: i32, + account_uid: &str, +) -> transaction::ActiveModel { + transaction::ActiveModel { + uid: ActiveValue::Set(item.uid.to_owned()), + account_uid: ActiveValue::Set(account_uid.to_string()), + transaction_time: ActiveValue::Set(item.transaction_time.to_owned()), + counterparty_id: ActiveValue::Set(counterparty_id), + amount: ActiveValue::set(item.amount()), + spending_category: ActiveValue::set(item.spending_category.to_owned()), + currency: ActiveValue::set(item.currency()), + reference: ActiveValue::set(item.reference.clone().unwrap_or_default()), + user_note: ActiveValue::set(item.user_note.clone().unwrap_or_default()), + status: ActiveValue::set(item.status.to_string()), + ..Default::default() + } +} + +fn counterparty_from_starling_feed_item(item: &StarlingTransaction) -> counterparty::ActiveModel { + let item_counterparty_uid = item.counterparty_uid.clone().unwrap_or_default(); + counterparty::ActiveModel { + uid: ActiveValue::Set(item_counterparty_uid), + name: ActiveValue::Set(item.counterparty_name.to_owned()), + r#type: ActiveValue::Set(item.counterparty_type.to_owned()), + ..Default::default() + } +} diff --git a/src/entities/account.rs b/src/entities/account.rs index 983388a..ec6e3c9 100644 --- a/src/entities/account.rs +++ b/src/entities/account.rs @@ -8,6 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub name: String, + #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(16)))")] pub uid: String, pub created_at: DateTimeUtc, pub default_category: String, diff --git a/src/entities/transaction.rs b/src/entities/transaction.rs index 6196fb3..43cda49 100644 --- a/src/entities/transaction.rs +++ b/src/entities/transaction.rs @@ -8,6 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub uid: String, + pub account_uid: String, pub transaction_time: DateTimeUtc, pub counterparty_id: i32, #[sea_orm(column_type = "Float")]