sumup_rs/
checkouts.rs

1use crate::{
2    AvailablePaymentMethodsResponse, Checkout, CheckoutListQuery, CreateCheckoutRequest,
3    DeletedCheckout, ProcessCheckoutRequest, ProcessCheckoutResponse, Result, SumUpClient,
4};
5
6impl SumUpClient {
7    /// Lists created checkout resources according to the applied checkout_reference.
8    ///
9    /// # Arguments
10    /// * `checkout_reference` - Unique ID of the payment checkout specified by the client application.
11    ///
12    /// # Examples
13    ///
14    /// ```rust,no_run
15    /// use sumup_rs::SumUpClient;
16    ///
17    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
19    ///
20    /// // List all checkouts
21    /// let checkouts = client.list_checkouts(None).await?;
22    /// println!("Found {} checkouts", checkouts.len());
23    ///
24    /// // List checkouts with specific reference
25    /// let checkouts = client.list_checkouts(Some("order-123")).await?;
26    /// println!("Found {} checkouts with reference 'order-123'", checkouts.len());
27    /// # Ok(())
28    /// # }
29    /// ```
30    pub async fn list_checkouts(&self, checkout_reference: Option<&str>) -> Result<Vec<Checkout>> {
31        let query = CheckoutListQuery {
32            checkout_reference: checkout_reference.map(|s| s.to_string()),
33            status: None,
34            merchant_code: None,
35            customer_id: None,
36            limit: None,
37            offset: None,
38        };
39        self.list_checkouts_with_query(&query).await
40    }
41
42    /// Lists created checkout resources with advanced query parameters.
43    ///
44    /// # Arguments
45    /// * `query` - Query parameters for filtering and pagination
46    ///
47    /// # Examples
48    ///
49    /// ```rust,no_run
50    /// use sumup_rs::{SumUpClient, CheckoutListQuery};
51    ///
52    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
53    /// let client = SumUpClient::new("your-api-key".to_string(), true)?;
54    ///
55    /// // Create a query to filter checkouts
56    /// let query = CheckoutListQuery {
57    ///     checkout_reference: Some("order-123".to_string()),
58    ///     status: Some("PAID".to_string()),
59    ///     merchant_code: Some("merchant123".to_string()),
60    ///     customer_id: Some("customer456".to_string()),
61    ///     limit: Some(10),
62    ///     offset: Some(0),
63    /// };
64    ///
65    /// let checkouts = client.list_checkouts_with_query(&query).await?;
66    /// println!("Found {} checkouts matching criteria", checkouts.len());
67    /// # Ok(())
68    /// # }
69    /// ```
70    pub async fn list_checkouts_with_query(
71        &self,
72        query: &CheckoutListQuery,
73    ) -> Result<Vec<Checkout>> {
74        let mut url = self.build_url("/v0.1/checkouts")?;
75
76        // Add query parameters
77        {
78            let mut query_pairs = url.query_pairs_mut();
79            if let Some(ref checkout_ref) = query.checkout_reference {
80                query_pairs.append_pair("checkout_reference", checkout_ref);
81            }
82            if let Some(ref status) = query.status {
83                query_pairs.append_pair("status", status);
84            }
85            if let Some(ref merchant_code) = query.merchant_code {
86                query_pairs.append_pair("merchant_code", merchant_code);
87            }
88            if let Some(ref customer_id) = query.customer_id {
89                query_pairs.append_pair("customer_id", customer_id);
90            }
91            if let Some(limit) = query.limit {
92                query_pairs.append_pair("limit", &limit.to_string());
93            }
94            if let Some(offset) = query.offset {
95                query_pairs.append_pair("offset", &offset.to_string());
96            }
97        }
98
99        let response = self
100            .http_client
101            .get(url)
102            .bearer_auth(&self.api_key)
103            .send()
104            .await?;
105
106        if response.status().is_success() {
107            let checkouts = response.json::<Vec<Checkout>>().await?;
108            Ok(checkouts)
109        } else {
110            self.handle_error(response).await
111        }
112    }
113
114    /// Creates a new payment checkout resource.
115    ///
116    /// # Arguments
117    /// * `body` - The request body containing the details for the new checkout.
118    pub async fn create_checkout(&self, body: &CreateCheckoutRequest) -> Result<Checkout> {
119        let url = self.build_url("/v0.1/checkouts")?;
120
121        let response = self
122            .http_client
123            .post(url)
124            .bearer_auth(&self.api_key)
125            .json(body)
126            .send()
127            .await?;
128
129        if response.status().is_success() {
130            let checkout = response.json::<Checkout>().await?;
131            Ok(checkout)
132        } else {
133            self.handle_error(response).await
134        }
135    }
136
137    /// Retrieves an identified checkout resource.
138    ///
139    /// # Arguments
140    /// * `checkout_id` - The unique ID of the checkout resource.
141    pub async fn retrieve_checkout(&self, checkout_id: &str) -> Result<Checkout> {
142        let url = self.build_url(&format!("/v0.1/checkouts/{}", checkout_id))?;
143
144        let response = self
145            .http_client
146            .get(url)
147            .bearer_auth(&self.api_key)
148            .send()
149            .await?;
150
151        if response.status().is_success() {
152            let checkout = response.json::<Checkout>().await?;
153            Ok(checkout)
154        } else {
155            self.handle_error(response).await
156        }
157    }
158
159    /// Processing a checkout will attempt to charge the provided payment instrument.
160    /// This can result in immediate success or require a 3DS redirect.
161    ///
162    /// # Arguments
163    /// * `checkout_id` - The unique ID of the checkout resource to process.
164    /// * `body` - The request body containing payment details.
165    pub async fn process_checkout(
166        &self,
167        checkout_id: &str,
168        body: &ProcessCheckoutRequest,
169    ) -> Result<ProcessCheckoutResponse> {
170        let url = self.build_url(&format!("/v0.1/checkouts/{}", checkout_id))?;
171
172        let response = self
173            .http_client
174            .put(url)
175            .bearer_auth(&self.api_key)
176            .json(body)
177            .send()
178            .await?;
179
180        let status = response.status().as_u16();
181        println!("🔍 Response status: {}", status);
182
183        match status {
184            200 => {
185                // Get response text first for debugging
186                let response_text = response.text().await.unwrap_or_default();
187                println!("🔍 200 Response body: {}", response_text);
188
189                // Check if this looks like a 3DS response (has next_step)
190                if response_text.contains("next_step") {
191                    // Try to parse as CheckoutAccepted (3DS response)
192                    match serde_json::from_str::<crate::CheckoutAccepted>(&response_text) {
193                        Ok(accepted) => Ok(ProcessCheckoutResponse::Accepted(accepted)),
194                        Err(e) => {
195                            println!("🔍 Failed to parse 3DS response: {}", e);
196                            Err(crate::Error::Json(e))
197                        }
198                    }
199                } else {
200                    // Try to parse as Checkout
201                    match serde_json::from_str::<Checkout>(&response_text) {
202                        Ok(checkout) => Ok(ProcessCheckoutResponse::Success(checkout)),
203                        Err(e) => {
204                            println!("🔍 Failed to parse as Checkout: {}", e);
205                            Err(crate::Error::Json(e))
206                        }
207                    }
208                }
209            }
210            202 => {
211                let response_text = response.text().await.unwrap_or_default();
212                println!("🔍 202 Response body: {}", response_text);
213
214                match serde_json::from_str::<crate::CheckoutAccepted>(&response_text) {
215                    Ok(accepted) => Ok(ProcessCheckoutResponse::Accepted(accepted)),
216                    Err(e) => {
217                        println!("🔍 Failed to parse 202 response: {}", e);
218                        Err(crate::Error::Json(e))
219                    }
220                }
221            }
222            _ => self.handle_error(response).await,
223        }
224    }
225
226    /// Deactivates an identified checkout resource.
227    ///
228    /// # Arguments
229    /// * `checkout_id` - The unique ID of the checkout resource to deactivate.
230    pub async fn deactivate_checkout(&self, checkout_id: &str) -> Result<DeletedCheckout> {
231        let url = self.build_url(&format!("/v0.1/checkouts/{}", checkout_id))?;
232
233        let response = self
234            .http_client
235            .delete(url)
236            .bearer_auth(&self.api_key)
237            .send()
238            .await?;
239
240        if response.status().is_success() {
241            let deleted_checkout = response.json::<DeletedCheckout>().await?;
242            Ok(deleted_checkout)
243        } else {
244            self.handle_error(response).await
245        }
246    }
247
248    /// Gets available payment methods for a merchant.
249    ///
250    /// # Arguments
251    /// * `merchant_code` - The merchant's unique code.
252    /// * `amount` - The transaction amount (optional).
253    /// * `currency` - The transaction currency (optional).
254    pub async fn get_available_payment_methods(
255        &self,
256        merchant_code: &str,
257        amount: Option<f64>,
258        currency: Option<&str>,
259    ) -> Result<AvailablePaymentMethodsResponse> {
260        let mut url = self.build_url(&format!(
261            "/v0.1/merchants/{}/payment-methods",
262            merchant_code
263        ))?;
264
265        {
266            let mut query_pairs = url.query_pairs_mut();
267            if let Some(amt) = amount {
268                query_pairs.append_pair("amount", &amt.to_string());
269            }
270            if let Some(curr) = currency {
271                query_pairs.append_pair("currency", curr);
272            }
273        }
274
275        let response = self
276            .http_client
277            .get(url)
278            .bearer_auth(&self.api_key)
279            .send()
280            .await?;
281
282        if response.status().is_success() {
283            let methods = response.json::<AvailablePaymentMethodsResponse>().await?;
284            Ok(methods)
285        } else {
286            self.handle_error(response).await
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use crate::{CreateCheckoutRequest, SumUpClient};
294    use wiremock::matchers::{body_json, header, method, path};
295    use wiremock::{Mock, MockServer, ResponseTemplate};
296
297    #[tokio::test]
298    async fn test_create_checkout_success() {
299        // 1. Arrange: Start a mock server
300        let mock_server = MockServer::start().await;
301
302        // The request body we expect our client to send
303        let request_body = CreateCheckoutRequest {
304            checkout_reference: "test_ref_123".to_string(),
305            amount: 10.50,
306            currency: "EUR".to_string(),
307            merchant_code: "M123".to_string(),
308            description: Some("A test checkout".to_string()),
309            return_url: None,
310            customer_id: None,
311            purpose: None,
312            redirect_url: None,
313        };
314
315        // The response body the mock server will return
316        let response_body = serde_json::json!({
317            "id": "88fcf8de-304d-4820-8f1c-ec880290eb92",
318            "status": "PENDING",
319            "checkout_reference": "test_ref_123",
320            "amount": 10.50,
321            "currency": "EUR",
322            "merchant_code": "M123",
323            "date": "2020-02-29T10:56:56+00:00",
324            "description": "A test checkout",
325            "transactions": []
326        });
327
328        // 2. Arrange: Set up the mock response
329        Mock::given(method("POST"))
330            .and(path("/v0.1/checkouts"))
331            .and(header("Authorization", "Bearer test-api-key"))
332            .and(body_json(&request_body))
333            .respond_with(
334                ResponseTemplate::new(201) // 201 Created
335                    .set_body_json(&response_body),
336            )
337            .mount(&mock_server)
338            .await;
339
340        // 3. Act: Create a client pointing to the mock server and call the function
341        let client =
342            SumUpClient::with_custom_url("test-api-key".to_string(), mock_server.uri()).unwrap();
343        let result = client.create_checkout(&request_body).await;
344
345        // 4. Assert: Check if the result is what we expect
346        assert!(result.is_ok());
347        let checkout = result.unwrap();
348        assert_eq!(checkout.id, "88fcf8de-304d-4820-8f1c-ec880290eb92");
349        assert_eq!(checkout.status, "PENDING");
350        assert_eq!(checkout.amount, 10.50);
351    }
352
353    #[tokio::test]
354    async fn test_retrieve_checkout_success() {
355        // 1. Arrange: Start a mock server
356        let mock_server = MockServer::start().await;
357
358        let checkout_id = "88fcf8de-304d-4820-8f1c-ec880290eb92";
359
360        // The response body the mock server will return
361        let response_body = serde_json::json!({
362            "id": "88fcf8de-304d-4820-8f1c-ec880290eb92",
363            "status": "PENDING",
364            "checkout_reference": "test_ref_123",
365            "amount": 10.50,
366            "currency": "EUR",
367            "merchant_code": "M123",
368            "date": "2020-02-29T10:56:56+00:00",
369            "description": "A test checkout",
370            "transactions": []
371        });
372
373        // 2. Arrange: Set up the mock response
374        Mock::given(method("GET"))
375            .and(path(format!("/v0.1/checkouts/{}", checkout_id)))
376            .and(header("Authorization", "Bearer test-api-key"))
377            .respond_with(
378                ResponseTemplate::new(200) // 200 OK
379                    .set_body_json(&response_body),
380            )
381            .mount(&mock_server)
382            .await;
383
384        // 3. Act: Create a client pointing to the mock server and call the function
385        let client =
386            SumUpClient::with_custom_url("test-api-key".to_string(), mock_server.uri()).unwrap();
387        let result = client.retrieve_checkout(checkout_id).await;
388
389        // 4. Assert: Check if the result is what we expect
390        assert!(result.is_ok());
391        let checkout = result.unwrap();
392        assert_eq!(checkout.id, checkout_id);
393        assert_eq!(checkout.status, "PENDING");
394        assert_eq!(checkout.amount, 10.50);
395    }
396}