investments 4.16.1

Helps you with managing your investments
Documentation
#[macro_use] mod record;

mod countries;
mod encoding;
mod foreign_income;
mod parser;
mod types;

use std::fs;
use std::path::{Path, PathBuf};

use crate::core::{EmptyResult, GenericResult};
use crate::types::{Date, Decimal};
use crate::util;

use self::foreign_income::{ForeignIncome, CurrencyIncome, CurrencyInfo, DeductionInfo, IncomeType,
                           ControlledForeignCompanyInfo};
use self::record::Record;
use self::parser::{TaxStatementReader, TaxStatementWriter};

pub use self::countries::CountryCode;

#[derive(Debug)]
pub struct TaxStatement {
    path: PathBuf,
    pub year: i32,
    records: Vec<Box<dyn Record>>,
}

impl TaxStatement {
    pub fn read(path: &Path) -> GenericResult<TaxStatement> {
        Ok(TaxStatementReader::read(path).map_err(|e| format!(
            "Error while reading {:?} tax statement: {}", path, e))?)
    }

    pub fn save(&self) -> EmptyResult {
        let temp_path = util::temp_path(&self.path);

        TaxStatementWriter::write(self, &temp_path).map_err(|e| {
            let _ = fs::remove_file(&temp_path);
            format!("Failed to save the tax statement to {:?}: {}", temp_path, e)
        })?;

        fs::rename(&temp_path, &self.path).map_err(|e| {
            let _ = fs::remove_file(&temp_path);
            format!("Failed to rename {:?} to {:?}: {}", temp_path, self.path, e)
        })?;

        Ok(())
    }

    pub fn add_dividend_income(
        &mut self, description: &str, date: Date,
        source_from: CountryCode, received_in: CountryCode, currency: &str, currency_rate: Decimal,
        amount: Decimal, paid_tax: Decimal, local_amount: Decimal, local_paid_tax: Decimal,
    ) -> EmptyResult {
        self.get_foreign_incomes()?.push(CurrencyIncome {
            type_: IncomeType::Dividend,
            description: description.to_owned(),
            source_from, received_in,

            date: date,
            tax_payment_date: date,
            currency: CurrencyInfo::new(currency, currency_rate)?,

            amount: amount,
            local_amount: local_amount,

            paid_tax: paid_tax,
            local_paid_tax: local_paid_tax,
            deduction: DeductionInfo::new_none(),

            controlled_foreign_company: ControlledForeignCompanyInfo::new_none(),
        });

        Ok(())
    }

    pub fn add_interest_income(
        &mut self, description: &str, date: Date, broker_jurisdiction: CountryCode,
        currency: &str, currency_rate: Decimal, amount: Decimal, local_amount: Decimal,
    ) -> EmptyResult {
        self.get_foreign_incomes()?.push(CurrencyIncome {
            type_: IncomeType::Interest,
            description: description.to_owned(),

            source_from: broker_jurisdiction,
            received_in: broker_jurisdiction,

            date: date,
            tax_payment_date: date,
            currency: CurrencyInfo::new(currency, currency_rate)?,

            amount: amount,
            local_amount: local_amount,

            paid_tax: dec!(0),
            local_paid_tax: dec!(0),
            deduction: DeductionInfo::new_none(),

            controlled_foreign_company: ControlledForeignCompanyInfo::new_none(),
        });

        Ok(())
    }

    pub fn add_stock_income(
        &mut self, description: &str, date: Date, broker_jurisdiction: CountryCode,
        currency: &str, currency_rate: Decimal, amount: Decimal, local_amount: Decimal,
        purchase_local_cost: Decimal,
    ) -> EmptyResult {
        self.get_foreign_incomes()?.push(CurrencyIncome {
            type_: IncomeType::Stock,
            description: description.to_owned(),

            source_from: broker_jurisdiction,
            received_in: broker_jurisdiction,

            date: date,
            tax_payment_date: date,
            currency: CurrencyInfo::new(currency, currency_rate)?,

            amount: amount,
            local_amount: local_amount,

            paid_tax: dec!(0),
            local_paid_tax: dec!(0),

            // Please note that we should always specify this deduction amount - even if it's zero.
            // If it's not specified the income doesn't participate into settlement of losses.
            deduction: DeductionInfo {
                code: 201,
                amount: purchase_local_cost,
            },

            controlled_foreign_company: ControlledForeignCompanyInfo::new_none(),
        });

        Ok(())
    }

    fn get_foreign_incomes(&mut self) -> GenericResult<&mut Vec<CurrencyIncome>> {
        Ok(self.get_mut_record(ForeignIncome::RECORD_NAME)?
            .map(|record: &mut ForeignIncome| &mut record.incomes)
            .ok_or("Foreign income must be enabled in the tax statement")?)
    }

    fn get_mut_record<T: 'static + Record>(&mut self, name: &str) -> GenericResult<Option<&mut T>> {
        let mut found_record = None;

        for record in &mut self.records {
            if record.name() != name {
                continue;
            }

            if found_record.is_some() {
                return Err!("The statement has several {} records", name);
            }

            found_record = Some(record);
        }

        Ok(match found_record {
            Some(record) => Some(
                record.as_mut_any().downcast_mut::<T>().ok_or_else(|| format!(
                    "Failed to cast {} record to the underlaying type", name))?),
            None => None,
        })
    }
}