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 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 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(¶ms),
114 )
115 .await
116 }
117
118 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(¶ms).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(¶ms)).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 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 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 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))); assert_eq!(invoice.swapped_output, Some(dec!(1000))); assert_eq!(invoice.swapped_usd_amount, Some(dec!(1500.00))); assert_eq!(invoice.swapped_usd_rate, Some(dec!(1.50))); }
449}