up_api/v1/
accounts.rs

1use crate::v1::{Client, error, BASE_URL, standard};
2
3use serde::Deserialize;
4
5// ----------------- Response Objects -----------------
6
7#[derive(Deserialize, Debug)]
8pub struct ListAccountsResponse {
9    /// The list of accounts returned in this response.
10    pub data: Vec<AccountResource>,
11    pub links: ResponseLinks,
12}
13
14#[derive(Deserialize, Debug)]
15pub struct GetAccountResponse {
16    /// The account returned in this response.
17    pub data: AccountResource,
18}
19
20#[derive(Deserialize, Debug)]
21pub struct AccountResource {
22    /// The type of this resource: `accounts`.
23    pub r#type: String,
24    /// The unique identifier for this account.
25    pub id: String,
26    pub attributes: Attributes,
27    pub relationships: Relationships,
28    pub links: Option<AccountResourceLinks>,
29}
30
31#[derive(Deserialize, Debug)]
32pub struct AccountResourceLinks {
33    /// The canonical link to this resource within the API.
34    #[serde(rename = "self")]
35    pub this: Option<String>,
36}
37
38#[derive(Deserialize, Debug)]
39pub struct ResponseLinks {
40    /// The link to the previous page in the results. If this value is `None`
41    /// there is no previous page.
42    pub prev: Option<String>,
43    /// The link to the next page in the results. If this value is `None` there
44    ///  is no next page.
45    pub next: Option<String>,
46}
47
48#[derive(Deserialize, Debug)]
49pub struct Relationships {
50    pub transactions: Transactions,
51}
52
53#[derive(Deserialize, Debug)]
54pub struct Transactions {
55    pub links: Option<TransactionLinks>,
56}
57
58#[derive(Deserialize, Debug)]
59pub struct TransactionLinks {
60    /// The link to retrieve the related resource(s) in this relationship.
61    pub related: String,
62}
63
64#[derive(Deserialize, Debug)]
65#[serde(rename_all = "camelCase")]
66pub struct Attributes {
67    /// The name associated with the account in the Up application.
68    pub display_name: String,
69    /// The bank account type of this account. Possible values: SAVER,
70    /// TRANSACTIONAL
71    pub account_type: AccountType,
72    /// The ownership structure for this account. Possible values: INDIVIDUAL,
73    /// JOINT
74    pub ownership_type: OwnershipType,
75    /// The available balance of the account, taking into account any amounts
76    /// that are currently on hold.
77    pub balance: standard::MoneyObject,
78    /// The date-time at which this account was first opened.
79    pub created_at: String,
80}
81
82#[derive(Deserialize, Debug)]
83#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
84pub enum AccountType {
85    Saver,
86    Transactional,
87}
88
89#[derive(Deserialize, Debug)]
90#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
91pub enum OwnershipType {
92    Individual,
93    Joint,
94}
95
96// ----------------- Input Objects -----------------
97
98#[derive(Default)]
99pub struct ListAccountsOptions {
100    /// The number of records to return in each page. 
101    page_size: Option<u8>,
102    /// The type of account for which to return records. This can be used to
103    /// filter Savers from spending accounts.
104    filter_account_type: Option<String>,
105    /// The account ownership structure for which to return records. This can
106    /// be used to filter 2Up accounts from Up accounts.
107    filter_ownership_type: Option<String>,
108}
109
110impl ListAccountsOptions {
111    /// Sets the page size.
112    pub fn page_size(&mut self, value: u8) {
113        self.page_size = Some(value);
114    }
115
116    /// Sets the account type filter value.
117    pub fn filter_account_type(&mut self, value: String) {
118        self.filter_account_type = Some(value);
119    }
120
121    /// Sets the ownership type filter value.
122    pub fn filter_ownership_type(&mut self, value: String) {
123        self.filter_ownership_type = Some(value);
124    }
125
126    fn add_params(&self, url: &mut reqwest::Url) {
127        let mut query = String::new();
128
129        if let Some(value) = &self.page_size {
130            if !query.is_empty() {
131                query.push('&');
132            }
133            query.push_str(&format!("page[size]={}", value));
134        }
135
136        if let Some(value) = &self.filter_account_type {
137            if !query.is_empty() {
138                query.push('&');
139            }
140            query.push_str(
141                &format!("filter[accountType]={}", urlencoding::encode(value))
142            );
143        }
144
145        if let Some(value) = &self.filter_ownership_type {
146            if !query.is_empty() {
147                query.push('&');
148            }
149            query.push_str(
150                &format!("filter[ownershipType]={}", urlencoding::encode(value))
151            );
152        }
153
154        if !query.is_empty() {
155            url.set_query(Some(&query));
156        }
157    }
158}
159
160impl Client {
161    /// Retrieve a paginated list of all accounts for the currently
162    /// authenticated user. The returned list is paginated and can be scrolled
163    /// by following the `prev` and `next` links where present. 
164    pub async fn list_accounts(
165        &self,
166        options: &ListAccountsOptions,
167    ) -> Result<ListAccountsResponse, error::Error> {
168        let mut url = reqwest::Url::parse(
169            &format!("{}/accounts", BASE_URL)
170        ).map_err(error::Error::UrlParse)?;
171        options.add_params(&mut url);
172
173        let res = reqwest::Client::new()
174            .get(url)
175            .header("Authorization", self.auth_header())
176            .send()
177            .await
178            .map_err(error::Error::Request)?;
179
180        match res.status() {
181            reqwest::StatusCode::OK => {
182                let body = res.text().await.map_err(error::Error::BodyRead)?;
183                let account_response: ListAccountsResponse =
184                    serde_json::from_str(&body).map_err(error::Error::Json)?;
185
186                Ok(account_response)
187            },
188            _ => {
189                let body = res.text().await.map_err(error::Error::BodyRead)?;
190                let error: error::ErrorResponse =
191                    serde_json::from_str(&body).map_err(error::Error::Json)?;
192
193                Err(error::Error::Api(error))
194            }
195        }
196    }
197
198    /// Retrieve a specific account by providing its unique identifier.
199    pub async fn get_account(
200        &self,
201        id: &str,
202    ) -> Result<GetAccountResponse, error::Error> {
203        // This assertion is because without an ID the request is thought to be
204        // a request for many accounts, and therefore the error messages are
205        // very unclear.
206        if id.is_empty() {
207            panic!("The provided account ID must not be empty.");
208        }
209
210        let url = reqwest::Url::parse(
211            &format!("{}/accounts/{}", BASE_URL, id)
212        ).map_err(error::Error::UrlParse)?;
213
214        let res = reqwest::Client::new()
215            .get(url)
216            .header("Authorization", self.auth_header())
217            .send()
218            .await
219            .map_err(error::Error::Request)?;
220
221        match res.status() {
222            reqwest::StatusCode::OK => {
223                let body = res.text().await.map_err(error::Error::BodyRead)?;
224                let account_response: GetAccountResponse =
225                    serde_json::from_str(&body).map_err(error::Error::Json)?;
226
227                Ok(account_response)
228            },
229            _ => {
230                let body = res.text().await.map_err(error::Error::BodyRead)?;
231                let error: error::ErrorResponse =
232                    serde_json::from_str(&body).map_err(error::Error::Json)?;
233
234                Err(error::Error::Api(error))
235            }
236        }
237    }
238}
239
240// ----------------- Page Navigation -----------------
241
242implement_pagination_v1!(ListAccountsResponse);