crypto_pay_api/models/transfer/
builder.rs

1use std::marker::PhantomData;
2
3use rust_decimal::Decimal;
4
5use crate::{
6    api::ExchangeRateAPI,
7    client::CryptoBot,
8    error::{CryptoBotError, CryptoBotResult, ValidationErrorKind},
9    models::{CryptoCurrencyCode, Missing, Set},
10    validation::{validate_amount, validate_count, ContextValidate, FieldValidate, ValidationContext},
11};
12
13use super::params::{GetTransfersParams, TransferParams};
14
15/* #region GetTransfersParamsBuilder */
16
17#[derive(Debug, Default)]
18pub struct GetTransfersParamsBuilder {
19    asset: Option<CryptoCurrencyCode>,
20    transfer_ids: Option<Vec<u64>>,
21    spend_id: Option<String>,
22    offset: Option<u32>,
23    count: Option<u16>,
24}
25
26impl GetTransfersParamsBuilder {
27    /// Create a new `GetTransfersParamsBuilder` with default values.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Set the asset for the transfers.
33    /// Optional. Defaults to all currencies.
34    pub fn asset(mut self, asset: CryptoCurrencyCode) -> Self {
35        self.asset = Some(asset);
36        self
37    }
38
39    /// Set the transfer IDs for the transfers.
40    /// Optional.
41    pub fn transfer_ids(mut self, ids: Vec<u64>) -> Self {
42        self.transfer_ids = Some(ids);
43        self
44    }
45
46    /// Set the spend ID for the transfers.
47    /// Optional. Unique UTF-8 transfer string.
48    pub fn spend_id(mut self, spend_id: impl Into<String>) -> Self {
49        self.spend_id = Some(spend_id.into());
50        self
51    }
52
53    /// Set the offset for the transfers.
54    /// Optional. Offset needed to return a specific subset of transfers.
55    /// Defaults to 0.
56    pub fn offset(mut self, offset: u32) -> Self {
57        self.offset = Some(offset);
58        self
59    }
60
61    /// Set the count for the transfers.
62    /// Optional. Defaults to 100. Values between 1-1000 are accepted.
63    pub fn count(mut self, count: u16) -> Self {
64        self.count = Some(count);
65        self
66    }
67}
68
69impl FieldValidate for GetTransfersParamsBuilder {
70    fn validate(&self) -> CryptoBotResult<()> {
71        if let Some(count) = &self.count {
72            validate_count(*count)?;
73        }
74        Ok(())
75    }
76}
77
78impl GetTransfersParamsBuilder {
79    pub fn build(self) -> CryptoBotResult<GetTransfersParams> {
80        self.validate()?;
81
82        Ok(GetTransfersParams::new(
83            self.asset,
84            self.transfer_ids,
85            self.spend_id,
86            self.offset,
87            self.count,
88        ))
89    }
90}
91
92/* #endregion */
93
94/* #region TransferParamsBuilder */
95
96#[derive(Debug)]
97pub struct TransferParamsBuilder<U = Missing, A = Missing, M = Missing, S = Missing> {
98    user_id: u64,
99    asset: CryptoCurrencyCode,
100    amount: Decimal,
101    spend_id: String,
102    comment: Option<String>,
103    disable_send_notification: Option<bool>,
104    _state: PhantomData<(U, A, M, S)>,
105}
106
107impl TransferParamsBuilder<Missing, Missing, Missing, Missing> {
108    /// Create a new `TransferParamsBuilder` with default values.
109    pub fn new() -> TransferParamsBuilder<Missing, Missing, Missing, Missing> {
110        Self {
111            user_id: 0,
112            asset: CryptoCurrencyCode::Ton,
113            amount: Decimal::ZERO,
114            spend_id: String::new(),
115            comment: None,
116            disable_send_notification: None,
117            _state: PhantomData,
118        }
119    }
120}
121
122impl Default for TransferParamsBuilder<Missing, Missing, Missing, Missing> {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl<A, M, S> TransferParamsBuilder<Missing, A, M, S> {
129    /// Set the Telegram user ID for the transfer.
130    pub fn user_id(mut self, user_id: u64) -> TransferParamsBuilder<Set, A, M, S> {
131        self.user_id = user_id;
132        self.transform()
133    }
134}
135
136impl<U, M, S> TransferParamsBuilder<U, Missing, M, S> {
137    /// Set the asset for the transfer.
138    pub fn asset(mut self, asset: CryptoCurrencyCode) -> TransferParamsBuilder<U, Set, M, S> {
139        self.asset = asset;
140        self.transform()
141    }
142}
143
144impl<U, A, S> TransferParamsBuilder<U, A, Missing, S> {
145    /// Set the amount for the transfer.
146    /// The minimum and maximum amount limits for each of the supported assets roughly correspond to 1-25000 USD.
147    pub fn amount(mut self, amount: Decimal) -> TransferParamsBuilder<U, A, Set, S> {
148        self.amount = amount;
149        self.transform()
150    }
151}
152
153impl<U, A, M> TransferParamsBuilder<U, A, M, Missing> {
154    /// Set the spend ID for the transfer.
155    /// Random UTF-8 string unique per transfer for idempotent requests.
156    /// The same spend_id can be accepted only once from your app.
157    /// Up to 64 symbols.
158    pub fn spend_id(mut self, spend_id: impl Into<String>) -> TransferParamsBuilder<U, A, M, Set> {
159        self.spend_id = spend_id.into();
160        self.transform()
161    }
162}
163
164impl<U, A, M, S> TransferParamsBuilder<U, A, M, S> {
165    /// Set the comment for the transfer.
166    /// Optional. Comment for the transfer.
167    /// Users will see this comment in the notification about the transfer.
168    /// Up to 1024 symbols.
169    pub fn comment(mut self, comment: impl Into<String>) -> Self {
170        self.comment = Some(comment.into());
171        self
172    }
173
174    /// Set the disable send notification for the transfer.
175    /// Optional. Pass true to not send to the user the notification about the transfer.
176    /// Defaults to false.
177    pub fn disable_send_notification(mut self, disable: bool) -> Self {
178        self.disable_send_notification = Some(disable);
179        self
180    }
181
182    fn transform<U2, A2, M2, S2>(self) -> TransferParamsBuilder<U2, A2, M2, S2> {
183        TransferParamsBuilder {
184            user_id: self.user_id,
185            asset: self.asset,
186            amount: self.amount,
187            spend_id: self.spend_id,
188            comment: self.comment,
189            disable_send_notification: self.disable_send_notification,
190            _state: PhantomData,
191        }
192    }
193}
194
195impl FieldValidate for TransferParamsBuilder<Set, Set, Set, Set> {
196    fn validate(&self) -> CryptoBotResult<()> {
197        if self.spend_id.chars().count() > 64 {
198            return Err(CryptoBotError::ValidationError {
199                kind: ValidationErrorKind::Range,
200                message: "Spend ID must be less than 64 symbols".to_string(),
201                field: Some("spend_id".to_string()),
202            });
203        }
204
205        if let Some(comment) = &self.comment {
206            if comment.chars().count() > 1024 {
207                return Err(CryptoBotError::ValidationError {
208                    kind: ValidationErrorKind::Range,
209                    message: "Comment must be less than 1024 symbols".to_string(),
210                    field: Some("comment".to_string()),
211                });
212            }
213        }
214
215        Ok(())
216    }
217}
218
219#[async_trait::async_trait]
220impl ContextValidate for TransferParamsBuilder<Set, Set, Set, Set> {
221    async fn validate_with_context(&self, ctx: &ValidationContext) -> CryptoBotResult<()> {
222        validate_amount(&self.amount, &self.asset, ctx).await
223    }
224}
225
226impl TransferParamsBuilder<Set, Set, Set, Set> {
227    pub async fn build(self, client: &CryptoBot) -> CryptoBotResult<TransferParams> {
228        self.validate()?;
229
230        let rates = client.get_exchange_rates().await?;
231
232        let ctx = ValidationContext { exchange_rates: rates };
233
234        self.validate_with_context(&ctx).await?;
235
236        Ok(TransferParams {
237            user_id: self.user_id,
238            asset: self.asset,
239            amount: self.amount,
240            spend_id: self.spend_id,
241            comment: self.comment,
242            disable_send_notification: self.disable_send_notification,
243        })
244    }
245}
246
247/* #endregion */
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_get_transfers_params() {
255        let params = GetTransfersParamsBuilder::new()
256            .asset(CryptoCurrencyCode::Ton)
257            .offset(2)
258            .spend_id("spend_id")
259            .build()
260            .unwrap();
261
262        assert_eq!(params.asset, Some(CryptoCurrencyCode::Ton));
263        assert_eq!(params.offset, Some(2));
264        assert_eq!(params.spend_id, Some("spend_id".to_string()));
265    }
266
267    #[test]
268    fn test_get_transfers_params_invalid_count() {
269        let params = GetTransfersParamsBuilder::new().count(1001).build();
270
271        assert!(matches!(
272            params,
273            Err(CryptoBotError::ValidationError {
274                kind: ValidationErrorKind::Range,
275                field: Some(field),
276                ..
277            }) if field == "count"
278        ));
279    }
280
281    #[tokio::test]
282    async fn test_transfer_params() {
283        let client = CryptoBot::test_client();
284
285        let params = TransferParamsBuilder::new()
286            .user_id(123456789)
287            .asset(CryptoCurrencyCode::Ton)
288            .amount(Decimal::from(100))
289            .spend_id("test_id")
290            .comment("test comment")
291            .disable_send_notification(true)
292            .build(&client)
293            .await
294            .unwrap();
295
296        assert_eq!(params.user_id, 123456789);
297        assert_eq!(params.asset, CryptoCurrencyCode::Ton);
298        assert_eq!(params.amount, Decimal::from(100));
299        assert_eq!(params.spend_id, "test_id");
300        assert_eq!(params.comment, Some("test comment".to_string()));
301        assert_eq!(params.disable_send_notification, Some(true));
302    }
303    #[tokio::test]
304    async fn test_transfer_params_invalid_spend_id() {
305        let client = CryptoBot::test_client();
306
307        let result = TransferParamsBuilder::default()
308            .user_id(123456789)
309            .asset(CryptoCurrencyCode::Ton)
310            .amount(Decimal::from(100))
311            .spend_id("x".repeat(65))
312            .build(&client)
313            .await;
314
315        assert!(matches!(
316            result,
317            Err(CryptoBotError::ValidationError {
318                kind: ValidationErrorKind::Range,
319                field: Some(field),
320                ..
321            }) if field == "spend_id"
322        ));
323    }
324
325    #[tokio::test]
326    async fn test_transfer_params_validate_amount() {
327        let client = CryptoBot::test_client();
328
329        let result = TransferParamsBuilder::new()
330            .user_id(123456789)
331            .asset(CryptoCurrencyCode::Ton)
332            .amount(Decimal::from(100000))
333            .spend_id("test_spend_id")
334            .build(&client)
335            .await;
336
337        assert!(matches!(
338            result,
339            Err(CryptoBotError::ValidationError {
340                kind: ValidationErrorKind::Range,
341                field: Some(field),
342                ..
343            }) if field == "amount"
344        ));
345    }
346
347    #[tokio::test]
348    async fn test_transfer_params_validate_comments() {
349        let client = CryptoBot::test_client();
350
351        let result = TransferParamsBuilder::new()
352            .user_id(123456789)
353            .asset(CryptoCurrencyCode::Ton)
354            .amount(Decimal::from(100))
355            .spend_id("test_spend_id")
356            .comment("x".repeat(1025))
357            .build(&client)
358            .await;
359
360        assert!(matches!(
361            result,
362            Err(CryptoBotError::ValidationError {
363                kind: ValidationErrorKind::Range,
364                field: Some(field),
365                ..
366            }) if field == "comment"
367        ));
368    }
369}