1use crate::v1::{Client, error, BASE_URL, standard};
2
3use serde::Deserialize;
4
5#[derive(Deserialize, Debug)]
8pub struct ListTransactionsResponse {
9 pub data: Vec<TransactionResource>,
11 pub links: ResponseLinks,
12}
13
14#[derive(Deserialize, Debug)]
15pub struct GetTransactionResponse {
16 pub data: TransactionResource,
18}
19
20#[derive(Deserialize, Debug)]
21pub struct TransactionResource {
22 pub r#type: String,
24 pub id: String,
26 pub attributes: Attributes,
27 pub relationships: Relationships,
28 pub links: Option<TransactionResourceLinks>,
29}
30
31#[derive(Deserialize, Debug)]
32pub struct TransactionResourceLinks {
33 #[serde(rename = "self")]
35 pub this: String,
36}
37
38#[derive(Deserialize, Debug)]
39#[serde(rename_all = "camelCase")]
40pub struct Relationships {
41 pub account: Account,
42 pub transfer_account: TransferAccount,
46 pub category: Category,
47 pub parent_category: ParentCategory,
48 pub tags: Tags,
49}
50
51#[derive(Deserialize, Debug)]
52pub struct Account {
53 pub data: AccountData,
54 pub links: Option<AccountLinks>,
55}
56
57#[derive(Deserialize, Debug)]
58pub struct AccountData {
59 pub r#type: String,
61 pub id: String,
63}
64
65#[derive(Deserialize, Debug)]
66pub struct AccountLinks {
67 pub related: String,
69}
70
71#[derive(Deserialize, Debug)]
72pub struct TransferAccount {
73 pub data: Option<AccountData>,
74 pub links: Option<AccountLinks>,
75}
76
77#[derive(Deserialize, Debug)]
78pub struct TransferAccountData {
79 pub r#type: String,
81 pub id: String,
83}
84
85#[derive(Deserialize, Debug)]
86pub struct TransferAccountLinks {
87 pub related: String,
89}
90
91#[derive(Deserialize, Debug)]
92pub struct Category {
93 pub data: Option<CategoryData>,
94 pub links: Option<CategoryLinks>,
95}
96
97#[derive(Deserialize, Debug)]
98pub struct CategoryData {
99 pub r#type: String,
101 pub id: String,
103}
104
105#[derive(Deserialize, Debug)]
106pub struct CategoryLinks {
107 #[serde(rename = "self")]
110 pub this: String,
111 pub related: Option<String>,
112}
113
114#[derive(Deserialize, Debug)]
115pub struct ParentCategory {
116 pub data: Option<ParentCategoryData>,
117 pub links: Option<ParentCategoryLinks>,
118}
119
120#[derive(Deserialize, Debug)]
121pub struct ParentCategoryData {
122 pub r#type: String,
124 pub id: String,
126}
127
128#[derive(Deserialize, Debug)]
129pub struct ParentCategoryLinks {
130 pub related: String,
132}
133
134#[derive(Deserialize, Debug)]
135pub struct Tags {
136 pub data: Vec<TagsData>,
137 pub links: Option<TagsLinks>,
138}
139
140#[derive(Deserialize, Debug)]
141pub struct TagsData {
142 pub r#type: String,
144 pub id: String,
146}
147
148#[derive(Deserialize, Debug)]
149pub struct TagsLinks {
150 #[serde(rename = "self")]
153 pub this: String,
154}
155
156#[derive(Deserialize, Debug)]
157#[serde(rename_all = "camelCase")]
158pub struct Attributes {
159 pub status: standard::TransactionStatusEnum,
163 pub raw_text: Option<String>,
167 pub description: String,
170 pub message: Option<String>,
173 pub is_categorizable: bool,
176 pub hold_info: Option<standard::HoldInfoObject>,
180 pub round_up: Option<standard::RoundUpObject>,
183 pub cashback: Option<standard::CashBackObject>,
186 pub amount: standard::MoneyObject,
190 pub foreign_amount: Option<standard::MoneyObject>,
196 pub card_purchase_method: Option<standard::CardPurchaseMethodObject>,
198 pub settled_at: Option<String>,
201 pub created_at: String,
203}
204
205#[derive(Deserialize, Debug)]
206pub struct ResponseLinks {
207 pub prev: Option<String>,
210 pub next: Option<String>,
213}
214
215pub struct ListTransactionsOptions<Tz: chrono::TimeZone> {
218 page_size: Option<u8>,
220 filter_status: Option<String>,
223 filter_since: Option<chrono::DateTime<Tz>>,
226 filter_until: Option<chrono::DateTime<Tz>>,
229 filter_category: Option<String>,
232 filter_tag: Option<String>,
235}
236
237impl<Tz: chrono::TimeZone> Default for ListTransactionsOptions<Tz> {
238 fn default() -> Self {
239 Self {
240 page_size: None,
241 filter_status: None,
242 filter_since: None,
243 filter_until: None,
244 filter_category: None,
245 filter_tag: None,
246 }
247 }
248}
249
250impl<Tz: chrono::TimeZone> ListTransactionsOptions<Tz> {
251 pub fn page_size(&mut self, value: u8) {
253 self.page_size = Some(value);
254 }
255
256 pub fn filter_status(&mut self, value: String) {
258 self.filter_status = Some(value);
259 }
260
261 pub fn filter_since(&mut self, value: chrono::DateTime<Tz>) {
263 self.filter_since = Some(value);
264 }
265
266 pub fn filter_until (&mut self, value: chrono::DateTime<Tz>) {
268 self.filter_until = Some(value);
269 }
270
271 pub fn filter_category(&mut self, value: String) {
273 self.filter_category = Some(value);
274 }
275
276 pub fn filter_tag (&mut self, value: String) {
278 self.filter_tag = Some(value);
279 }
280
281 fn add_params(&self, url: &mut reqwest::Url) {
282 let mut query = String::new();
283
284 if let Some(value) = &self.page_size {
285 if !query.is_empty() {
286 query.push('&');
287 }
288 query.push_str(&format!("page[size]={}", value));
289 }
290
291 if let Some(value) = &self.filter_status {
292 if !query.is_empty() {
293 query.push('&');
294 }
295 query.push_str(
296 &format!("filter[status]={}", urlencoding::encode(value))
297 );
298 }
299
300 if let Some(value) = &self.filter_since {
301 if !query.is_empty() {
302 query.push('&');
303 }
304 query.push_str(
305 &format!("filter[since]={}", urlencoding::encode(&value.to_rfc3339()))
306 );
307 }
308
309 if let Some(value) = &self.filter_until {
310 if !query.is_empty() {
311 query.push('&');
312 }
313 query.push_str(
314 &format!("filter[until]={}", urlencoding::encode(&value.to_rfc3339()))
315 );
316 }
317
318 if let Some(value) = &self.filter_category {
319 if !query.is_empty() {
320 query.push('&');
321 }
322 query.push_str(
323 &format!("filter[category]={}", urlencoding::encode(value))
324 );
325 }
326
327 if let Some(value) = &self.filter_tag {
328 if !query.is_empty() {
329 query.push('&');
330 }
331 query.push_str(
332 &format!("filter[tag]={}", urlencoding::encode(value))
333 );
334 }
335
336 if !query.is_empty() {
337 url.set_query(Some(&query));
338 }
339 }
340}
341
342impl Client {
343 pub async fn list_transactions<Tz: chrono::TimeZone>(
351 &self,
352 options: &ListTransactionsOptions<Tz>,
353 ) -> Result<ListTransactionsResponse, error::Error> {
354 let mut url = reqwest::Url::parse(
355 &format!("{}/transactions", BASE_URL)
356 ).map_err(error::Error::UrlParse)?;
357 options.add_params(&mut url);
358
359 let res = reqwest::Client::new()
360 .get(url)
361 .header("Authorization", self.auth_header())
362 .send()
363 .await
364 .map_err(error::Error::Request)?;
365
366 match res.status() {
367 reqwest::StatusCode::OK => {
368 let body =
369 res.text()
370 .await
371 .map_err(error::Error::BodyRead)?;
372 let transaction_response: ListTransactionsResponse =
373 serde_json::from_str(&body)
374 .map_err(error::Error::Json)?;
375
376 Ok(transaction_response)
377 },
378 _ => {
379 let body =
380 res.text()
381 .await
382 .map_err(error::Error::BodyRead)?;
383 let error: error::ErrorResponse =
384 serde_json::from_str(&body)
385 .map_err(error::Error::Json)?;
386
387 Err(error::Error::Api(error))
388 }
389 }
390 }
391
392 pub async fn get_transaction(
394 &self,
395 id: &String,
396 ) -> Result<GetTransactionResponse, error::Error> {
397 if id.is_empty() {
401 panic!("The provided transaction ID must not be empty.");
402 }
403
404 let url = reqwest::Url::parse(
405 &format!("{}/transactions/{}", BASE_URL, id)
406 ).map_err(error::Error::UrlParse)?;
407
408 let res = reqwest::Client::new()
409 .get(url)
410 .header("Authorization", self.auth_header())
411 .send()
412 .await
413 .map_err(error::Error::Request)?;
414
415 match res.status() {
416 reqwest::StatusCode::OK => {
417 let body = res.text().await.map_err(error::Error::BodyRead)?;
418 let transaction_response: GetTransactionResponse =
419 serde_json::from_str(&body).map_err(error::Error::Json)?;
420
421 Ok(transaction_response)
422 },
423 _ => {
424 let body = res.text().await.map_err(error::Error::BodyRead)?;
425 let error: error::ErrorResponse =
426 serde_json::from_str(&body).map_err(error::Error::Json)?;
427
428 Err(error::Error::Api(error))
429 }
430 }
431 }
432
433 pub async fn list_transactions_by_account<Tz: chrono::TimeZone>(
440 &self,
441 account_id: &String,
442 options: &ListTransactionsOptions<Tz>,
443 ) -> Result<ListTransactionsResponse, error::Error> {
444 let mut url = reqwest::Url::parse(
445 &format!("{}/accounts/{}/transactions", BASE_URL, account_id)
446 ).map_err(error::Error::UrlParse)?;
447 options.add_params(&mut url);
448
449 let res = reqwest::Client::new()
450 .get(url)
451 .header("Authorization", self.auth_header())
452 .send()
453 .await
454 .map_err(error::Error::Request)?;
455
456 match res.status() {
457 reqwest::StatusCode::OK => {
458 let body = res.text().await.map_err(error::Error::BodyRead)?;
459 let transaction_response: ListTransactionsResponse =
460 serde_json::from_str(&body).map_err(error::Error::Json)?;
461
462 Ok(transaction_response)
463 },
464 _ => {
465 let body = res.text().await.map_err(error::Error::BodyRead)?;
466 let error: error::ErrorResponse =
467 serde_json::from_str(&body).map_err(error::Error::Json)?;
468
469 Err(error::Error::Api(error))
470 }
471 }
472 }
473}
474
475
476implement_pagination_v1!(ListTransactionsResponse);