crypto_pay_api/api/
invoice.rs

1use async_trait::async_trait;
2
3use crate::{
4    client::CryptoBot,
5    error::{CryptoBotError, CryptoBotResult},
6    models::{
7        APIEndpoint, APIMethod, CreateInvoiceParams, DeleteInvoiceParams, GetInvoicesParams, GetInvoicesResponse,
8        Invoice, Method,
9    },
10};
11
12use super::InvoiceAPI;
13
14#[async_trait]
15impl InvoiceAPI for CryptoBot {
16    /// Creates a new cryptocurrency invoice
17    ///
18    /// An invoice is a request for cryptocurrency payment with a specific amount
19    /// and currency. Once created, the invoice can be paid by any user.
20    ///
21    /// # Arguments
22    /// * `params` - Parameters for creating the invoice. See [`CreateInvoiceParams`] for details.
23    ///
24    /// # Returns
25    /// * `Ok(Invoice)` - The created invoice
26    /// * `Err(CryptoBotError)` - If validation fails or the request fails
27    ///
28    /// # Errors
29    /// This method will return an error if:
30    /// * The parameters are invalid (e.g., negative amount)
31    /// * The currency is not supported
32    /// * The API request fails
33    /// * The exchange rate validation fails (for paid_amount/paid_currency)
34    ///
35    /// # Example
36    /// ```no_run
37    /// use crypto_pay_api::prelude::*;
38    ///
39    /// #[tokio::main]
40    /// async fn main() -> Result<(), CryptoBotError> {
41    ///     let client = CryptoBot::builder().api_token("YOUR_API_TOKEN").build().unwrap();
42    ///     
43    ///     let params = CreateInvoiceParamsBuilder::new()
44    ///         .asset(CryptoCurrencyCode::Ton)
45    ///         .amount(dec!(10.5))
46    ///         .description("Payment for service")
47    ///         .paid_btn_name(PayButtonName::ViewItem)
48    ///         .paid_btn_url("https://example.com/order/123")
49    ///         .build(&client)
50    ///         .await
51    ///         .unwrap();
52    ///     
53    ///     let invoice = client.create_invoice(&params).await?;
54    ///     println!("Invoice created: {}", invoice.amount);
55    ///     
56    ///     Ok(())
57    /// }
58    /// ```
59    ///
60    /// # See Also
61    /// * [Invoice](struct.Invoice.html) - The structure representing an invoice
62    /// * [CreateInvoiceParams](struct.CreateInvoiceParams.html) - The parameters for creating an invoice
63    async fn create_invoice(&self, params: &CreateInvoiceParams) -> Result<Invoice, CryptoBotError> {
64        self.make_request(
65            &APIMethod {
66                endpoint: APIEndpoint::CreateInvoice,
67                method: Method::POST,
68            },
69            Some(params),
70        )
71        .await
72    }
73
74    /// Deletes an existing invoice
75    ///
76    /// Once deleted, the invoice becomes invalid and cannot be paid.
77    /// This is useful for cancelling unpaid invoices.
78    ///
79    /// # Arguments
80    /// * `invoice_id` - The unique identifier of the invoice to delete
81    ///
82    /// # Returns
83    /// * `Ok(true)` - If the invoice was successfully deleted
84    /// * `Err(CryptoBotError)` - If the invoice doesn't exist or the request fails
85    ///
86    /// # Example
87    /// ```no_run
88    /// use crypto_pay_api::prelude::*;
89    ///
90    /// #[tokio::main]
91    /// async fn main() -> Result<(), CryptoBotError> {
92    ///     let client = CryptoBot::builder().api_token("YOUR_API_TOKEN").build().unwrap();
93    ///     
94    ///     match client.delete_invoice(12345).await {
95    ///         Ok(_) => println!("Invoice deleted successfully"),
96    ///         Err(e) => eprintln!("Failed to delete invoice: {}", e),
97    ///     }
98    ///     
99    ///     Ok(())
100    /// }
101    /// ```
102    ///
103    /// # See Also
104    /// * [Invoice](struct.Invoice.html) - The structure representing an invoice
105    /// * [DeleteInvoiceParams](struct.DeleteInvoiceParams.html) - The parameters for deleting an invoice
106    async fn delete_invoice(&self, invoice_id: u64) -> CryptoBotResult<bool> {
107        let params = DeleteInvoiceParams { invoice_id };
108        self.make_request(
109            &APIMethod {
110                endpoint: APIEndpoint::DeleteInvoice,
111                method: Method::DELETE,
112            },
113            Some(&params),
114        )
115        .await
116    }
117
118    /// Gets a list of invoices with optional filtering
119    ///
120    /// Retrieves all invoices matching the specified filter parameters.
121    /// If no parameters are provided, returns all invoices.
122    ///
123    /// # Arguments
124    /// * `params` - Optional filter parameters. See [`GetInvoicesParams`] for available filters.
125    ///
126    /// # Returns
127    /// * `Ok(Vec<Invoice>)` - List of invoices matching the filter criteria
128    /// * `Err(CryptoBotError)` - If the parameters are invalid or the request fails
129    ///
130    /// # Example
131    /// ```no_run
132    /// use crypto_pay_api::prelude::*;
133    ///
134    /// #[tokio::main]
135    /// async fn main() -> Result<(), CryptoBotError> {
136    ///     let client = CryptoBot::builder().api_token("YOUR_API_TOKEN").build().unwrap();
137    ///
138    ///     let params = GetInvoicesParamsBuilder::new()
139    ///         .asset(CryptoCurrencyCode::Ton)
140    ///         .status(InvoiceStatus::Paid)
141    ///         .build()
142    ///         .unwrap();
143    ///
144    ///     let invoices = client.get_invoices(Some(&params)).await?;
145    ///
146    ///     for invoice in invoices {
147    ///         println!("Invoice #{}: {} {} (paid at: {})",
148    ///             invoice.invoice_id,
149    ///             invoice.amount,
150    ///             invoice.asset.unwrap().to_string(),
151    ///             invoice.paid_at.unwrap_or_default()
152    ///         );
153    ///     }
154    ///
155    ///     Ok(())
156    /// }
157    /// ```
158    ///
159    /// # See Also
160    /// * [Invoice](struct.Invoice.html) - The structure representing an invoice
161    /// * [GetInvoicesParams](struct.GetInvoicesParams.html) - Available filter parameters
162    async fn get_invoices(&self, params: Option<&GetInvoicesParams>) -> CryptoBotResult<Vec<Invoice>> {
163        let response: GetInvoicesResponse = self
164            .make_request(
165                &APIMethod {
166                    endpoint: APIEndpoint::GetInvoices,
167                    method: Method::GET,
168                },
169                params,
170            )
171            .await?;
172
173        Ok(response.items)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use mockito::Mock;
180    use rust_decimal_macros::dec;
181    use serde_json::json;
182
183    use super::*;
184    use crate::models::{CreateInvoiceParamsBuilder, CryptoCurrencyCode, GetInvoicesParamsBuilder, SwapToAssets};
185    use crate::utils::test_utils::TestContext;
186
187    impl TestContext {
188        pub fn mock_create_invoice_response(&mut self) -> Mock {
189            self.server
190                .mock("POST", "/createInvoice")
191                .with_header("content-type", "application/json")
192                .with_header("Crypto-Pay-API-Token", "test_token")
193                .with_body(
194                    json!({
195                        "ok": true,
196                        "result": {
197                            "invoice_id": 528890,
198                            "hash": "IVDoTcNBYEfk",
199                            "currency_type": "crypto",
200                            "asset": "TON",
201                            "amount": "10.5",
202                            "pay_url": "https://t.me/CryptoTestnetBot?start=IVDoTcNBYEfk",
203                            "bot_invoice_url": "https://t.me/CryptoTestnetBot?start=IVDoTcNBYEfk",
204                            "mini_app_invoice_url": "https://t.me/CryptoTestnetBot/app?startapp=invoice-IVDoTcNBYEfk",
205                            "web_app_invoice_url": "https://testnet-app.send.tg/invoices/IVDoTcNBYEfk",
206                            "description": "Test invoice",
207                            "status": "active",
208                            "created_at": "2025-02-08T12:11:01.341Z",
209                            "allow_comments": true,
210                            "allow_anonymous": true
211                        }
212                    })
213                    .to_string(),
214                )
215                .create()
216        }
217
218        pub fn mock_get_invoices_response(&mut self) -> Mock {
219            self.server
220                .mock("GET", "/getInvoices")
221                .with_header("content-type", "application/json")
222                .with_header("Crypto-Pay-API-Token", "test_token")
223                .with_body(json!({
224                    "ok": true,
225                    "result": {
226                        "items": [
227                            {
228                                "invoice_id": 528890,
229                                "hash": "IVDoTcNBYEfk",
230                                "currency_type": "crypto",
231                                "asset": "TON",
232                                "amount": "10.5",
233                                "pay_url": "https://t.me/CryptoTestnetBot?start=IVDoTcNBYEfk",
234                                "bot_invoice_url": "https://t.me/CryptoTestnetBot?start=IVDoTcNBYEfk",
235                                "mini_app_invoice_url": "https://t.me/CryptoTestnetBot/app?startapp=invoice-IVDoTcNBYEfk",
236                                "web_app_invoice_url": "https://testnet-app.send.tg/invoices/IVDoTcNBYEfk",
237                                "description": "Test invoice",
238                                "status": "active",
239                                "created_at": "2025-02-08T12:11:01.341Z",
240                                "allow_comments": true,
241                                "allow_anonymous": true
242                            },
243                        ]
244                    }
245                })
246                .to_string(),
247            )
248            .create()
249        }
250
251        pub fn mock_get_invoices_response_with_invoice_ids(&mut self) -> Mock {
252            self.server
253                .mock("GET", "/getInvoices")
254                .match_body(json!({ "invoice_ids": "530195"}).to_string().as_str())
255                .with_header("content-type", "application/json")
256                .with_header("Crypto-Pay-API-Token", "test_token")
257                .with_body(json!({
258                    "ok": true,
259                    "result": {
260                        "items": [
261                            {
262                                "invoice_id": 530195,
263                                "hash": "IVcKhSGh244v",
264                                "currency_type": "crypto",
265                                "asset": "BTC",
266                                "amount": "0.5",
267                                "pay_url": "https://t.me/CryptoTestnetBot?start=IVcKhSGh244v",
268                                "bot_invoice_url": "https://t.me/CryptoTestnetBot?start=IVcKhSGh244v",
269                                "mini_app_invoice_url": "https://t.me/CryptoTestnetBot/app?startapp=invoice-IVcKhSGh244v",
270                                "web_app_invoice_url": "https://testnet-app.send.tg/invoices/IVcKhSGh244v",
271                                "status": "active",
272                                "created_at": "2025-02-09T03:46:07.811Z",
273                                "allow_comments": true,
274                                "allow_anonymous": true
275                            }
276                        ]
277                    }
278                })
279                .to_string(),
280            )
281            .create()
282        }
283
284        pub fn mock_delete_invoice_response(&mut self) -> Mock {
285            self.server
286                .mock("DELETE", "/deleteInvoice")
287                .with_header("content-type", "application/json")
288                .with_header("Crypto-Pay-API-Token", "test_token")
289                .with_body(
290                    json!({
291                        "ok": true,
292                        "result": true
293                    })
294                    .to_string(),
295                )
296                .create()
297        }
298    }
299
300    #[test]
301    fn test_create_invoice() {
302        let mut ctx = TestContext::new();
303        let _m = ctx.mock_exchange_rates_response();
304        let _m = ctx.mock_create_invoice_response();
305
306        let client = CryptoBot::builder()
307            .api_token("test_token")
308            .base_url(ctx.server.url())
309            .build()
310            .unwrap();
311
312        let result = ctx.run(async {
313            let params = CreateInvoiceParamsBuilder::new()
314                .asset(CryptoCurrencyCode::Ton)
315                .amount(dec!(10.5))
316                .description("Test invoice".to_string())
317                .expires_in(3600)
318                .build(&client)
319                .await
320                .unwrap();
321
322            client.create_invoice(&params).await
323        });
324
325        println!("result: {:?}", result);
326        assert!(result.is_ok());
327
328        let invoice = result.unwrap();
329        assert_eq!(invoice.amount, dec!(10.5));
330        assert_eq!(invoice.asset, Some(CryptoCurrencyCode::Ton));
331        assert_eq!(invoice.description, Some("Test invoice".to_string()));
332    }
333
334    #[test]
335    fn test_get_invoices_without_params() {
336        let mut ctx = TestContext::new();
337        let _m = ctx.mock_get_invoices_response();
338        let client = CryptoBot::builder()
339            .api_token("test_token")
340            .base_url(ctx.server.url())
341            .build()
342            .unwrap();
343        let result = ctx.run(async { client.get_invoices(None).await });
344
345        println!("result:{:?}", result);
346
347        assert!(result.is_ok());
348
349        let invoices = result.unwrap();
350        assert!(!invoices.is_empty());
351        assert_eq!(invoices.len(), 1);
352    }
353
354    #[test]
355    fn test_get_invoices_with_params() {
356        let mut ctx = TestContext::new();
357        let _m = ctx.mock_get_invoices_response_with_invoice_ids();
358        let client = CryptoBot::builder()
359            .api_token("test_token")
360            .base_url(ctx.server.url())
361            .build()
362            .unwrap();
363        let params = GetInvoicesParamsBuilder::new()
364            .invoice_ids(vec![530195])
365            .build()
366            .unwrap();
367
368        let result = ctx.run(async { client.get_invoices(Some(&params)).await });
369
370        println!("result: {:?}", result);
371
372        assert!(result.is_ok());
373
374        let invoices = result.unwrap();
375        assert!(!invoices.is_empty());
376        assert_eq!(invoices.len(), 1);
377        assert_eq!(invoices[0].invoice_id, 530195);
378    }
379
380    #[test]
381    fn test_delete_invoice() {
382        let mut ctx = TestContext::new();
383        let _m = ctx.mock_delete_invoice_response();
384
385        let client = CryptoBot::builder()
386            .api_token("test_token")
387            .base_url(ctx.server.url())
388            .build()
389            .unwrap();
390
391        let result = ctx.run(async { client.delete_invoice(528890).await });
392
393        assert!(result.is_ok());
394        assert!(result.unwrap());
395    }
396
397    #[test]
398    fn test_swap_to_assets_serialization() {
399        // This test checks that the enum serializes to the correct string
400        let asset = SwapToAssets::Usdt;
401        let serialized = serde_json::to_string(&asset).unwrap();
402        assert_eq!(serialized, "\"USDT\"");
403
404        let deserialized: SwapToAssets = serde_json::from_str("\"BTC\"").unwrap();
405        assert_eq!(deserialized, SwapToAssets::Btc);
406    }
407
408    #[test]
409    fn test_invoice_swap_fields_serialization() {
410        // This test checks that all swap-related fields serialize and deserialize correctly
411
412        let json = r#"
413        {
414            "invoice_id": 678657,
415            "hash": "IVq7Vg91PPXn",
416            "currency_type": "crypto",
417            "asset": "TON",
418            "amount": "125.5",
419            "pay_url": "https://t.me/CryptoTestnetBot?start=IVq7Vg91PPXn",
420            "bot_invoice_url": "https://t.me/CryptoTestnetBot?start=IVq7Vg91PPXn",
421            "mini_app_invoice_url": "https://t.me/CryptoTestnetBot/app?startapp=invoice-IVq7Vg91PPXn&mode=compact",
422            "web_app_invoice_url": "https://testnet-app.send.tg/invoices/IVq7Vg91PPXn",
423            "status": "active",
424            "created_at": "2025-06-17T04:23:31.810Z",
425            "allow_comments": true,
426            "allow_anonymous": true,
427            "swap_to": "TON",
428            "is_swapped": "true",
429            "swapped_uid": "unique_swap_id",
430            "swapped_to": "ETH",
431            "swapped_rate": "123.45",
432            "swapped_output": "1000",
433            "swapped_usd_amount": "1500.00",
434            "swapped_usd_rate": "1.50"
435        }
436        "#;
437        let invoice: Invoice = serde_json::from_str(json).expect("Deserialization failed");
438
439        // Now check that the fields were parsed correctly
440        assert_eq!(invoice.swap_to, Some(SwapToAssets::Ton));
441        assert_eq!(invoice.is_swapped, Some("true".to_string()));
442        assert_eq!(invoice.swapped_uid, Some("unique_swap_id".to_string()));
443        assert_eq!(invoice.swapped_to, Some(SwapToAssets::Eth));
444        assert_eq!(invoice.swapped_rate, Some(dec!(123.45))); // 123.45
445        assert_eq!(invoice.swapped_output, Some(dec!(1000))); // 1000
446        assert_eq!(invoice.swapped_usd_amount, Some(dec!(1500.00))); // 1500.00
447        assert_eq!(invoice.swapped_usd_rate, Some(dec!(1.50))); // 1.50
448    }
449}