up_api/v1/
transactions.rs

1use crate::v1::{Client, error, BASE_URL, standard};
2
3use serde::Deserialize;
4
5// ----------------- Response Objects -----------------
6
7#[derive(Deserialize, Debug)]
8pub struct ListTransactionsResponse {
9    /// The list of transactions returned in this response.
10    pub data: Vec<TransactionResource>,
11    pub links: ResponseLinks,
12}
13
14#[derive(Deserialize, Debug)]
15pub struct GetTransactionResponse {
16    /// The transaction returned in this response.
17    pub data: TransactionResource,
18}
19
20#[derive(Deserialize, Debug)]
21pub struct TransactionResource {
22    /// The type of this resource: `transactions`
23    pub r#type: String,
24    /// The unique identifier for this transaction.
25    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    /// The canonical link to this resource within the API.
34    #[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    /// If this transaction is a transfer between accounts, this relationship
43    /// will contain the account the transaction went to/came from. The `amount`
44    /// field can be used to determine the direction of the transfer.
45    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    /// The type of this resource: `accounts`
60    pub r#type: String,
61    /// The unique identifier of the resource within its type.
62    pub id: String,
63}
64
65#[derive(Deserialize, Debug)]
66pub struct AccountLinks {
67    /// The link to retrieve the related resource(s) in this relationship.
68    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    /// The type of this resource: `accounts`
80    pub r#type: String,
81    /// The unique identifier of the resource within its type.
82    pub id: String,
83}
84
85#[derive(Deserialize, Debug)]
86pub struct TransferAccountLinks {
87    /// The link to retrieve the related resource(s) in this relationship.
88    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    /// The type of this resource: `categories`
100    pub r#type: String,
101    /// The unique identifier of the resource within its type.
102    pub id: String,
103}
104
105#[derive(Deserialize, Debug)]
106pub struct CategoryLinks {
107    /// The link to retrieve or modify linkage between this resources and the
108    /// related resource(s) in this relationship.
109    #[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    /// The type of this resource: `categories`
123    pub r#type: String,
124    /// The unique identifier of the resource within its type.
125    pub id: String,
126}
127
128#[derive(Deserialize, Debug)]
129pub struct ParentCategoryLinks {
130    /// The link to retrieve the related resource(s) in this relationship.
131    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    /// The type of this resource: `tags`
143    pub r#type: String,
144    /// The label of the tag, which also acts as the tag’s unique identifier.
145    pub id: String,
146}
147
148#[derive(Deserialize, Debug)]
149pub struct TagsLinks {
150    /// The link to retrieve or modify linkage between this resources and the
151    /// related resource(s) in this relationship.
152    #[serde(rename = "self")]
153    pub this: String,
154}
155
156#[derive(Deserialize, Debug)]
157#[serde(rename_all = "camelCase")]
158pub struct Attributes {
159    /// The current processing status of this transaction, according to whether
160    /// or not this transaction has settled or is still held. Possible values:
161    /// `HELD`, `SETTLED`
162    pub status: standard::TransactionStatusEnum,
163    /// The original, unprocessed text of the transaction. This is often not a
164    /// perfect indicator of the actual merchant, but it is useful for
165    /// reconciliation purposes in some cases.
166    pub raw_text: Option<String>,
167    /// A short description for this transaction. Usually the merchant name for
168    /// purchases.
169    pub description: String,
170    /// Attached message for this transaction, such as a payment message, or a
171    /// transfer note.
172    pub message: Option<String>,
173    /// Boolean flag set to true on transactions that support the use of
174    /// categories.
175    pub is_categorizable: bool,
176    /// If this transaction is currently in the `HELD` status, or was ever in
177    /// the `HELD` status, the `amount` and `foreignAmount` of the transaction
178    /// while `HELD`.
179    pub hold_info: Option<standard::HoldInfoObject>,
180    /// Details of how this transaction was rounded-up. If no Round Up was
181    /// applied this field will be `null`.
182    pub round_up: Option<standard::RoundUpObject>,
183    /// If all or part of this transaction was instantly reimbursed in the form
184    /// of cashback, details of the reimbursement.
185    pub cashback: Option<standard::CashBackObject>,
186    /// The amount of this transaction in Australian dollars. For transactions
187    /// that were once `HELD` but are now `SETTLED`, refer to the `holdInfo`
188    /// field for the original `amount` the transaction was `HELD` at.
189    pub amount: standard::MoneyObject,
190    /// The foreign currency amount of this transaction. This field will be
191    /// `null` for domestic transactions. The amount was converted to the AUD
192    /// amount reflected in the `amount` of this transaction. Refer to the
193    /// `holdInfo` field for the original foreignAmount the transaction was
194    /// `HELD` at.
195    pub foreign_amount: Option<standard::MoneyObject>,
196    /// Information about the card used for this transaction, if applicable.
197    pub card_purchase_method: Option<standard::CardPurchaseMethodObject>,
198    /// The date-time at which this transaction settled. This field will be
199    /// `null` for transactions that are currently in the `HELD` status.
200    pub settled_at: Option<String>,
201    /// The date-time at which this transaction was first encountered.
202    pub created_at: String,
203}
204
205#[derive(Deserialize, Debug)]
206pub struct ResponseLinks {
207    /// The link to the previous page in the results. If this value is null
208    /// there is no previous page.
209    pub prev: Option<String>,
210    /// The link to the next page in the results. If this value is null there is
211    /// no next page.
212    pub next: Option<String>,
213}
214
215// ----------------- Input Objects -----------------
216
217pub struct ListTransactionsOptions<Tz: chrono::TimeZone> {
218    /// The number of records to return in each page. 
219    page_size: Option<u8>,
220    /// The transaction status for which to return records. This can be used to
221    /// filter `HELD` transactions from those that are `SETTLED`.
222    filter_status: Option<String>,
223    /// The start date-time from which to return records, formatted according to
224    /// rfc-3339. Not to be used for pagination purposes.
225    filter_since: Option<chrono::DateTime<Tz>>,
226    /// The end date-time up to which to return records, formatted according to
227    /// rfc-3339. Not to be used for pagination purposes.
228    filter_until: Option<chrono::DateTime<Tz>>,
229    /// The category identifier for which to filter transactions. Both parent
230    /// and child categories can be filtered through this parameter.
231    filter_category: Option<String>,
232    /// A transaction tag to filter for which to return records. If the tag does
233    /// not exist, zero records are returned and a success response is given.
234    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    /// Sets the page size.
252    pub fn page_size(&mut self, value: u8) {
253        self.page_size = Some(value);
254    }
255
256    /// Sets the status filter value.
257    pub fn filter_status(&mut self, value: String) {
258        self.filter_status = Some(value);
259    }
260
261    /// Sets the since filter value.
262    pub fn filter_since(&mut self, value: chrono::DateTime<Tz>) {
263        self.filter_since = Some(value);
264    }
265
266    /// Sets the until filter value.
267    pub fn filter_until (&mut self, value: chrono::DateTime<Tz>) {
268        self.filter_until = Some(value);
269    }
270
271    /// Sets the category filter value.
272    pub fn filter_category(&mut self, value: String) {
273        self.filter_category = Some(value);
274    }
275
276    /// Sets the tag filter value.
277    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    /// Retrieve a list of all transactions across all accounts for the
344    /// currently authenticated user. The returned list is paginated and can be
345    /// scrolled by following the `next` and `prev` links where present. To
346    /// narrow the results to a specific date range pass one or both of
347    /// `filter[since]` and `filter[until]` in the query string. These filter
348    /// parameters should not be used for pagination. Results are ordered newest
349    /// first to oldest last.
350    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    /// Retrieve a specific transaction by providing its unique identifier.
393    pub async fn get_transaction(
394        &self,
395        id: &String,
396    ) -> Result<GetTransactionResponse, error::Error> {
397        // This assertion is because without an ID the request is thought to be
398        // a request for many transactions, and therefore the error messages
399        // are very unclear.
400        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    /// Retrieve a list of all transactions for a specific account. The returned
434    /// list is paginated and can be scrolled by following the `next` and `prev`
435    /// links where present. To narrow the results to a specific date range pass
436    /// one or both of `filter[since]` and `filter[until]` in the query string.
437    /// These filter parameters should not be used for pagination. Results are
438    /// ordered newest first to oldest last.
439    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
476// ----------------- Page Navigation -----------------
477
478implement_pagination_v1!(ListTransactionsResponse);