Skip to main content

oanda_client/
transaction.rs

1//! OANDA transaction endpoint contracts.
2
3use crate::client::RequestParts;
4use crate::ids::{AccountID, TransactionID};
5use crate::models::{Transaction, TransactionFilter};
6use crate::validation::Validate;
7use crate::{Client, Error, Result, Timestamp};
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10
11/// Query parameters for `list_transactions`.
12///
13/// The default uses the provider’s time range, page size, and transaction filters.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
15#[serde(rename_all = "camelCase")]
16pub struct ListTransactionsQuery {
17    /// The starting time (inclusive) of the time range for the Transactions being queried. \[default=Account
18    /// Creation Time\] If the Account is created after the provided timestamp, the Account creation time will
19    /// be used as the starting time.
20    #[serde(rename = "from", default, skip_serializing_if = "Option::is_none")]
21    pub from: Option<Timestamp>,
22    /// The ending time (inclusive) of the time range for the Transactions being queried. \[default=Request
23    /// Time\]
24    #[serde(rename = "to", default, skip_serializing_if = "Option::is_none")]
25    pub to: Option<Timestamp>,
26    /// The number of Transactions to include in each page of the results. \[default=100, maximum=1000\]
27    #[serde(rename = "pageSize", default, skip_serializing_if = "Option::is_none")]
28    pub page_size: Option<i64>,
29    /// A filter for restricting the types of Transactions to retrieve.
30    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
31    pub r#type: Option<Vec<TransactionFilter>>,
32}
33
34impl Validate for ListTransactionsQuery {
35    fn validate(&self) -> Result<()> {
36        if self
37            .page_size
38            .is_some_and(|size| !(1..=1000).contains(&size))
39        {
40            return Err(Error::InvalidInput("pageSize must be 1..=1000".into()));
41        }
42        if let (Some(from), Some(to)) = (self.from, self.to)
43            && (to < from
44                || to.into_inner().signed_duration_since(from.into_inner())
45                    > chrono::Duration::days(365))
46        {
47            return Err(Error::InvalidInput(
48                "transaction range must be ordered and at most 365 days".into(),
49            ));
50        }
51        Ok(())
52    }
53}
54
55/// Successful response for `list_transactions`.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ListTransactionsResponse {
59    /// The starting time provided in the request.
60    #[serde(rename = "from", default, skip_serializing_if = "Option::is_none")]
61    pub from: Option<Timestamp>,
62    /// The ending time provided in the request.
63    #[serde(rename = "to", default, skip_serializing_if = "Option::is_none")]
64    pub to: Option<Timestamp>,
65    /// The pageSize provided in the request
66    #[serde(rename = "pageSize", default, skip_serializing_if = "Option::is_none")]
67    pub page_size: Option<i64>,
68    /// The Transaction-type filter provided in the request
69    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
70    pub r#type: Option<Vec<TransactionFilter>>,
71    /// The number of Transactions that are contained in the pages returned
72    #[serde(rename = "count", default, skip_serializing_if = "Option::is_none")]
73    pub count: Option<i64>,
74    /// The list of URLs that represent idrange queries providing the data for each page in the query results
75    #[serde(rename = "pages", default, skip_serializing_if = "Option::is_none")]
76    pub pages: Option<Vec<String>>,
77    /// The ID of the most recent Transaction created for the Account
78    #[serde(
79        rename = "lastTransactionID",
80        default,
81        skip_serializing_if = "Option::is_none"
82    )]
83    pub last_transaction_id: Option<TransactionID>,
84}
85
86impl Client {
87    /// List transactions.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
92    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
93    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
94    pub async fn list_transactions(
95        &self,
96        account_id: &AccountID,
97        query: Option<&ListTransactionsQuery>,
98    ) -> std::result::Result<
99        crate::ApiResponse<ListTransactionsResponse>,
100        crate::OperationError<crate::GenericRejection>,
101    > {
102        let account_id_encoded = crate::client::segment(account_id.as_str());
103        let path = format!("/v3/accounts/{account_id_encoded}/transactions");
104        let request = RequestParts::new(Method::GET, &path);
105        let request = match query {
106            Some(query) => request.query(query)?,
107            None => request,
108        };
109        self.execute(request).await
110    }
111}
112
113/// Successful response for `transaction_details`.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct TransactionDetailsResponse {
117    /// The details of the Transaction requested
118    #[serde(
119        rename = "transaction",
120        default,
121        skip_serializing_if = "Option::is_none"
122    )]
123    pub transaction: Option<Transaction>,
124    /// The ID of the most recent Transaction created for the Account
125    #[serde(
126        rename = "lastTransactionID",
127        default,
128        skip_serializing_if = "Option::is_none"
129    )]
130    pub last_transaction_id: Option<TransactionID>,
131}
132
133impl Client {
134    /// Transaction details.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
139    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
140    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
141    pub async fn transaction_details(
142        &self,
143        account_id: &AccountID,
144        transaction_id: &TransactionID,
145    ) -> std::result::Result<
146        crate::ApiResponse<TransactionDetailsResponse>,
147        crate::OperationError<crate::GenericRejection>,
148    > {
149        let account_id_encoded = crate::client::segment(account_id.as_str());
150        let transaction_id_encoded = crate::client::segment(transaction_id.as_str());
151        let path =
152            format!("/v3/accounts/{account_id_encoded}/transactions/{transaction_id_encoded}");
153        self.execute(RequestParts::new(Method::GET, &path)).await
154    }
155}
156
157/// Query parameters for `transactions_by_id_range`.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct TransactionsByIdRangeQuery {
161    /// The starting Transaction ID (inclusive) to fetch. \[required\]
162    #[serde(rename = "from")]
163    pub from: TransactionID,
164    /// The ending Transaction ID (inclusive) to fetch. \[required\]
165    #[serde(rename = "to")]
166    pub to: TransactionID,
167    /// The filter that restricts the types of Transactions to retrieve.
168    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
169    pub r#type: Option<Vec<TransactionFilter>>,
170}
171
172impl Validate for TransactionsByIdRangeQuery {
173    fn validate(&self) -> Result<()> {
174        let from = self.from.as_str().parse::<u64>();
175        let to = self.to.as_str().parse::<u64>();
176        if !matches!((from, to), (Ok(from), Ok(to)) if from <= to) {
177            return Err(Error::InvalidInput("invalid transaction ID range".into()));
178        }
179        Ok(())
180    }
181}
182
183/// Successful response for `transactions_by_id_range`.
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct TransactionsByIdRangeResponse {
187    /// The list of Transactions that satisfy the request.
188    #[serde(
189        rename = "transactions",
190        default,
191        skip_serializing_if = "Option::is_none"
192    )]
193    pub transactions: Option<Vec<Transaction>>,
194    /// The ID of the most recent Transaction created for the Account
195    #[serde(
196        rename = "lastTransactionID",
197        default,
198        skip_serializing_if = "Option::is_none"
199    )]
200    pub last_transaction_id: Option<TransactionID>,
201}
202
203impl Client {
204    /// Transactions by id range.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
209    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
210    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
211    pub async fn transactions_by_id_range(
212        &self,
213        account_id: &AccountID,
214        query: &TransactionsByIdRangeQuery,
215    ) -> std::result::Result<
216        crate::ApiResponse<TransactionsByIdRangeResponse>,
217        crate::OperationError<crate::GenericRejection>,
218    > {
219        let account_id_encoded = crate::client::segment(account_id.as_str());
220        let path = format!("/v3/accounts/{account_id_encoded}/transactions/idrange");
221        self.execute(RequestParts::new(Method::GET, &path).query(query)?)
222            .await
223    }
224}
225
226/// Query parameters for `transactions_since_id`.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct TransactionsSinceIdQuery {
230    /// The ID of the last Transaction fetched. This query will return all Transactions newer than the
231    /// `TransactionID`. \[required\]
232    #[serde(rename = "id")]
233    pub id: TransactionID,
234    /// A filter for restricting the types of Transactions to retrieve.
235    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
236    pub r#type: Option<Vec<TransactionFilter>>,
237}
238
239impl Validate for TransactionsSinceIdQuery {
240    fn validate(&self) -> Result<()> {
241        Ok(())
242    }
243}
244
245/// Successful response for `transactions_since_id`.
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct TransactionsSinceIdResponse {
249    /// The list of Transactions that satisfy the request.
250    #[serde(
251        rename = "transactions",
252        default,
253        skip_serializing_if = "Option::is_none"
254    )]
255    pub transactions: Option<Vec<Transaction>>,
256    /// The ID of the most recent Transaction created for the Account
257    #[serde(
258        rename = "lastTransactionID",
259        default,
260        skip_serializing_if = "Option::is_none"
261    )]
262    pub last_transaction_id: Option<TransactionID>,
263}
264
265impl Client {
266    /// Transactions since id.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
271    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
272    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
273    pub async fn transactions_since_id(
274        &self,
275        account_id: &AccountID,
276        query: &TransactionsSinceIdQuery,
277    ) -> std::result::Result<
278        crate::ApiResponse<TransactionsSinceIdResponse>,
279        crate::OperationError<crate::GenericRejection>,
280    > {
281        let account_id_encoded = crate::client::segment(account_id.as_str());
282        let path = format!("/v3/accounts/{account_id_encoded}/transactions/sinceid");
283        self.execute(RequestParts::new(Method::GET, &path).query(query)?)
284            .await
285    }
286}