diff --git a/Cargo.toml b/Cargo.toml index cfc4fb4..ab916b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ name = "up-api" version = "0.1.0" edition = "2021" -#description = "A convenient and easy to use wrapper for the Up Bank API." -#license = "MIT OR Apache-2.0" -#authors = ["Aaron Manning"] -#repository = "https://github.com/aaron-jack-manning/up-api" +description = "A convenient and easy to use wrapper for the Up Bank API." +license = "MIT OR Apache-2.0" +authors = ["Aaron Manning"] +repository = "https://github.com/aaron-jack-manning/up-api" [dependencies] reqwest = "0.11.11" diff --git a/README.md b/README.md index eb0ac4f..e9d6994 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,37 @@ A convenient and easy to use wrapper for the [Up Bank API](https://developer.up. ## Example -The following example shows the calculation of the sum of all transactions after a given date. +The following example shows the calculation of the sum of all transactions after a given date (up to the page limit). ``` -use up_api::Client; -use up_api::transactions::ListTransactionsOptions; +use up_api::v1::Client; +use up_api::v1::transactions::ListTransactionsOptions; #[tokio::main] async fn main() { let token = std::env::var("UP_ACCESS_TOKEN").unwrap(); + let client = Client::new(token.to_string()); let mut options = ListTransactionsOptions::default(); - options.filter_since("2020-01-01T01:02:03+10:00".to_string()); + options.filter_since("2020-01-01T01:02:03Z".to_string()); + options.page_size(100); - let transactions = client.list_transactions().unwrap(); + let transactions = client.list_transactions(&options).await.unwrap(); let total : f32 = transactions .data .into_iter() .map(|t| t.attributes.amount.value) - .map(|v| v.parse::()) + .map(|v| v.parse::().unwrap()) + .filter(|a| a > &0.0) .sum(); println!("{}", total); } ``` + +## Planned Features + +Currently this API wrapper supports all of the `v1` Up API endpoints except [webhooks](https://developer.up.com.au/#webhooks). This is planned for a (hopefull soon) future release. diff --git a/src/lib.rs b/src/lib.rs index 564e7af..2f51e9c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,39 @@ //! # Up API //! //! A convenient and easy to use wrapper for the [Up Bank API](https://developer.up.com.au). - -// Include examples so that this exactly matches the README. +//! +//! ## Example +//! +//! The following example shows the calculation of the sum of all transactions after a given date (up to the page limit). +//! +//! ``` +//! use up_api::v1::Client; +//! use up_api::v1::transactions::ListTransactionsOptions; +//! +//! #[tokio::main] +//! async fn main() { +//! let token = std::env::var("UP_ACCESS_TOKEN").unwrap(); +//! +//! let client = Client::new(token.to_string()); +//! +//! let mut options = ListTransactionsOptions::default(); +//! options.filter_since("2020-01-01T01:02:03Z".to_string()); +//! options.page_size(100); +//! +//! let transactions = client.list_transactions(&options).await.unwrap(); +//! +//! let total : f32 = +//! transactions +//! .data +//! .into_iter() +//! .map(|t| t.attributes.amount.value) +//! .map(|v| v.parse::().unwrap()) +//! .filter(|a| a > &0.0) +//! .sum(); +//! +//! println!("{}", total); +//! } +//! ``` /// Module for interacting with the v1 (beta) release of the Up API. pub mod v1; diff --git a/src/v1/accounts.rs b/src/v1/accounts.rs index 1120aac..c328a0e 100644 --- a/src/v1/accounts.rs +++ b/src/v1/accounts.rs @@ -1,24 +1,37 @@ -use crate::v1::{Client, error, BASE_URL}; +use crate::v1::{Client, error, BASE_URL, standard}; use serde::Deserialize; -#[derive(Deserialize, Debug)] -pub struct GetAccountResponse { - /// The account returned in this response. - pub data : Data, -} +// ----------------- Response Objects ----------------- #[derive(Deserialize, Debug)] pub struct ListAccountsResponse { /// The list of accounts returned in this response. - pub data : Vec, + pub data : Vec, pub links : ResponseLinks, } #[derive(Deserialize, Debug)] -pub struct DataLinks { - #[serde(rename = "self")] +pub struct GetAccountResponse { + /// The account returned in this response. + pub data : AccountResource, +} + +#[derive(Deserialize, Debug)] +pub struct AccountResource { + /// The type of this resource: `accounts`. + pub r#type : String, + /// The unique identifier for this account. + pub id : String, + pub attributes : Attributes, + pub relationships : Relationships, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct AccountResourceLinks { /// The canonical link to this resource within the API. + #[serde(rename = "self")] pub this : Option, } @@ -30,17 +43,6 @@ pub struct ResponseLinks { pub next : Option, } -#[derive(Deserialize, Debug)] -pub struct Data { - /// The type of this resource: `accounts`. - pub r#type : String, - /// The unique identifier for this account. - pub id : String, - pub attributes : Attributes, - pub relationships : Relationships, - pub links : Option, -} - #[derive(Deserialize, Debug)] pub struct Relationships { pub transactions : Transactions, @@ -58,34 +60,21 @@ pub struct TransactionLinks { } #[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] pub struct Attributes { - #[serde(rename = "displayName")] /// The name associated with the account in the Up application. pub display_name : String, - #[serde(rename = "accountType")] /// The bank account type of this account. Possible values: SAVER, TRANSACTIONAL pub account_type : String, - #[serde(rename = "ownershipType")] /// The ownership structure for this account. Possible values: INDIVIDUAL, JOINT pub ownership_type : String, /// The available balance of the account, taking into account any amounts that are currently on hold. - pub balance : Balance, - #[serde(rename = "createdAt")] + pub balance : standard::MoneyObject, /// The date-time at which this account was first opened. pub created_at : String, } -#[derive(Deserialize, Debug)] -pub struct Balance { - #[serde(rename = "currencyCode")] - /// The ISO 4217 currency code. - pub currency_code : String, - /// The amount of money, formatted as a string in the relevant currency. For example, for an Australian dollar value of $10.56, this field will be `"10.56"`. The currency symbol is not included in the string - pub value : String, - #[serde(rename = "valueInBaseUnits")] - /// The amount of money in the smallest denomination for the currency, as a 64-bit integer. For example, for an Australian dollar value of $10.56, this field will be `1056`. - pub value_in_base_units : i64, -} +// ----------------- Input Objects ----------------- #[derive(Default)] pub struct ListAccountsOptions { @@ -94,7 +83,7 @@ pub struct ListAccountsOptions { /// The type of account for which to return records. This can be used to filter Savers from spending accounts. filter_account_type : Option, /// The account ownership structure for which to return records. This can be used to filter 2Up accounts from Up accounts. - filter_owership_type : Option, + filter_ownership_type : Option, } impl ListAccountsOptions { @@ -109,21 +98,36 @@ impl ListAccountsOptions { } /// Sets the ownership type filter value. - pub fn filter_owership_type(&mut self, value : String) { - self.filter_owership_type = Some(value); + pub fn filter_ownership_type(&mut self, value : String) { + self.filter_ownership_type = Some(value); } fn add_params(&self, url : &mut reqwest::Url) { + let mut query = String::new(); + if let Some(value) = &self.page_size { - url.set_query(Some(&format!("page[size]={}", value))); + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("page[size]={}", value)); } if let Some(value) = &self.filter_account_type { - url.set_query(Some(&format!("filter[accountType]={}", value))); + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[accountType]={}", value)); } - if let Some(value) = &self.filter_owership_type { - url.set_query(Some(&format!("filter[ownershipType]={}", value))); + if let Some(value) = &self.filter_ownership_type { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[ownershipType]={}", value)); + } + + if !query.is_empty() { + url.set_query(Some(&query)); } } } @@ -144,7 +148,6 @@ impl Client { match res.status() { reqwest::StatusCode::OK => { let body = res.text().await.map_err(error::Error::BodyRead)?; - println!("{}", body); let account_response : ListAccountsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; Ok(account_response) @@ -178,7 +181,6 @@ impl Client { match res.status() { reqwest::StatusCode::OK => { let body = res.text().await.map_err(error::Error::BodyRead)?; - println!("{}", body); let account_response : GetAccountResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; Ok(account_response) diff --git a/src/v1/categories.rs b/src/v1/categories.rs index e69de29..b686f54 100644 --- a/src/v1/categories.rs +++ b/src/v1/categories.rs @@ -0,0 +1,235 @@ +use crate::v1::{Client, error, BASE_URL}; + +use serde::{Deserialize, Serialize}; + +// ----------------- Response Objects ----------------- + +#[derive(Deserialize, Debug)] +pub struct ListCategoriesResponse { + /// The list of categories returned in this response. + pub data : Vec, +} + +#[derive(Deserialize, Debug)] +pub struct GetCategoryResponse { + /// The category returned in this response. + pub data : CategoryResource, + +} + +#[derive(Deserialize, Debug)] +pub struct CategoryResource { + /// The type of this resource: categories + pub r#type : String, + /// The unique identifier for this category. This is a human-readable but URL-safe value. + pub id : String, + pub attributes : Attributes, + pub relationships : Relationships, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct Attributes { + /// The name of this category as seen in the Up application. + pub name : String, +} + +#[derive(Deserialize, Debug)] +pub struct Relationships { + pub parent : Parent, + pub children : Children, +} + +#[derive(Deserialize, Debug)] +pub struct Parent { + pub data : Option, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct ParentData { + /// The type of this resource: `categories` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct ParentLinks { + /// The link to retrieve the related resource(s) in this relationship. + pub related : String, +} + +#[derive(Deserialize, Debug)] +pub struct Children { + pub data : Vec, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct ChildrenData { + /// The type of this resource: `categories` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct ChildrenLinks { + /// The link to retrieve the related resource(s) in this relationship. + pub related : String, +} + +#[derive(Deserialize, Debug)] +pub struct CategoryResourceLinks { + /// The canonical link to this resource within the API. + #[serde(rename = "self")] + pub this : String, +} + +// ----------------- Input Objects ----------------- + +#[derive(Default)] +pub struct ListCategoriesOptions { + /// The unique identifier of a parent category for which to return only its children. + filter_parent : Option, +} + +impl ListCategoriesOptions { + /// Sets the parent filter value. + pub fn filter_parent(&mut self, value : String) { + self.filter_parent = Some(value); + } + + fn add_params(&self, url : &mut reqwest::Url) { + let mut query = String::new(); + + if let Some(value) = &self.filter_parent { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[parent]={}", value)); + } + + if !query.is_empty() { + url.set_query(Some(&query)); + } + } +} + +// ----------------- Input Objects ----------------- + +#[derive(Serialize)] +struct CategoriseTransactionRequest { + /// The category to set on the transaction. Set this entire key to `null` de-categorize a transaction. + data : Option, +} + +#[derive(Serialize)] +struct CategoryInputResourceIdentifier { + /// The type of this resource: `categories` + r#type : String, + /// The unique identifier of the category, as returned by the `list_categories` method. + id : String, +} + +impl Client { + /// Retrieve a list of all categories and their ancestry. The returned list is not paginated. + pub async fn list_categories(&self, options : &ListCategoriesOptions) -> Result { + let mut url = reqwest::Url::parse(&format!("{}/categories", BASE_URL)).map_err(error::Error::UrlParse)?; + options.add_params(&mut url); + + let res = reqwest::Client::new() + .get(url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::OK => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let category_response : ListCategoriesResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Ok(category_response) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } + + /// Retrieve a specific category by providing its unique identifier. + pub async fn get_category(&self, id : &str) -> Result { + // This assertion is because without an ID the request is thought to be a request for + // many accounts, and therefore the error messages are very unclear. + if id.is_empty() { + panic!("The provided account ID must not be empty."); + } + + let url = reqwest::Url::parse(&format!("{}/categories/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; + + let res = reqwest::Client::new() + .get(url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::OK => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let category_response : GetCategoryResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Ok(category_response ) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } + + /// Updates the category associated with a transaction. Only transactions for which `is_categorizable` is set to true support this operation. The `id` is taken from the list exposed on `list_categories` and cannot be one of the top-level (parent) categories. To de-categorize a transaction, set the entire `data` key to `null`. The associated category, along with its request URL is also exposed via the category relationship on the transaction resource returned from `get_transaction`. + pub async fn categorise_transaction(&self, transaction_id : &str, category : Option<&str>) -> Result<(), error::Error> { + let url = reqwest::Url::parse(&format!("{}/transactions/{}/relationships/category", BASE_URL, transaction_id)).map_err(error::Error::UrlParse)?; + + let category = category.map(|id| { + CategoryInputResourceIdentifier { + r#type : String::from("categories"), + id : String::from(id), + } + }); + + let body = CategoriseTransactionRequest { data : category }; + let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?; + + println!("{}", body); + + let res = reqwest::Client::new() + .patch(url) + .header("Authorization", self.auth_header()) + .header("Content-Type", "application/json") + .body(body) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::NO_CONTENT => { + Ok(()) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } +} diff --git a/src/v1/error.rs b/src/v1/error.rs index 25d5719..6591490 100644 --- a/src/v1/error.rs +++ b/src/v1/error.rs @@ -40,11 +40,11 @@ impl std::error::Error for Error {} #[derive(Deserialize, Debug)] pub struct ErrorResponse { /// The list of errors returned in this response. - pub errors : Vec, + pub errors : Vec, } #[derive(Deserialize, Debug)] -pub struct UpError { +pub struct ErrorObject { /// The HTTP status code associated with this error. The status indicates the broad type of error according to HTTP semantics. pub status : String, /// A short description of this error. This should be stable across multiple occurrences of this type of error and typically expands on the reason for the status code. diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 39dfa2a..23dea07 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -2,16 +2,16 @@ pub mod error; /// Types for modelling and interacting with [accounts](https://developer.up.com.au/#accounts). pub mod accounts; -/// INCOMPLETE: Types for modelling and interacting with [categories](https://developer.up.com.au/#categories). +/// Types for modelling and interacting with [categories](https://developer.up.com.au/#categories). pub mod categories; /// Types for modelling and interacting with [tags](https://developer.up.com.au/#tags). pub mod tags; -/// INCOMPLETE: Types for modelling and interacting with [transactions](https://developer.up.com.au/#transactions). +/// Types for modelling and interacting with [transactions](https://developer.up.com.au/#transactions). pub mod transactions; /// Types for modelling and interacting with [utilities](https://developer.up.com.au/#utility_endpoints). pub mod utilities; -/// INCOMPLETE: Types for modelling and interacting with [webhooks](https://developer.up.com.au/#webhooks). -pub mod webhooks; +/// Types which are stardized (and named) across many resources. +pub mod standard; static BASE_URL : &str = "https://api.up.com.au/api/v1"; diff --git a/src/v1/standard.rs b/src/v1/standard.rs new file mode 100644 index 0000000..5ffe3c3 --- /dev/null +++ b/src/v1/standard.rs @@ -0,0 +1,82 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AccountTypeEnum { + Saver, + Transactional, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MoneyObject { + /// The ISO 4217 currency code. + pub currency_code : String, + /// The amount of money, formatted as a string in the relevant currency. For example, for an Australian dollar value of $10.56, this field will be `"10.56"`. The currency symbol is not included in the string + pub value : String, + /// The amount of money in the smallest denomination for the currency, as a 64-bit integer. For example, for an Australian dollar value of $10.56, this field will be `1056`. + pub value_in_base_units : i64, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OwnershipTypeEnum { + Individual, + Joint, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TransactionStatusEnum { + Held, + Settled, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct HoldInfoObject { + /// The amount of this transaction while in the `HELD` status, in Australian dollars. + pub amount : MoneyObject, + /// The foreign currency amount of this transaction while in the `HELD` status. This field will be `null` for domestic transactions. The amount was converted to the AUD amount reflected in the `amount` field. + pub foreign_amount : Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RoundUpObject { + /// The total amount of this Round Up, including any boosts, represented as a negative value. + pub amount : MoneyObject, + /// The portion of the Round Up `amount` owing to boosted Round Ups, represented as a negative value. If no boost was added to the Round Up this field will be `null`. + pub boost_portion : Option, +} + +#[derive(Deserialize, Debug)] +pub struct CashBackObject { + /// A brief description of why this cashback was paid. + pub description : String, + /// The total amount of cashback paid, represented as a positive value. + pub amount : MoneyObject, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CardPurchaseMethodEnum { + BarCode, + OCR, + CardPin, + CardDetails, + CardOnFile, + #[serde(rename = "ECOMMERCE")] + ECommerce, + MagneticStripe, + Contactless, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CardPurchaseMethodObject { + /// The type of card purchase. + pub method : CardPurchaseMethodEnum, + /// The last four digits of the card used for the purchase, if applicable. + pub card_number_suffix : Option, +} diff --git a/src/v1/tags.rs b/src/v1/tags.rs index 0bc800b..ba047a6 100644 --- a/src/v1/tags.rs +++ b/src/v1/tags.rs @@ -2,15 +2,17 @@ use crate::v1::{Client, error, BASE_URL}; use serde::{Deserialize, Serialize}; +// ----------------- Response Objects ----------------- + #[derive(Deserialize, Debug)] pub struct ListTagsResponse { /// The list of tags returned in this response. - pub data : Vec, + pub data : Vec, pub links : ResponseLinks, } #[derive(Deserialize, Debug)] -pub struct Data { +pub struct TagResource { /// The type of this resource: `tags` pub r#type : String, /// The label of the tag, which also acts as the tag’s unique identifier. @@ -40,6 +42,8 @@ pub struct ResponseLinks { pub next : Option, } +// ----------------- Input Objects ----------------- + #[derive(Default)] pub struct ListTagsOptions { /// The number of records to return in each page. @@ -53,12 +57,23 @@ impl ListTagsOptions { } fn add_params(&self, url : &mut reqwest::Url) { + let mut query = String::new(); + if let Some(value) = &self.page_size { - url.set_query(Some(&format!("page[size]={}", value))); + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("page[size]={}", value)); + } + + if !query.is_empty() { + url.set_query(Some(&query)) } } } +// ----------------- Request Objects ----------------- + #[derive(Serialize)] struct TagInputResourceIdentifier { /// The type of this resource: `tags` @@ -73,7 +88,6 @@ struct TagRequest { data : Vec } - impl Client { /// Retrieve a list of all tags currently in use. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered lexicographically. The transactions relationship for each tag exposes a link to get the transactions with the given tag. pub async fn list_tags(&self, options : &ListTagsOptions) -> Result { @@ -90,7 +104,6 @@ impl Client { match res.status() { reqwest::StatusCode::OK => { let body = res.text().await.map_err(error::Error::BodyRead)?; - println!("{}", body); let tags_response : ListTagsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; Ok(tags_response) @@ -123,6 +136,7 @@ impl Client { let res = reqwest::Client::new() .post(url) .header("Authorization", self.auth_header()) + .header("Content-Type", "application/json") .body(body) .send() .await @@ -160,6 +174,7 @@ impl Client { let res = reqwest::Client::new() .delete(url) .header("Authorization", self.auth_header()) + .header("Content-Type", "application/json") .body(body) .send() .await diff --git a/src/v1/transactions.rs b/src/v1/transactions.rs index e69de29..7f91cd2 100644 --- a/src/v1/transactions.rs +++ b/src/v1/transactions.rs @@ -0,0 +1,380 @@ +use crate::v1::{Client, error, BASE_URL, standard}; + +use serde::Deserialize; + +// ----------------- Response Objects ----------------- + +#[derive(Deserialize, Debug)] +pub struct ListTransactionsResponse { + /// The list of transactions returned in this response. + pub data : Vec, + pub links : ResponseLinks, +} + +#[derive(Deserialize, Debug)] +pub struct GetTransactionResponse { + /// The transaction returned in this response. + pub data : TransactionResource, +} + +#[derive(Deserialize, Debug)] +pub struct TransactionResource { + /// The type of this resource: `transactions` + pub r#type : String, + /// The unique identifier for this transaction. + pub id : String, + pub attributes : Attributes, + pub relationships : Relationships, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct TransactionResourceLinks { + /// The canonical link to this resource within the API. + #[serde(rename = "self")] + pub this : String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Relationships { + pub account : Account, + /// If this transaction is a transfer between accounts, this relationship will contain the account the transaction went to/came from. The `amount` field can be used to determine the direction of the transfer. + pub transfer_account : TransferAccount, + pub category : Category, + pub parent_category : ParentCategory, + pub tags : Tags, +} + +#[derive(Deserialize, Debug)] +pub struct Account { + pub data : AccountData, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct AccountData { + /// The type of this resource: `accounts` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct AccountLinks { + /// The link to retrieve the related resource(s) in this relationship. + pub related : String, +} + +#[derive(Deserialize, Debug)] +pub struct TransferAccount { + pub data : Option, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct TransferAccountData { + /// The type of this resource: `accounts` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct TransferAccountLinks { + /// The link to retrieve the related resource(s) in this relationship. + pub related : String, +} + +#[derive(Deserialize, Debug)] +pub struct Category { + pub data : Option, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct CategoryData { + /// The type of this resource: `categories` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct CategoryLinks { + /// The link to retrieve or modify linkage between this resources and the related resource(s) in this relationship. + #[serde(rename = "self")] + pub this : String, + pub related : Option, +} + +#[derive(Deserialize, Debug)] +pub struct ParentCategory { + pub data : Option, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct ParentCategoryData { + /// The type of this resource: `categories` + pub r#type : String, + /// The unique identifier of the resource within its type. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct ParentCategoryLinks { + /// The link to retrieve the related resource(s) in this relationship. + pub related : String, +} + +#[derive(Deserialize, Debug)] +pub struct Tags { + pub data : Vec, + pub links : Option, +} + +#[derive(Deserialize, Debug)] +pub struct TagsData { + /// The type of this resource: `tags` + pub r#type : String, + /// The label of the tag, which also acts as the tag’s unique identifier. + pub id : String, +} + +#[derive(Deserialize, Debug)] +pub struct TagsLinks { + /// The link to retrieve or modify linkage between this resources and the related resource(s) in this relationship. + #[serde(rename = "self")] + pub this : String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Attributes { + /// The current processing status of this transaction, according to whether or not this transaction has settled or is still held. Possible values: `HELD`, `SETTLED` + pub status : standard::TransactionStatusEnum, + /// The original, unprocessed text of the transaction. This is often not a perfect indicator of the actual merchant, but it is useful for reconciliation purposes in some cases. + pub raw_text : Option, + /// A short description for this transaction. Usually the merchant name for purchases. + pub description : String, + /// Attached message for this transaction, such as a payment message, or a transfer note. + pub message : Option, + /// Boolean flag set to true on transactions that support the use of categories. + pub is_categorizable : bool, + /// If this transaction is currently in the `HELD` status, or was ever in the `HELD` status, the `amount` and `foreignAmount` of the transaction while `HELD`. + pub hold_info : Option, + /// Details of how this transaction was rounded-up. If no Round Up was applied this field will be `null`. + pub round_up : Option, + /// If all or part of this transaction was instantly reimbursed in the form of cashback, details of the reimbursement. + pub cashback : Option, + /// The amount of this transaction in Australian dollars. For transactions that were once `HELD` but are now `SETTLED`, refer to the `holdInfo` field for the original `amount` the transaction was `HELD` at. + pub amount : standard::MoneyObject, + /// The foreign currency amount of this transaction. This field will be `null` for domestic transactions. The amount was converted to the AUD amount reflected in the `amount` of this transaction. Refer to the `holdInfo` field for the original foreignAmount the transaction was `HELD` at. + pub foreign_amount : Option, + /// Information about the card used for this transaction, if applicable. + pub card_purchase_method : Option, + /// The date-time at which this transaction settled. This field will be `null` for transactions that are currently in the `HELD` status. + pub settled_at : Option, + /// The date-time at which this transaction was first encountered. + pub created_at : String, +} + +#[derive(Deserialize, Debug)] +pub struct ResponseLinks { + /// The link to the previous page in the results. If this value is null there is no previous page. + pub prev : Option, + /// The link to the next page in the results. If this value is null there is no next page. + pub next : Option, +} + +// ----------------- Input Objects ----------------- + +#[derive(Default)] +pub struct ListTransactionsOptions { + /// The number of records to return in each page. + page_size : Option, + /// The transaction status for which to return records. This can be used to filter `HELD` transactions from those that are `SETTLED`. + filter_status : Option, + /// The start date-time from which to return records, formatted according to rfc-3339. Not to be used for pagination purposes. + filter_since : Option, + /// The end date-time up to which to return records, formatted according to rfc-3339. Not to be used for pagination purposes. + filter_until : Option, + /// The category identifier for which to filter transactions. Both parent and child categories can be filtered through this parameter. + filter_category : Option, + /// A transaction tag to filter for which to return records. If the tag does not exist, zero records are returned and a success response is given. + filter_tag : Option, +} + +impl ListTransactionsOptions { + /// Sets the page size. + pub fn page_size(&mut self, value : u8) { + self.page_size = Some(value); + } + + /// Sets the status filter value. + pub fn filter_status(&mut self, value : String) { + self.filter_status = Some(value); + } + + /// Sets the since filter value. + pub fn filter_since(&mut self, value : String) { + self.filter_since = Some(value); + } + + /// Sets the until filter value. + pub fn filter_until (&mut self, value : String) { + self.filter_until = Some(value); + } + + /// Sets the category filter value. + pub fn filter_category(&mut self, value : String) { + self.filter_category = Some(value); + } + + /// Sets the tag filter value. + pub fn filter_tag (&mut self, value : String) { + self.filter_tag = Some(value); + } + + fn add_params(&self, url : &mut reqwest::Url) { + let mut query = String::new(); + + if let Some(value) = &self.page_size { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("page[size]={}", value)); + } + + if let Some(value) = &self.filter_status { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[status]={}", value)); + } + + if let Some(value) = &self.filter_since { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[since]={}", value)); + } + + if let Some(value) = &self.filter_until { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[until]={}", value)); + } + + if let Some(value) = &self.filter_category { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[category]={}", value)); + } + + if let Some(value) = &self.filter_tag { + if !query.is_empty() { + query.push('&'); + } + query.push_str(&format!("filter[tag]={}", value)); + } + + if !query.is_empty() { + url.set_query(Some(&query)); + } + } +} + +impl Client { + /// Retrieve a list of all transactions across all accounts for the currently authenticated user. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. To narrow the results to a specific date range pass one or both of `filter[since]` and `filter[until]` in the query string. These filter parameters should not be used for pagination. Results are ordered newest first to oldest last. + pub async fn list_transactions(&self, options : &ListTransactionsOptions) -> Result { + let mut url = reqwest::Url::parse(&format!("{}/transactions", BASE_URL)).map_err(error::Error::UrlParse)?; + options.add_params(&mut url); + + let res = reqwest::Client::new() + .get(url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::OK => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let transaction_response : ListTransactionsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Ok(transaction_response) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } + + /// Retrieve a specific transaction by providing its unique identifier. + pub async fn get_transaction(&self, id : &String) -> Result { + // This assertion is because without an ID the request is thought to be a request for + // many accounts, and therefore the error messages are very unclear. + if id.is_empty() { + panic!("The provided transaction ID must not be empty."); + } + + let url = reqwest::Url::parse(&format!("{}/transactions/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; + + let res = reqwest::Client::new() + .get(url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::OK => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let transaction_response : GetTransactionResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Ok(transaction_response) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } + + /// Retrieve a list of all transactions for a specific account. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. To narrow the results to a specific date range pass one or both of `filter[since]` and `filter[until]` in the query string. These filter parameters should not be used for pagination. Results are ordered newest first to oldest last. + pub async fn list_transactions_by_account(&self, account_id : &String, options : &ListTransactionsOptions) -> Result { + let mut url = reqwest::Url::parse(&format!("{}/accounts/{}/transactions", BASE_URL, account_id)).map_err(error::Error::UrlParse)?; + options.add_params(&mut url); + + let res = reqwest::Client::new() + .get(url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(error::Error::Request)?; + + match res.status() { + reqwest::StatusCode::OK => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let transaction_response : ListTransactionsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Ok(transaction_response) + }, + _ => { + let body = res.text().await.map_err(error::Error::BodyRead)?; + let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; + + Err(error::Error::Api(error)) + } + } + } +} diff --git a/src/v1/utilities.rs b/src/v1/utilities.rs index 7a22022..6cde2be 100644 --- a/src/v1/utilities.rs +++ b/src/v1/utilities.rs @@ -2,6 +2,8 @@ use crate::v1::{Client, error, BASE_URL}; use serde::Deserialize; +// ----------------- Request Objects ----------------- + #[derive(Deserialize, Debug)] pub struct PingResponse { pub meta : Meta, diff --git a/src/v1/webhooks.rs b/src/v1/webhooks.rs deleted file mode 100644 index e69de29..0000000