bank 0.4.0

Various bank related traits and implementation. An abstract bank-transaction parser; easily add your own bank. SEPA RF implementation for easy formatting and checking of RF fields.
// bank-rs ; An abstract bank-transaction parser.
// Copyright (C) 2017  Ruben De Smet
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

use std::io;
use std::io::{BufReader, Read, BufRead, Error, ErrorKind};
use std::str::FromStr;

use chrono::prelude::*;
use bigdecimal::BigDecimal;
use encoding::all::ISO_8859_1;
use encoding::{Encoding, DecoderTrap};

use GenericTransaction;

use csv;

pub struct Argenta;

enum ArgentaAccountType {
    GiroPlus,
}

impl ArgentaAccountType {
    fn from_string(s: &str) -> io::Result<ArgentaAccountType> {
        Ok(match s.trim() {
            "Giro +" => ArgentaAccountType::GiroPlus,
            _ => return Err(Error::new(ErrorKind::InvalidData, "Unrecognised Argenta account type"))
        })
    }
}

struct ArgentaTransactionIterator<'a, R: Read + 'a> {
    origin: ::Iban,
    iter: csv::ByteRecords<'a, R>,
}

fn parse_belgian_date(s: &str) -> Date<Utc> {
    let v = s.split("-").map(|x| x.parse().unwrap()).collect::<Vec<i32>>();
    Utc.ymd(v[2], v[1] as u32, v[0] as u32)
}

impl<'a, R: Read + 'a> ArgentaTransactionIterator<'a, R> {
    fn transaction_from_line(&self, s: Vec<Vec<u8>>) -> io::Result<GenericTransaction> {
        // Valutadatum;Ref. v/d verrichting;Beschrijving;Bedrag v/d verrichting;Munt;Datum v. verrichting;Rekening tegenpartij;Naam v/d tegenpartij :;Mededeling 1 :;Mededeling 2 :

        let s = s.into_iter()
            .map(|x| {
                ISO_8859_1.decode(&x, DecoderTrap::Strict)
                    .unwrap()
            }).collect::<Vec<_>>();
        debug!("Parsing {:?}", s);
        // TODO: Some nice map implementation for the parse errors
        Ok(GenericTransaction {
            settlement_date: parse_belgian_date(&s[0]),
            id: s[1].clone(),
            description: s[2].clone(),
            amount: BigDecimal::from_str(&s[3].replace('.',"").replace(',',".")).unwrap(),
            currency: s[4].clone(),
            date: parse_belgian_date(&s[5]),
            origin_account: self.origin.clone(),
            counterparty_account: s[6].parse()?,
            counterparty_name: s[7].clone(),
            message_1: s[8].clone(),
            message_2: s[9].clone(),
        })
    }
}

impl<'a, R: Read> Iterator for ArgentaTransactionIterator<'a, R> {
    type Item = Box<::Transaction>;
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().and_then(|record| {
            let record = record.unwrap();
            self.transaction_from_line(record).map(|a| Box::new(a) as Box<::Transaction>).ok()
        })
    }
}

pub struct ArgentaTransactions<R> {
    iban: ::Iban,
    account_type: ArgentaAccountType,

    reader: csv::Reader<R>,
}

impl<R: Read, Rt: Read> ::Transactions<R> for ArgentaTransactions<Rt> {
    fn get_origin_account(&self) -> Option<::Iban> {
        Some(self.iban.clone())
    }
    fn read_transactions<'a>(&'a mut self) -> Box<Iterator<Item=Box<::Transaction>> + 'a> {
        Box::new(ArgentaTransactionIterator {
            origin: self.iban.clone(),
            iter: self.reader.byte_records(),
        })
    }
}

impl Argenta {
    fn recognise_transaction_file_impl<R: Read + 'static>(reader: &mut BufReader<R>) -> io::Result<(::Iban, ArgentaAccountType)> {
        let mut line = String::new();
        reader.read_line(&mut line).and_then(|amount| {
            //Nr v/d rekening :;BE11 9731 2593 6548;Giro +
            if amount == 0 || !line.starts_with("Nr v/d rekening :;") {
                Err(Error::new(ErrorKind::InvalidInput, "Not an Argenta CSV"))
            } else {
                Ok(line.split(';').skip(1))
            }
        }).and_then(|mut first_line| {
            match first_line.next() {
                Some(e) => e.parse::<::Iban>().and_then(|iban| {Ok((first_line, iban))}),
                None => Err(Error::new(ErrorKind::InvalidInput, "File does not contain Argenta IBAN"))
            }
        }).and_then(|(mut first_line, iban)| {
            match first_line.next() {
                Some(e) => ArgentaAccountType::from_string(e).and_then(|acc_t| {
                    Ok((iban, acc_t))
                }),
                None => Err(Error::new(ErrorKind::InvalidInput, "File does not contain a valid Argenta account type"))
            }
        }).and_then(|(iban, account_type)| {
            match reader.read_line(&mut line) {
                Err(e) => Err(e),
                Ok(0) => Err(Error::new(ErrorKind::UnexpectedEof, "File is not valid Argenta data")),
                Ok(_) => Ok((iban, account_type))
            }
        })
    }
}

impl ::Bank for Argenta {
    fn get_name() -> &'static str {
        "Argenta Spaarbank"
    }
    fn recognise_transaction_file<R: Read + 'static>(reader: &mut BufReader<R>) -> bool {
        Self::recognise_transaction_file_impl(reader).is_ok()
    }
    fn read_transaction_file<R: Read + 'static>(mut reader: BufReader<R>) -> io::Result<Box<::Transactions<R>>> {
        Self::recognise_transaction_file_impl(&mut reader).and_then(
            |(iban, account_type)| {
            let reader = csv::Reader::from_reader(reader)
                             .has_headers(false)
                             .delimiter(b';');
            Ok(Box::new(ArgentaTransactions {
                iban: iban,
                account_type: account_type,
                reader: reader,
            }) as Box<::Transactions<R>>)
        })
    }
}

#[test]
fn recognise_argenta() {
    use std::fs::File;
    use Bank;
    use std::io::{Seek, SeekFrom};

    let f = File::open("tests/test_argenta.csv").unwrap();
    let mut reader = BufReader::new(f);

    assert!(Argenta::recognise_transaction_file(&mut reader));
    reader.seek(SeekFrom::Start(0)).unwrap();

    let mut transactions = Argenta::read_transaction_file(reader).unwrap();
    let transactions = transactions.read_transactions().collect::<Vec<_>>();
    assert_eq!(transactions.len(), 2);

    assert_eq!(transactions[0].settlement_date(), Utc.ymd(2017, 03, 22));
    assert_eq!(*transactions[0].amount(), BigDecimal::from_str("-80.280000").unwrap());
}

#[test]
fn read_argenta() {
    use std::fs::File;
    use Bank;
    use std::io::Seek;

    let f = File::open("tests/test_argenta.csv").unwrap();
    let mut reader = BufReader::new(f);

    let mut transactions = Argenta::read_transaction_file(reader).unwrap();
    let transactions = transactions.read_transactions().collect::<Vec<_>>();
    assert_eq!(transactions.len(), 2);

    assert_eq!(transactions[0].settlement_date(), Utc.ymd(2017, 03, 22));
    assert_eq!(*transactions[0].amount(), BigDecimal::from_str("-80.280000").unwrap());
}

#[test]
fn test_generic_reader() {
    use std::fs::File;
    use Bank;
    use std::io::Seek;

    let f = File::open("tests/test_argenta.csv").unwrap();
    let mut reader = BufReader::new(f);

    let mut transactions = ::read_transaction_file(reader).unwrap();
    let transactions = transactions.read_transactions().collect::<Vec<_>>();
    assert_eq!(transactions.len(), 2);

    assert_eq!(transactions[0].settlement_date(), Utc.ymd(2017, 03, 22));
    assert_eq!(*transactions[0].amount(), BigDecimal::from_str("-80.280000").unwrap());
}