fiocz_rs/
lib.rs

1#![deny(missing_docs)]
2
3//! Fio API client
4//!
5//! # Example
6//!
7//! ```no_run
8//! use fiocz_rs::Fio;
9//!
10//! #[tokio::main]
11//! async fn main() {
12//!     let fio = Fio::new("token");
13//!
14//!    match fio.movements_since_last().await {
15//!       Ok(statement) => {
16//!           println!("Got newest movements containing {} transactions", statement.account_statement.transaction_list.transaction.len())
17//!      }
18//!      Err(e) => {
19//!          println!("Failed to get newest account movements: {:?}", e)
20//!      }
21//!  }
22//! }
23//! ```
24//!
25//! # Current functionality
26//!
27//! * Get account movements in period
28//! * Get account movements since last
29//! * Get account statement
30//! * Get last statement id
31//! * Set last movement id
32//! * Set last movement date
33//! * Import transactions
34//!
35
36
37pub mod error;
38pub mod types;
39
40use reqwest::StatusCode;
41use serde::{de::DeserializeOwned};
42use log::{debug, error, warn};
43use crate::error::Error;
44use crate::types::account_statement::Statement;
45use crate::types::transaction::Import;
46
47/// Fiocz API client
48pub struct Fio {
49    token: String,
50}
51
52impl Fio {
53    /// Create new API client
54    /// # Arguments
55    /// * `token` - Fio API token
56    #[must_use]
57    pub fn new(token: &str) -> Self {
58        Self { token: token.to_string() }
59    }
60
61    async fn api_get<T: DeserializeOwned>(&self, rest_method: &str) -> Result<T, Error> {
62        match self.api_get_text(rest_method).await {
63            Ok(v) => {
64                let de: Result<T, _> = serde_json::from_str(&v);
65                match de {
66                    Ok(reply) => Ok(reply),
67                    Err(e) => {
68                        error!("Couldn't parse reply for {} call: {}", rest_method, e);
69                        debug!("Source JSON: {}", v);
70                        Err(e.into())
71                    }
72                }
73            }
74            Err(e) => {
75                Err(e)
76            }
77        }
78    }
79
80    async fn api_get_text(&self, rest_method: &str) -> Result<String, Error> {
81        match reqwest::get(format!("https://fioapi.fio.cz/v1/rest/{rest_method}")).await {
82            Ok(resp) => {
83                if resp.status() == StatusCode::CONFLICT {
84                    return Err(Error::Limit);
85                }
86                if resp.status() == StatusCode::INTERNAL_SERVER_ERROR {
87                    return Err(Error::Malformed);
88                }
89                if resp.status() == StatusCode::PAYLOAD_TOO_LARGE {
90                    return Err(Error::TooLarge);
91                }
92                match resp.text().await {
93                    Ok(v) => {
94                        Ok(v)
95                    }
96                    Err(e) => {
97                        Err(e.into())
98                    }
99                }
100            }
101            Err(e) => { Err(e.into()) }
102        }
103    }
104
105    async fn api_post(&self, rest_method: &str, body: String) -> Result<String, Error> {
106        let client = reqwest::Client::new();
107        let form = reqwest::multipart::Form::new().text("token", self.token.clone()).text("type", "xml").part("file", match reqwest::multipart::Part::text(body).file_name("import.xml").mime_str("application/xml") {
108            Ok(file) => { file }
109            Err(e) => {
110                return Err(e.into());
111            }
112        });
113        match client.post(format!("https://fioapi.fio.cz/v1/rest/{rest_method}")).multipart(form).send().await {
114            Ok(resp) => {
115                if resp.status() == StatusCode::CONFLICT {
116                    return Err(Error::Limit);
117                }
118                if resp.status() == StatusCode::INTERNAL_SERVER_ERROR {
119                    return Err(Error::Malformed);
120                }
121                if resp.status() == StatusCode::PAYLOAD_TOO_LARGE {
122                    return Err(Error::TooLarge);
123                }
124                match resp.text().await {
125                    Ok(v) => {
126                        Ok(v)
127                    }
128                    Err(e) => {
129                        Err(e.into())
130                    }
131                }
132            }
133            Err(e) => { Err(e.into()) }
134        }
135    }
136
137    async fn api_get_empty(&self, rest_method: &str) -> Result<(), Error> {
138        match self.api_get_text(rest_method).await {
139            Ok(_) => { Ok(()) }
140            Err(e) => {
141                Err(e)
142            }
143        }
144    }
145
146    fn validate_date_string(date: &str) -> bool {
147        if date.len() != 10 {
148            error!("Incorrect length");
149            return false;
150        }
151        for (index, c) in date.chars().enumerate() {
152            if [0usize, 1usize, 2usize, 3usize, 5usize, 6usize, 8usize, 9usize].contains(&index) {
153                if !c.is_ascii_digit() {
154                    warn!("{c} is not a digit on position {index}");
155                    return false;
156                }
157            } else if c != '-' {
158                warn!("{c} is not a dash on position {index}");
159                return false;
160            }
161        }
162        true
163    }
164
165    fn validate_year_string(year: &str) -> bool {
166        if year.len() != 4 {
167            error!("Incorrect length");
168            return false;
169        }
170        for (index, c) in year.chars().enumerate() {
171            if !c.is_ascii_digit() {
172                warn!("{c} is not a digit on position {index}");
173                return false;
174            }
175        }
176        true
177    }
178
179    /// Get account movements in period
180    /// # Arguments
181    /// * `start` - Start date in format YYYY-MM-DD
182    /// * `end` - End date in format YYYY-MM-DD
183    /// # Returns
184    /// * `Statement` - Account movements
185    /// # Errors
186    /// * `Error::InvalidDateFormat` - Invalid date format
187    /// * `Error::Limit` - Too many requests
188    pub async fn movements_in_period(&self, start: &str, end: &str) -> Result<Statement, Error> {
189        if !Self::validate_date_string(start) {
190            return Err(Error::InvalidDateFormat);
191        }
192        if !Self::validate_date_string(end) {
193            return Err(Error::InvalidDateFormat);
194        }
195        self.api_get::<Statement>(&format!("periods/{token}/{start}/{end}/transactions.json", token = self.token)).await
196    }
197    /// Get account movements since last
198    /// # Returns
199    /// * `Statement` - Account movements
200    /// # Errors
201    /// * `Error::Limit` - Too many requests
202    pub async fn movements_since_last(&self) -> Result<Statement, Error> {
203        self.api_get::<Statement>(&format!("last/{token}/transactions.json", token = self.token)).await
204    }
205
206    /// Get account statement
207    /// # Arguments
208    /// * `year` - Year in format YYYY
209    /// * `id` - Statement ID
210    /// # Returns
211    /// * `Statement` - Account statement
212    /// # Errors
213    /// * `Error::InvalidDateFormat` - Invalid date format
214    pub async fn statements(&self, year: &str, id: &str) -> Result<Statement, Error> {
215        if !Self::validate_year_string(year) {
216            return Err(Error::InvalidDateFormat);
217        }
218        self.api_get::<Statement>(&format!("by-id/{token}/{year}/{id}/transactions.json", token = self.token)).await
219    }
220
221    /// Set last movement id
222    /// # Arguments
223    /// * `id` - Movement ID
224    /// # Errors
225    /// * `Error::Limit` - Too many requests
226    pub async fn set_last_id(&self, id: &str) -> Result<(), Error> {
227        self.api_get_empty(&format!("set-last-id/{token}/{id}/", token = self.token)).await
228    }
229
230    /// Set last movement date
231    /// # Arguments
232    /// * `date` - Date in format YYYY-MM-DD
233    /// # Errors
234    /// * `Error::InvalidDateFormat` - Invalid date format
235    pub async fn set_last_date(&self, date: &str) -> Result<(), Error> {
236        if !Self::validate_date_string(date) {
237            return Err(Error::InvalidDateFormat);
238        }
239        self.api_get_empty(&format!("set-last-date/{token}/{date}/", token = self.token)).await
240    }
241
242    /// Get last statement id
243    /// # Returns
244    /// * `String` - Year
245    /// * `String` - ID
246    /// # Errors
247    /// * `Error::InvalidResponse` - Invalid response
248    /// * `Error::Limit` - Too many requests
249    pub async fn last_statement_id(&self) -> Result<(String, String), Error> {
250        match self.api_get_text(&format!("lastStatement/{token}/statement", token = self.token)).await {
251            Ok(id) => {
252                id.split_once(',').map_or_else(|| Err(Error::InvalidResponse("Not enough elements returned".to_string())),
253                                               |result| Ok((result.0.to_string(), result.1.to_string())))
254            }
255            Err(e) => {
256                Err(e)
257            }
258        }
259    }
260
261    /// Import transactions
262    /// # Arguments
263    /// * `transactions` - Transactions to import
264    /// # Returns
265    /// * `String` - Import ID
266    /// # Errors
267    /// * `Error::Limit` - Too many requests
268    pub async fn import_transactions(&self, transactions: Import) -> Result<String, Error> {
269        match self.api_post("import/", transactions.to_xml()).await {
270            Ok(v) => {
271                Ok(v)
272            }
273            Err(e) => { Err(e) }
274        }
275    }
276}