investments 1.2.0

Helps you with managing your investments
Documentation
use chrono::Duration;

use static_table_derive::StaticTable;

use crate::analyse::deposit_emulator::{DepositEmulator, Transaction};
use crate::config::DepositConfig;
use crate::currency::{Cash, MultiCurrencyCashAccount};
use crate::formatting::{self, table::Style};
use crate::localities;
use crate::types::{Date, Decimal};

pub fn list(mut deposits: Vec<DepositConfig>, today: Date, cron_mode: bool, notify_days: Option<u32>) {
    let mut deposits: Vec<DepositConfig> = deposits.drain(..).filter(|deposit| {
        deposit.open_date <= today
    }).collect();

    if deposits.is_empty() {
        return
    }
    deposits.sort_by_key(|deposit| deposit.close_date);

    if cron_mode {
        print_cron_mode(deposits, today, notify_days)
    } else {
        print(deposits, today);
    }
}

#[derive(StaticTable)]
struct Row {
    #[column(name="Open date")]
    open_date: Date,
    #[column(name="Close date")]
    close_date: Date,
    #[column(name="Name")]
    name: String,
    #[column(name="Amount")]
    amount: Cash,
    #[column(name="Interest")]
    interest: Decimal,
    #[column(name="Current amount")]
    current_amount: Cash,
}

fn print(deposits: Vec<DepositConfig>, today: Date) {
    let mut table = Table::new();
    let mut total_amount = MultiCurrencyCashAccount::new();
    let mut total_current_amount = MultiCurrencyCashAccount::new();

    for deposit in deposits {
        let (amount, current_amount) = calculate_amounts(&deposit, today);
        total_amount.deposit(amount);
        total_current_amount.deposit(current_amount);

        let mut row = table.add_row(Row {
            open_date: deposit.open_date,
            close_date: deposit.close_date,
            name: deposit.name,
            amount: amount,
            interest: deposit.interest.normalize(),
            current_amount: current_amount,
        });

        if deposit.close_date <= today {
            let style = Style::new().dimmed();
            for cell in &mut row {
                cell.style(style);
            }
        }
    }

    let mut totals = table.add_empty_row();
    totals.set_amount(total_amount);
    totals.set_current_amount(total_current_amount);

    table.print("Open deposits");
}

fn print_cron_mode(deposits: Vec<DepositConfig>, today: Date, notify_days: Option<u32>) {
    let mut expiring_deposits = Vec::new();
    let mut closed_deposits = Vec::new();

    for deposit in deposits {
        if deposit.close_date <= today {
            closed_deposits.push(deposit);
        } else if let Some(notify_days) = notify_days {
            if today + Duration::days(i64::from(notify_days)) == deposit.close_date {
                expiring_deposits.push(deposit);
            }
        }
    }

    if !expiring_deposits.is_empty() {
        println!("The following deposits are about to close:");
        for deposit in &expiring_deposits {
            print_closed_deposit(deposit);
        }
    }

    if !closed_deposits.is_empty() {
        if !expiring_deposits.is_empty() {
            println!();
        }

        println!("The following deposits are closed:");
        for deposit in &closed_deposits {
            print_closed_deposit(deposit);
        }
    }
}

fn print_closed_deposit(deposit: &DepositConfig) {
    let (amount, close_amount) = calculate_amounts(deposit, deposit.close_date);
    println!(
        "{date} {name}: {amount} -> {close_amount}",
        date=formatting::format_date(deposit.close_date), name=deposit.name, amount=amount,
        close_amount=close_amount);
}

fn calculate_amounts(deposit: &DepositConfig, today: Date) -> (Cash, Cash) {
    let currency = deposit.currency.as_ref().map_or_else(
        || localities::russia().currency, String::as_str);

    let mut contributions = vec![(deposit.open_date, deposit.amount)];
    contributions.extend(&deposit.contributions);

    let transactions: Vec<_> = contributions.iter().filter_map(|&(date, amount)| {
        if date <= today {
            Some(Transaction::new(date, amount))
        } else {
            None
        }
    }).collect();

    let amount = transactions.iter().map(|transaction| transaction.amount).sum();
    let amount = Cash::new(currency, amount);

    let end_date = if today <= deposit.close_date {
        today
    } else {
        deposit.close_date
    };

    let current_amount = DepositEmulator::new(deposit.open_date, end_date, deposit.interest)
        .with_monthly_capitalization(deposit.capitalization)
        .emulate(&transactions);
    let current_amount = Cash::new(currency, current_amount).round();

    (amount, current_amount)
}