degiro_tax_report/
lib.rs

1pub mod money;
2pub mod portfolio;
3
4use anyhow::anyhow;
5use chrono::{NaiveDate, NaiveTime};
6use csv::DeserializeRecordsIter;
7use dateparser::parse;
8use futures::Stream;
9use money::Money;
10use rev_lines::RevLines;
11use serde::{de, Deserialize, Serialize};
12use std::fmt::Write;
13use std::fs::File;
14use std::io::BufReader;
15use std::iter::Peekable;
16use std::pin::Pin;
17use std::task::{Context, Poll};
18
19pub enum TransactionType {
20    Buy,
21    Sell,
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25#[serde(rename_all = "PascalCase")]
26pub struct Transaction {
27    #[serde(deserialize_with = "deserialize_naive_date")]
28    date: NaiveDate,
29    #[serde(deserialize_with = "deserialize_naive_time")]
30    time: NaiveTime,
31    product: String,
32    #[serde(rename(deserialize = "ISIN"))]
33    isin: String,
34    reference: String,
35    quantity: isize,
36    venue: String,
37    price: Money,
38    #[serde(rename(deserialize = "Local value"))]
39    local_value: Money,
40    value: Money,
41    #[serde(rename(deserialize = "Transaction and/or third"))]
42    transaction: Option<String>,
43    #[serde(rename(deserialize = "Exchange rate"))]
44    exchange_rate: Option<String>,
45    total: String,
46    #[serde(rename(deserialize = "Order ID"))]
47    order_id: String,
48}
49
50#[derive(Debug)]
51pub enum TransactionError {
52    SellWithNegPrice { order_id: String },
53    BuyingWithNegPrice { order_id: String },
54}
55
56impl Transaction {
57    pub fn new(
58        date: NaiveDate,
59        isin: String,
60        quantity: isize,
61        value: Money,
62        order_id: String,
63    ) -> Result<Self, TransactionError> {
64        if quantity.is_negative() && value.amount.is_negative() {
65            return Err(TransactionError::SellWithNegPrice { order_id });
66        }
67
68        if quantity.is_positive() && value.amount.is_positive() {
69            return Err(TransactionError::BuyingWithNegPrice { order_id });
70        }
71
72        Ok(Self {
73            date,
74            time: NaiveTime::from_hms(1, 1, 1),
75            product: "".to_string(),
76            isin,
77            reference: "".to_string(),
78            quantity,
79            venue: "".to_string(),
80            price: Default::default(),
81            value,
82            local_value: Default::default(),
83            transaction: None,
84            exchange_rate: None,
85            total: "".to_string(),
86            order_id,
87        })
88    }
89
90    pub fn new_unchecked(
91        date: NaiveDate,
92        isin: String,
93        quantity: isize,
94        value: Money,
95        order_id: String,
96    ) -> Self {
97        match Self::new(date, isin, quantity, value, order_id) {
98            Ok(ok) => ok,
99            Err(e) => panic!("{:#?}", e),
100        }
101    }
102
103    pub fn date(&self) -> &NaiveDate {
104        &self.date
105    }
106
107    pub fn r#type(&self) -> TransactionType {
108        match self.value.amount.is_negative() {
109            true => TransactionType::Buy,
110            false => TransactionType::Sell,
111        }
112    }
113}
114
115fn deserialize_naive_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
116where
117    D: de::Deserializer<'de>,
118{
119    let s: &str = de::Deserialize::deserialize(deserializer)?;
120    let dt = match local_date_parse(s) {
121        Ok(dt) => dt,
122        Err(parse_err) => {
123            let dt = parse(s)
124                .map_err(|e| de::Error::custom(format!("error: {} error: {}", parse_err, e)))?;
125            dt.naive_local().date()
126        }
127    };
128    Ok(dt)
129}
130
131fn deserialize_naive_time<'de, D>(deserializer: D) -> Result<NaiveTime, D::Error>
132where
133    D: de::Deserializer<'de>,
134{
135    let s: &str = de::Deserialize::deserialize(deserializer)?;
136    let nt = match local_time_parse(s) {
137        Ok(dt) => dt,
138        Err(parse_err) => {
139            let dt = parse(s).map_err(|e| de::Error::custom(format!("{}\n{}", parse_err, e)))?;
140            dt.naive_local().time()
141        }
142    };
143    Ok(nt)
144}
145
146fn local_date_parse(s: &str) -> Result<NaiveDate, chrono::ParseError> {
147    NaiveDate::parse_from_str(s, "%d-%m-%Y")
148}
149
150fn local_time_parse(s: &str) -> Result<NaiveTime, chrono::ParseError> {
151    NaiveTime::parse_from_str(s, "%H:%M")
152}
153
154pub struct CsvStream {
155    parser: ReverseCsv,
156}
157
158impl CsvStream {
159    pub fn new(file: File) -> std::io::Result<Self> {
160        let parser = ReverseCsv::new(file)?;
161        Ok(Self { parser })
162    }
163}
164
165impl Stream for CsvStream {
166    type Item = anyhow::Result<Transaction>;
167
168    fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
169        let next = self
170            .as_mut()
171            .parser
172            .next()
173            .map(|res| res.map_err(|e| anyhow!("{}", e)));
174        Poll::Ready(next)
175    }
176}
177
178struct ReverseCsv {
179    rev_lines: Peekable<RevLines<File>>,
180}
181
182impl ReverseCsv {
183    pub fn new(file: File) -> std::io::Result<Self> {
184        let reader = BufReader::new(file);
185        let rev_lines = RevLines::new(reader)?.peekable();
186
187        Ok(Self { rev_lines })
188    }
189}
190
191const HEADERS: &str = "Date,Time,Product,ISIN,Reference,Venue,Quantity,Price,,Local value,,Value,,Exchange rate,Transaction and/or third,,Total,,Order ID\n";
192
193impl Iterator for ReverseCsv {
194    type Item = anyhow::Result<Transaction>;
195
196    fn next(&mut self) -> Option<Self::Item> {
197        let line = self.rev_lines.next()?;
198        // if it is the first/headers skip
199        self.rev_lines.peek()?;
200
201        let mut input = HEADERS.to_string();
202        input.write_str(&line).unwrap();
203
204        let mut rdr = csv::ReaderBuilder::new()
205            .has_headers(true)
206            .from_reader(input.as_bytes());
207        let mut iter: DeserializeRecordsIter<_, Transaction> = rdr.deserialize();
208
209        let item = iter.next()?;
210
211        assert!(iter.peekable().peek().is_none());
212
213        let res = match item {
214            Ok(k) => anyhow::Result::Ok(k),
215            Err(e) => anyhow::Result::Err(anyhow!("{}", e)),
216        };
217
218        Some(res)
219    }
220}