sumup_rs/
transactions.rs

1use crate::{Result, SumUpClient, Transaction, TransactionHistoryResponse};
2use serde::Serialize;
3
4#[derive(Debug, Clone, Serialize, Default)]
5pub struct TransactionHistoryQuery<'a> {
6    #[serde(skip_serializing_if = "Option::is_none")]
7    pub limit: Option<i32>,
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub order: Option<&'a str>,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub newest_time: Option<&'a str>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub oldest_time: Option<&'a str>,
14    // Add other query parameters as needed
15}
16
17impl SumUpClient {
18    /// Lists detailed history of all transactions associated with the merchant profile.
19    /// Uses the modern v2.1 endpoint.
20    ///
21    /// # Arguments
22    /// * `merchant_code` - The merchant's unique code.
23    /// * `query` - A struct with query parameters for filtering and pagination.
24    pub async fn list_transactions_history(
25        &self,
26        merchant_code: &str,
27        query: &TransactionHistoryQuery<'_>,
28    ) -> Result<TransactionHistoryResponse> {
29        let url = self.build_url(&format!(
30            "/v2.1/merchants/{}/transactions/history",
31            merchant_code
32        ))?;
33
34        let response = self
35            .http_client
36            .get(url)
37            .bearer_auth(&self.api_key)
38            .query(query)
39            .send()
40            .await?;
41
42        if response.status().is_success() {
43            let history = response.json::<TransactionHistoryResponse>().await?;
44            Ok(history)
45        } else {
46            self.handle_error(response).await
47        }
48    }
49
50    /// Retrieves the full details of an identified transaction.
51    /// Uses the modern v2.1 endpoint.
52    ///
53    /// # Arguments
54    /// * `merchant_code` - The merchant's unique code.
55    /// * `transaction_id` - The transaction's unique ID.
56    pub async fn retrieve_transaction_by_id(
57        &self,
58        merchant_code: &str,
59        transaction_id: &str,
60    ) -> Result<Transaction> {
61        let mut url = self.build_url(&format!("/v2.1/merchants/{}/transactions", merchant_code))?;
62        url.query_pairs_mut().append_pair("id", transaction_id);
63
64        let response = self
65            .http_client
66            .get(url)
67            .bearer_auth(&self.api_key)
68            .send()
69            .await?;
70
71        if response.status().is_success() {
72            let transaction = response.json::<Transaction>().await?;
73            Ok(transaction)
74        } else {
75            self.handle_error(response).await
76        }
77    }
78
79    /// Refunds a transaction.
80    ///
81    /// # Arguments
82    /// * `merchant_code` - The merchant's unique code.
83    /// * `transaction_id` - The transaction's unique ID.
84    /// * `amount` - The amount to refund (optional, defaults to full amount).
85    /// * `reason` - The reason for the refund.
86    pub async fn refund_transaction(
87        &self,
88        merchant_code: &str,
89        transaction_id: &str,
90        amount: Option<f64>,
91        reason: &str,
92    ) -> Result<Transaction> {
93        let url = self.build_url(&format!(
94            "/v0.1/merchants/{}/transactions/{}/refunds",
95            merchant_code, transaction_id
96        ))?;
97
98        let mut body = serde_json::Map::new();
99        body.insert(
100            "reason".to_string(),
101            serde_json::Value::String(reason.to_string()),
102        );
103        if let Some(amt) = amount {
104            body.insert(
105                "amount".to_string(),
106                serde_json::Value::Number(serde_json::Number::from_f64(amt).unwrap()),
107            );
108        }
109
110        let response = self
111            .http_client
112            .post(url)
113            .bearer_auth(&self.api_key)
114            .json(&body)
115            .send()
116            .await?;
117
118        if response.status().is_success() {
119            let transaction = response.json::<Transaction>().await?;
120            Ok(transaction)
121        } else {
122            self.handle_error(response).await
123        }
124    }
125}
126
127// --- Pagination Helpers ---
128
129impl SumUpClient {
130    /// Extracts the next page URL from a transaction history response.
131    /// Returns `None` if there are no more pages.
132    ///
133    /// # Arguments
134    /// * `history` - The transaction history response to extract from
135    ///
136    /// # Returns
137    /// * `Some(url)` - The URL for the next page, if available
138    /// * `None` - If there are no more pages
139    ///
140    /// # Examples
141    ///
142    /// ```rust,no_run
143    /// use sumup_rs::{SumUpClient, TransactionHistoryQuery};
144    ///
145    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
146    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
147    /// let query = TransactionHistoryQuery {
148    ///     limit: Some(10),
149    ///     order: Some("desc"),
150    ///     newest_time: None,
151    ///     oldest_time: None,
152    /// };
153    /// let history = client.list_transactions_history("merchant123", &query).await?;
154    ///
155    /// if let Some(next_url) = SumUpClient::get_next_page_url_from_history(&history) {
156    ///     println!("Next page available at: {}", next_url);
157    /// } else {
158    ///     println!("No more pages available");
159    /// }
160    /// # Ok(())
161    /// # }
162    /// ```
163    pub fn get_next_page_url_from_history(history: &TransactionHistoryResponse) -> Option<String> {
164        history
165            .links
166            .iter()
167            .find(|link| link.rel == "next")
168            .map(|link| link.href.clone())
169    }
170
171    /// Extracts the previous page URL from a transaction history response.
172    /// Returns `None` if there is no previous page.
173    ///
174    /// # Arguments
175    /// * `history` - The transaction history response to extract from
176    ///
177    /// # Returns
178    /// * `Some(url)` - The URL for the previous page, if available
179    /// * `None` - If there is no previous page
180    ///
181    /// # Examples
182    ///
183    /// ```rust,no_run
184    /// use sumup_rs::{SumUpClient, TransactionHistoryQuery};
185    ///
186    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
187    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
188    /// let query = TransactionHistoryQuery {
189    ///     limit: Some(10),
190    ///     order: Some("desc"),
191    ///     newest_time: None,
192    ///     oldest_time: None,
193    /// };
194    /// let history = client.list_transactions_history("merchant123", &query).await?;
195    ///
196    /// if let Some(prev_url) = SumUpClient::get_previous_page_url_from_history(&history) {
197    ///     println!("Previous page available at: {}", prev_url);
198    /// } else {
199    ///     println!("No previous page available");
200    /// }
201    /// # Ok(())
202    /// # }
203    /// ```
204    pub fn get_previous_page_url_from_history(
205        history: &TransactionHistoryResponse,
206    ) -> Option<String> {
207        history
208            .links
209            .iter()
210            .find(|link| link.rel == "prev")
211            .map(|link| link.href.clone())
212    }
213
214    /// Checks if there are more pages available in a transaction history response.
215    ///
216    /// # Arguments
217    /// * `history` - The transaction history response to check
218    ///
219    /// # Returns
220    /// * `true` - If there are more pages available
221    /// * `false` - If this is the last page
222    ///
223    /// # Examples
224    ///
225    /// ```rust,no_run
226    /// use sumup_rs::{SumUpClient, TransactionHistoryQuery};
227    ///
228    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
229    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
230    /// let query = TransactionHistoryQuery {
231    ///     limit: Some(10),
232    ///     order: Some("desc"),
233    ///     newest_time: None,
234    ///     oldest_time: None,
235    /// };
236    /// let history = client.list_transactions_history("merchant123", &query).await?;
237    ///
238    /// if SumUpClient::has_next_page_from_history(&history) {
239    ///     println!("More pages available");
240    /// } else {
241    ///     println!("This is the last page");
242    /// }
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub fn has_next_page_from_history(history: &TransactionHistoryResponse) -> bool {
247        history.links.iter().any(|link| link.rel == "next")
248    }
249
250    /// Fetches all transactions for a merchant by automatically handling pagination.
251    /// This is a convenience method that fetches all pages and combines the results.
252    ///
253    /// # Arguments
254    /// * `merchant_code` - The merchant's unique code
255    /// * `order` - Sort order (e.g., "asc", "desc")
256    /// * `max_pages` - Maximum number of pages to fetch (None for unlimited)
257    ///
258    /// # Returns
259    /// * `Vec<Transaction>` - All transactions from all pages
260    ///
261    /// # Examples
262    ///
263    /// ```rust,no_run
264    /// use sumup_rs::SumUpClient;
265    ///
266    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
267    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
268    ///
269    /// // Fetch all transactions (up to 5 pages to avoid overwhelming the API)
270    /// let all_transactions = client.list_all_transactions_history("merchant123", Some("desc"), Some(5)).await?;
271    /// println!("Fetched {} transactions", all_transactions.len());
272    ///
273    /// // Fetch all transactions without page limit (use with caution)
274    /// let all_transactions = client.list_all_transactions_history("merchant123", Some("desc"), None).await?;
275    /// println!("Fetched {} transactions", all_transactions.len());
276    /// # Ok(())
277    /// # }
278    /// ```
279    pub async fn list_all_transactions_history(
280        &self,
281        merchant_code: &str,
282        order: Option<&str>,
283        max_pages: Option<usize>,
284    ) -> Result<Vec<Transaction>> {
285        let mut all_transactions = Vec::new();
286        let mut page_count = 0;
287        let mut newest_time: Option<String> = None;
288
289        loop {
290            // Check if we've reached the maximum number of pages
291            if let Some(max) = max_pages {
292                if page_count >= max {
293                    break;
294                }
295            }
296
297            // Fetch the current page
298            let history = self
299                .list_transactions_history(
300                    merchant_code,
301                    &TransactionHistoryQuery {
302                        limit: Some(100), // Use a reasonable page size
303                        order,
304                        newest_time: newest_time.as_deref(),
305                        oldest_time: None, // No oldest_time for this convenience method
306                    },
307                )
308                .await?;
309
310            // Check if there are more pages before moving data
311            let has_next_page = Self::has_next_page_from_history(&history);
312
313            // Get the newest time from the last transaction for the next page
314            let last_transaction = history.items.last().map(|t| t.timestamp.clone());
315
316            // Add transactions from this page
317            all_transactions.extend(history.items);
318
319            // Update newest_time for next iteration
320            if let Some(timestamp) = last_transaction {
321                newest_time = Some(timestamp);
322            } else {
323                break;
324            }
325
326            // Check if we should continue
327            if !has_next_page {
328                break;
329            }
330
331            page_count += 1;
332        }
333
334        Ok(all_transactions)
335    }
336}