1#![deny(missing_docs)]
2
3pub 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
47pub struct Fio {
49 token: String,
50}
51
52impl Fio {
53 #[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 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 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 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 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 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 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 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}