crypto_pay_api/models/check/
builder.rs

1use std::marker::PhantomData;
2
3use rust_decimal::Decimal;
4use rust_decimal_macros::dec;
5
6use crate::{
7    api::ExchangeRateAPI,
8    client::CryptoBot,
9    error::{CryptoBotError, CryptoBotResult, ValidationErrorKind},
10    models::{CryptoCurrencyCode, Missing, Set},
11    validation::{validate_amount, validate_count, ContextValidate, FieldValidate, ValidationContext},
12};
13
14use super::{CheckStatus, CreateCheckParams, GetChecksParams};
15
16/* #region CreateCheckParamsBuilder */
17
18pub struct CreateCheckParamsBuilder<A = Missing, M = Missing> {
19    asset: CryptoCurrencyCode,
20    amount: Decimal,
21    pin_to_user_id: Option<u64>,
22    pin_to_username: Option<String>,
23    _state: PhantomData<(A, M)>,
24}
25
26impl Default for CreateCheckParamsBuilder {
27    fn default() -> Self {
28        Self {
29            asset: CryptoCurrencyCode::Ton,
30            amount: dec!(0),
31            pin_to_user_id: None,
32            pin_to_username: None,
33            _state: PhantomData,
34        }
35    }
36}
37
38impl CreateCheckParamsBuilder {
39    /// Create a new `CreateCheckParamsBuilder` with default values.
40    pub fn new() -> Self {
41        Self::default()
42    }
43}
44
45impl<M> CreateCheckParamsBuilder<Missing, M> {
46    /// Set the asset for the check.
47    /// Cryptocurrency alphabetic code.
48    pub fn asset(mut self, asset: CryptoCurrencyCode) -> CreateCheckParamsBuilder<Set, M> {
49        self.asset = asset;
50        self.transform()
51    }
52}
53
54impl<A> CreateCheckParamsBuilder<A, Missing> {
55    /// Set the amount for the check.
56    /// Amount of the check in float.
57    pub fn amount(mut self, amount: Decimal) -> CreateCheckParamsBuilder<A, Set> {
58        self.amount = amount;
59        self.transform()
60    }
61}
62
63impl<A, M> CreateCheckParamsBuilder<A, M> {
64    /// Set the user ID to pin the check to.
65    /// Optional. ID of the user who will be able to activate the check.
66    pub fn pin_to_user_id(mut self, pin_to_user_id: u64) -> Self {
67        self.pin_to_user_id = Some(pin_to_user_id);
68        self.transform()
69    }
70
71    /// Set the username to pin the check to.
72    /// Optional. A user with the specified username will be able to activate the check.
73    pub fn pin_to_username(mut self, pin_to_username: &str) -> Self {
74        self.pin_to_username = Some(pin_to_username.to_string());
75        self
76    }
77
78    fn transform<A2, M2>(self) -> CreateCheckParamsBuilder<A2, M2> {
79        CreateCheckParamsBuilder {
80            asset: self.asset,
81            amount: self.amount,
82            pin_to_user_id: self.pin_to_user_id,
83            pin_to_username: self.pin_to_username,
84            _state: PhantomData,
85        }
86    }
87}
88
89impl FieldValidate for CreateCheckParamsBuilder<Set, Set> {
90    fn validate(&self) -> CryptoBotResult<()> {
91        if self.amount < Decimal::ZERO {
92            return Err(CryptoBotError::ValidationError {
93                kind: ValidationErrorKind::Range,
94                message: "Amount must be greater than 0".to_string(),
95                field: Some("amount".to_string()),
96            });
97        }
98        Ok(())
99    }
100}
101
102#[async_trait::async_trait]
103impl ContextValidate for CreateCheckParamsBuilder<Set, Set> {
104    async fn validate_with_context(&self, ctx: &ValidationContext) -> CryptoBotResult<()> {
105        validate_amount(&self.amount, &self.asset, ctx).await
106    }
107}
108
109impl CreateCheckParamsBuilder<Set, Set> {
110    pub async fn build(self, client: &CryptoBot) -> CryptoBotResult<CreateCheckParams> {
111        self.validate()?;
112
113        let exchange_rates = client.get_exchange_rates().await?;
114
115        let ctx = ValidationContext { exchange_rates };
116
117        self.validate_with_context(&ctx).await?;
118
119        Ok(CreateCheckParams {
120            asset: self.asset,
121            amount: self.amount,
122            pin_to_user_id: self.pin_to_user_id,
123            pin_to_username: self.pin_to_username,
124        })
125    }
126}
127
128/* #endregion */
129
130/* #region GetChecksParamsBuilder */
131
132#[derive(Debug, Default)]
133pub struct GetChecksParamsBuilder {
134    asset: Option<CryptoCurrencyCode>,
135    check_ids: Option<Vec<u64>>,
136    status: Option<CheckStatus>,
137    offset: Option<u32>,
138    count: Option<u16>,
139}
140
141impl GetChecksParamsBuilder {
142    /// Create a new `GetChecksParamsBuilder` with default values.
143    pub fn new() -> Self {
144        Self::default()
145    }
146
147    /// Set the asset for the checks.
148    /// Optional. Defaults to all currencies.
149    pub fn asset(mut self, asset: CryptoCurrencyCode) -> Self {
150        self.asset = Some(asset);
151        self
152    }
153
154    /// Set the check IDs for the checks.
155    pub fn check_ids(mut self, check_ids: Vec<u64>) -> Self {
156        self.check_ids = Some(check_ids);
157        self
158    }
159
160    /// Set the status for the checks.
161    /// Optional. Status of check to be returned.
162    /// Defaults to all statuses.
163    pub fn status(mut self, status: CheckStatus) -> Self {
164        self.status = Some(status);
165        self
166    }
167
168    /// Set the offset for the checks.
169    /// Optional. Offset needed to return a specific subset of check.
170    /// Defaults to 0.
171    pub fn offset(mut self, offset: u32) -> Self {
172        self.offset = Some(offset);
173        self
174    }
175
176    /// Set the count for the checks.
177    /// Optional. Number of check to be returned. Values between 1-1000 are accepted.
178    /// Defaults to 100.
179    pub fn count(mut self, count: u16) -> Self {
180        self.count = Some(count);
181        self
182    }
183}
184
185impl FieldValidate for GetChecksParamsBuilder {
186    fn validate(&self) -> CryptoBotResult<()> {
187        if let Some(count) = &self.count {
188            validate_count(*count)?;
189        }
190        Ok(())
191    }
192}
193
194impl GetChecksParamsBuilder {
195    pub fn build(self) -> CryptoBotResult<GetChecksParams> {
196        self.validate()?;
197
198        Ok(GetChecksParams {
199            asset: self.asset,
200            check_ids: self.check_ids,
201            status: self.status,
202            offset: self.offset,
203            count: self.count,
204        })
205    }
206}
207/* #endregion */
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_get_checks_params_builder() {
215        let params = GetChecksParamsBuilder::new()
216            .count(5)
217            .asset(CryptoCurrencyCode::Ton)
218            .status(CheckStatus::Activated)
219            .offset(10)
220            .build()
221            .unwrap();
222        assert_eq!(params.count, Some(5));
223        assert_eq!(params.asset, Some(CryptoCurrencyCode::Ton));
224        assert_eq!(params.status, Some(CheckStatus::Activated));
225        assert_eq!(params.offset, Some(10));
226    }
227
228    #[test]
229    fn test_get_checks_params_builder_invalid_count() {
230        let params = GetChecksParamsBuilder::new().count(1001).build();
231        assert!(matches!(
232            params,
233            Err(CryptoBotError::ValidationError {
234                kind: ValidationErrorKind::Range,
235                field: Some(field),
236                ..
237            }) if field == "count"
238        ));
239    }
240
241    #[tokio::test]
242    async fn test_create_check_params_builder() {
243        let client = CryptoBot::test_client();
244
245        let params = CreateCheckParamsBuilder::new()
246            .asset(CryptoCurrencyCode::Ton)
247            .amount(Decimal::from(100))
248            .pin_to_user_id(123456789)
249            .pin_to_username("test_username")
250            .build(&client)
251            .await
252            .unwrap();
253
254        assert_eq!(params.asset, CryptoCurrencyCode::Ton);
255        assert_eq!(params.amount, Decimal::from(100));
256        assert_eq!(params.pin_to_user_id, Some(123456789));
257        assert_eq!(params.pin_to_username, Some("test_username".to_string()));
258    }
259
260    #[tokio::test]
261    async fn test_create_check_params_builder_invalid_amount() {
262        let client = CryptoBot::test_client();
263        let result = CreateCheckParamsBuilder::new()
264            .asset(CryptoCurrencyCode::Ton)
265            .amount(Decimal::from(-100))
266            .build(&client)
267            .await;
268
269        assert!(matches!(
270            result,
271            Err(CryptoBotError::ValidationError {
272                kind: ValidationErrorKind::Range,
273                field: Some(field),
274                ..
275            }) if field == "amount"
276        ));
277    }
278
279    #[tokio::test]
280    async fn test_create_check_params_builder_missing_exchange_rate() {
281        let client = CryptoBot::test_client();
282        let result = CreateCheckParamsBuilder::new()
283            .asset(CryptoCurrencyCode::Btc)
284            .amount(Decimal::from(100))
285            .build(&client)
286            .await;
287
288        assert!(matches!(
289            result,
290            Err(CryptoBotError::ValidationError {
291                kind: ValidationErrorKind::Missing,
292                field: Some(field),
293                ..
294            }) if field == "exchange_rate"
295        ));
296    }
297}