sumup_rs/
lib.rs

1#![allow(
2    clippy::type_complexity,
3    clippy::large_enum_variant,
4    clippy::result_large_err,
5    clippy::if_same_then_else
6)]
7use reqwest::Client;
8use url::Url;
9
10// Re-export models for easier access
11pub mod models;
12pub use models::*;
13
14// Declare modules for API endpoints
15pub mod checkouts;
16pub mod customers;
17pub mod members;
18pub mod memberships;
19pub mod merchant;
20pub mod payouts;
21pub mod readers;
22pub mod receipts;
23pub mod roles;
24pub mod transactions;
25
26// Re-export query types for convenience
27pub use transactions::TransactionHistoryQuery;
28
29// --- Custom Error Type ---
30#[derive(Debug)]
31pub enum Error {
32    Http(reqwest::Error),
33    Json(serde_json::Error),
34    Url(url::ParseError),
35    // Structured API error with parsed response body
36    ApiError { status: u16, body: ApiErrorBody },
37}
38
39/// Structured representation of SumUp API error responses
40#[derive(Debug, serde::Deserialize)]
41pub struct ApiErrorBody {
42    #[serde(rename = "type")]
43    pub error_type: Option<String>,
44    pub title: Option<String>,
45    pub status: Option<u16>,
46    pub detail: Option<String>,
47    pub error_code: Option<String>,
48    pub message: Option<String>,
49    pub param: Option<String>,
50    // Sometimes the API returns additional context
51    #[serde(flatten)]
52    pub additional_fields: std::collections::HashMap<String, serde_json::Value>,
53}
54
55impl From<reqwest::Error> for Error {
56    fn from(err: reqwest::Error) -> Self {
57        Error::Http(err)
58    }
59}
60
61impl From<serde_json::Error> for Error {
62    fn from(err: serde_json::Error) -> Self {
63        Error::Json(err)
64    }
65}
66
67impl From<url::ParseError> for Error {
68    fn from(err: url::ParseError) -> Self {
69        Error::Url(err)
70    }
71}
72
73impl std::fmt::Display for Error {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Error::Http(e) => write!(f, "HTTP error: {}", e),
77            Error::Json(e) => write!(f, "JSON error: {}", e),
78            Error::Url(e) => write!(f, "URL error: {}", e),
79            Error::ApiError { status, body } => {
80                // Try to provide the most useful error message
81                let status_str = status.to_string();
82                let message = body
83                    .detail
84                    .as_ref()
85                    .or(body.message.as_ref())
86                    .or(body.title.as_ref())
87                    .unwrap_or(&status_str);
88                write!(f, "API error {}: {}", status, message)
89            }
90        }
91    }
92}
93
94impl std::error::Error for Error {}
95
96pub type Result<T> = std::result::Result<T, Error>;
97
98// --- The Main Client ---
99pub struct SumUpClient {
100    pub(crate) http_client: Client,
101    pub(crate) api_key: String,
102    pub(crate) base_url: Url,
103}
104
105impl SumUpClient {
106    /// Creates a new client for the SumUp API.
107    ///
108    /// # Arguments
109    ///
110    /// * `api_key` - Your SumUp API key (or OAuth token).
111    /// * `use_sandbox` - Set to `true` to use the sandbox environment.
112    pub fn new(api_key: String, use_sandbox: bool) -> Result<Self> {
113        let base_url_str = if use_sandbox {
114            "https://api.sumup.com" // NOTE: The docs state the same URL for sandbox but to use a sandbox key.
115        } else {
116            "https://api.sumup.com"
117        };
118
119        Ok(Self {
120            http_client: Client::new(),
121            api_key,
122            base_url: Url::parse(base_url_str)?,
123        })
124    }
125
126    /// Creates a new client with a custom base URL.
127    ///
128    /// # Arguments
129    ///
130    /// * `api_key` - Your SumUp API key (or OAuth token).
131    /// * `base_url` - Custom base URL for the API.
132    pub fn with_custom_url(api_key: String, base_url: String) -> Result<Self> {
133        Ok(Self {
134            http_client: Client::new(),
135            api_key,
136            base_url: Url::parse(&base_url)?,
137        })
138    }
139
140    pub(crate) fn build_url(&self, path: &str) -> Result<Url> {
141        Ok(self.base_url.join(path)?)
142    }
143
144    /// Helper function to handle API error responses.
145    pub(crate) async fn handle_error<T>(&self, response: reqwest::Response) -> Result<T> {
146        let status = response.status().as_u16();
147
148        // Get the response text first
149        let response_text = response.text().await.unwrap_or_default();
150
151        // Try to parse the error response as structured JSON
152        let body = match serde_json::from_str::<ApiErrorBody>(&response_text) {
153            Ok(parsed_body) => parsed_body,
154            Err(_) => {
155                // Fall back to plain text if JSON parsing fails
156                ApiErrorBody {
157                    error_type: None,
158                    title: None,
159                    status: Some(status),
160                    detail: Some(response_text),
161                    error_code: None,
162                    message: None,
163                    param: None,
164                    additional_fields: std::collections::HashMap::new(),
165                }
166            }
167        };
168
169        Err(Error::ApiError { status, body })
170    }
171
172    /// Get the current API key being used by the client.
173    pub fn api_key(&self) -> &str {
174        &self.api_key
175    }
176
177    /// Get the base URL being used by the client.
178    pub fn base_url(&self) -> &Url {
179        &self.base_url
180    }
181}