crypto_pay_api/api/
misc.rs

1use chrono::{DateTime, Utc};
2
3use crate::{
4    client::CryptoBot,
5    error::{CryptoBotError, CryptoBotResult, ValidationErrorKind},
6    models::{APIEndpoint, APIMethod, AppStats, Currency, GetMeResponse, GetStatsParams, Method},
7};
8use async_trait::async_trait;
9
10use super::MiscAPI;
11
12pub struct GetMeBuilder<'a> {
13    client: &'a CryptoBot,
14}
15
16impl<'a> GetMeBuilder<'a> {
17    pub fn new(client: &'a CryptoBot) -> Self {
18        Self { client }
19    }
20
21    /// Executes the request to get application information
22    pub async fn execute(self) -> CryptoBotResult<GetMeResponse> {
23        self.client
24            .make_request(
25                &APIMethod {
26                    endpoint: APIEndpoint::GetMe,
27                    method: Method::GET,
28                },
29                None::<&()>,
30            )
31            .await
32    }
33}
34
35pub struct GetCurrenciesBuilder<'a> {
36    client: &'a CryptoBot,
37}
38
39impl<'a> GetCurrenciesBuilder<'a> {
40    pub fn new(client: &'a CryptoBot) -> Self {
41        Self { client }
42    }
43
44    /// Executes the request to get supported currencies
45    pub async fn execute(self) -> CryptoBotResult<Vec<Currency>> {
46        self.client
47            .make_request(
48                &APIMethod {
49                    endpoint: APIEndpoint::GetCurrencies,
50                    method: Method::GET,
51                },
52                None::<&()>,
53            )
54            .await
55    }
56}
57
58pub struct GetStatsBuilder<'a> {
59    client: &'a CryptoBot,
60    params: GetStatsParams,
61}
62
63impl<'a> GetStatsBuilder<'a> {
64    pub fn new(client: &'a CryptoBot) -> Self {
65        Self {
66            client,
67            params: GetStatsParams::default(),
68        }
69    }
70
71    /// Set the start date for the statistics.
72    /// Optional. Defaults is current date minus 24 hours.
73    pub fn start_at(mut self, start_at: DateTime<Utc>) -> Self {
74        self.params.start_at = Some(start_at);
75        self
76    }
77
78    /// Set the end date for the statistics.
79    /// Optional. Defaults is current date.
80    pub fn end_at(mut self, end_at: DateTime<Utc>) -> Self {
81        self.params.end_at = Some(end_at);
82        self
83    }
84
85    /// Executes the request to get application statistics
86    pub async fn execute(self) -> CryptoBotResult<AppStats> {
87        let now = Utc::now();
88
89        if let Some(start) = self.params.start_at {
90            if start > now {
91                return Err(CryptoBotError::ValidationError {
92                    kind: ValidationErrorKind::Range,
93                    message: "start_at cannot be in the future".to_string(),
94                    field: Some("start_at".to_string()),
95                });
96            }
97        }
98
99        if let (Some(start), Some(end)) = (self.params.start_at, self.params.end_at) {
100            if end < start {
101                return Err(CryptoBotError::ValidationError {
102                    kind: ValidationErrorKind::Range,
103                    message: "end_at cannot be earlier than start_at".to_string(),
104                    field: Some("end_at".to_string()),
105                });
106            }
107        }
108
109        self.client
110            .make_request(
111                &APIMethod {
112                    endpoint: APIEndpoint::GetStats,
113                    method: Method::GET,
114                },
115                Some(&self.params),
116            )
117            .await
118    }
119}
120
121#[async_trait]
122impl MiscAPI for CryptoBot {
123    /// Gets basic information about your application
124    ///
125    /// Retrieves information about your application, including app ID, name,
126    /// and payment processing bot username.
127    ///
128    /// # Returns
129    /// * `GetMeBuilder` - A builder to execute the request
130    fn get_me(&self) -> GetMeBuilder<'_> {
131        GetMeBuilder::new(self)
132    }
133
134    /// Gets a list of all supported cryptocurrencies
135    ///
136    /// Returns information about all cryptocurrencies supported by CryptoBot,
137    /// including both crypto and fiat currencies.
138    ///
139    /// # Returns
140    /// * `GetCurrenciesBuilder` - A builder to execute the request
141    fn get_currencies(&self) -> GetCurrenciesBuilder<'_> {
142        GetCurrenciesBuilder::new(self)
143    }
144
145    /// Gets application statistics for a specified time period
146    ///
147    /// Retrieves statistics about your application's usage, including
148    /// transaction volumes, number of invoices, and user counts.
149    ///
150    /// # Returns
151    /// * `GetStatsBuilder` - A builder to construct the filter parameters
152    fn get_stats(&self) -> GetStatsBuilder<'_> {
153        GetStatsBuilder::new(self)
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use chrono::{Duration, Utc};
160    use mockito::Mock;
161    use rust_decimal::Decimal;
162    use serde_json::json;
163
164    use crate::{
165        api::MiscAPI,
166        client::CryptoBot,
167        models::{CryptoCurrencyCode, CurrencyCode},
168        prelude::{CryptoBotError, ValidationErrorKind},
169        utils::test_utils::TestContext,
170    };
171
172    impl TestContext {
173        pub fn mock_get_me_response(&mut self) -> Mock {
174            self.server
175                .mock("GET", "/getMe")
176                .with_header("content-type", "application/json")
177                .with_header("Crypto-Pay-API-Token", "test_token")
178                .with_body(
179                    json!({
180                        "ok": true,
181                        "result": {
182                            "app_id": 28692,
183                            "name": "Stated Seaslug App",
184                            "payment_processing_bot_username": "CryptoTestnetBot"
185                        }
186                    })
187                    .to_string(),
188                )
189                .create()
190        }
191
192        pub fn mock_currencies_response(&mut self) -> Mock {
193            println!("Setting up mock response");
194            self.server
195                .mock("GET", "/getCurrencies")
196                .with_header("content-type", "application/json")
197                .with_header("Crypto-Pay-API-Token", "test_token")
198                .with_body(
199                    json!({
200                        "ok": true,
201                        "result": [
202                            {
203                                "is_blockchain": false,
204                                "is_stablecoin": true,
205                                "is_fiat": false,
206                                "name": "Tether",
207                                "code": "USDT",
208                                "url": "https://tether.to/",
209                                "decimals": 18
210                            },
211                            {
212                                "is_blockchain": true,
213                                "is_stablecoin": false,
214                                "is_fiat": false,
215                                "name": "Toncoin",
216                                "code": "TON",
217                                "url": "https://ton.org/",
218                                "decimals": 9
219                            },
220                        ]
221                    })
222                    .to_string(),
223                )
224                .create()
225        }
226
227        pub fn mock_get_stats_response(&mut self) -> Mock {
228            self.server
229                .mock("GET", "/getStats")
230                .with_header("content-type", "application/json")
231                .with_header("Crypto-Pay-API-Token", "test_token")
232                .with_body(
233                    json!({
234                        "ok": true,
235                        "result": {
236                            "volume": 0,
237                            "conversion": 0,
238                            "unique_users_count": 0,
239                            "created_invoice_count": 0,
240                            "paid_invoice_count": 0,
241                            "start_at": "2025-02-07T10:55:17.438Z",
242                            "end_at": "2025-02-08T10:55:17.438Z"
243                        }
244                    })
245                    .to_string(),
246                )
247                .create()
248        }
249    }
250
251    #[test]
252    fn test_get_me() {
253        let mut ctx = TestContext::new();
254        let _m = ctx.mock_get_me_response();
255
256        let client = CryptoBot::builder()
257            .api_token("test_token")
258            .base_url(ctx.server.url())
259            .build()
260            .unwrap();
261
262        let result = ctx.run(async { client.get_me().execute().await });
263
264        println!("Result: {:?}", result);
265
266        assert!(result.is_ok());
267        let me = result.unwrap();
268        assert_eq!(me.app_id, 28692);
269        assert_eq!(me.name, "Stated Seaslug App");
270        assert_eq!(me.payment_processing_bot_username, "CryptoTestnetBot");
271        assert_eq!(me.webhook_endpoint, None);
272    }
273
274    #[test]
275    fn test_get_currencies() {
276        let mut ctx = TestContext::new();
277        let _m = ctx.mock_currencies_response();
278
279        let client = CryptoBot::builder()
280            .api_token("test_token")
281            .base_url(ctx.server.url())
282            .build()
283            .unwrap();
284
285        let result = ctx.run(async { client.get_currencies().execute().await });
286
287        assert!(result.is_ok());
288        let currencies = result.unwrap();
289
290        assert_eq!(currencies.len(), 2); // Mocked only 2
291        assert_eq!(currencies[0].code, CurrencyCode::Crypto(CryptoCurrencyCode::Usdt));
292        assert_eq!(currencies[1].code, CurrencyCode::Crypto(CryptoCurrencyCode::Ton));
293    }
294
295    #[test]
296    fn test_get_stats_without_params() {
297        let mut ctx = TestContext::new();
298        let _m = ctx.mock_get_stats_response();
299
300        let client = CryptoBot::builder()
301            .api_token("test_token")
302            .base_url(ctx.server.url())
303            .build()
304            .unwrap();
305
306        let result = ctx.run(async { client.get_stats().execute().await });
307
308        println!("result: {:?}", result);
309
310        assert!(result.is_ok());
311        let stats = result.unwrap();
312        assert_eq!(stats.volume, Decimal::from(0));
313        assert_eq!(stats.conversion, Decimal::from(0));
314    }
315
316    #[test]
317    fn test_get_stats_with_params() {
318        let mut ctx = TestContext::new();
319        let _m = ctx.mock_get_stats_response();
320
321        let client = CryptoBot::builder()
322            .api_token("test_token")
323            .base_url(ctx.server.url())
324            .build()
325            .unwrap();
326
327        let result = ctx.run(async {
328            client
329                .get_stats()
330                .start_at(Utc::now() - Duration::days(7))
331                .end_at(Utc::now())
332                .execute()
333                .await
334        });
335
336        assert!(result.is_ok());
337        let stats = result.unwrap();
338        assert_eq!(stats.volume, Decimal::from(0));
339        assert_eq!(stats.conversion, Decimal::from(0));
340    }
341
342    #[test]
343    fn test_get_stats_start_date_in_future_rejected() {
344        let ctx = TestContext::new();
345        let client = CryptoBot::builder()
346            .api_token("test_token")
347            .base_url(ctx.server.url())
348            .build()
349            .unwrap();
350
351        let future = Utc::now() + Duration::days(1);
352        let result = ctx.run(async { client.get_stats().start_at(future).execute().await });
353
354        assert!(matches!(
355            result,
356            Err(CryptoBotError::ValidationError {
357                field,
358                kind: ValidationErrorKind::Range,
359                ..
360            }) if field == Some("start_at".to_string())
361        ));
362    }
363
364    #[test]
365    fn test_get_stats_end_before_start_rejected() {
366        let ctx = TestContext::new();
367        let client = CryptoBot::builder()
368            .api_token("test_token")
369            .base_url(ctx.server.url())
370            .build()
371            .unwrap();
372
373        let start = Utc::now();
374        let end = start - Duration::hours(1);
375
376        let result = ctx.run(async { client.get_stats().start_at(start).end_at(end).execute().await });
377
378        assert!(matches!(
379            result,
380            Err(CryptoBotError::ValidationError {
381                field,
382                kind: ValidationErrorKind::Range,
383                ..
384            }) if field == Some("end_at".to_string())
385        ));
386    }
387}