maib_client/mia/
models.rs

1use std::sync::Arc;
2
3use rust_decimal::Decimal;
4use sha2::Digest;
5
6#[derive(Debug, serde::Serialize, serde::Deserialize)]
7pub struct ClientId(String);
8
9impl ClientId {
10    pub fn new(value: String) -> Self {
11        Self(value)
12    }
13}
14
15impl core::fmt::Display for ClientId {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        return write!(f, "ClientId([redacted])");
18    }
19}
20
21#[derive(Debug, serde::Serialize, serde::Deserialize)]
22pub struct ClientSecret(String);
23
24impl ClientSecret {
25    pub fn new(value: String) -> Self {
26        Self(value)
27    }
28}
29
30impl core::fmt::Display for ClientSecret {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        return write!(f, "ClientSecret([redacted])");
33    }
34}
35
36#[derive(Debug, serde::Serialize, serde::Deserialize)]
37pub struct AccessToken(pub(crate) String);
38
39impl AccessToken {
40    pub fn new(value: String) -> Self {
41        Self(value)
42    }
43
44    /// This might leak the value in logs!!!
45    pub fn as_str(&self) -> &str {
46        return self.0.as_str();
47    }
48}
49
50impl core::fmt::Display for AccessToken {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        return write!(f, "AccessToken([redacted])");
53    }
54}
55
56#[derive(Debug)]
57pub struct AccessTokenDuration(core::time::Duration);
58
59impl From<AccessTokenDuration> for core::time::Duration {
60    fn from(value: AccessTokenDuration) -> Self {
61        return value.0;
62    }
63}
64
65#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66pub struct QRId(String);
67
68impl QRId {
69    pub fn new(value: String) -> Self {
70        return Self(value);
71    }
72
73    pub fn as_str(&self) -> &str {
74        return self.0.as_str();
75    }
76}
77
78impl std::cmp::PartialEq<str> for QRId {
79    fn eq(&self, other: &str) -> bool {
80        return self.0.eq(other);
81    }
82}
83
84impl core::fmt::Display for QRId {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        return write!(f, "{}", self.0);
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
91pub struct Signature(String);
92
93impl Signature {
94    pub fn new(value: String) -> Self {
95        return Self(value);
96    }
97
98    pub fn as_str(&self) -> &str {
99        return self.0.as_str();
100    }
101}
102
103impl core::fmt::Display for Signature {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        return write!(f, "Signature([redacted])");
106    }
107}
108
109/// Signature key provided by MAIB.
110#[derive(Debug)]
111pub struct SignatureKey(Arc<str>);
112
113impl SignatureKey {
114    pub fn as_str(&self) -> &str {
115        return &self.0;
116    }
117}
118
119impl From<String> for SignatureKey {
120    fn from(value: String) -> Self {
121        return Self(Arc::from(value));
122    }
123}
124
125#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
126pub struct ExtensionId(String);
127
128impl ExtensionId {
129    pub fn new(value: String) -> Self {
130        return Self(value);
131    }
132
133    pub fn as_str(&self) -> &str {
134        return self.0.as_str();
135    }
136}
137
138impl core::fmt::Display for ExtensionId {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        return write!(f, "{}", self.0);
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
145pub struct PaymentId(String);
146
147impl PaymentId {
148    pub fn new(value: String) -> Self {
149        return Self(value);
150    }
151
152    pub fn as_str(&self) -> &str {
153        return self.0.as_str();
154    }
155}
156
157impl core::fmt::Display for PaymentId {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        return write!(f, "{}", self.0);
160    }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164#[serde(rename_all = "PascalCase")]
165pub enum PaymentType {
166    Fixed,
167    Controlled,
168    Free,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
172#[serde(rename_all = "PascalCase")]
173pub enum PaymentStatus {
174    Executed,
175    Refunded,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
179pub enum TokenType {
180    Bearer,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
184#[serde(rename_all = "PascalCase")]
185pub enum QRType {
186    /// QR payment that can be paid
187    /// more than once.
188    Static,
189
190    /// QR payment that can be paid once.
191    Dynamic,
192
193    /// QR payment can pe paid more than once.
194    ///
195    /// This also allows to modify amount and expiration date
196    /// while is considere valid payment.
197    Hybrid,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
201pub enum Currency {
202    MDL,
203}
204
205impl Currency {
206    pub fn code(self) -> &'static str {
207        match self {
208            Currency::MDL => "MDL",
209        }
210    }
211
212    pub fn minor_currency_unit(self) -> i32 {
213        match self {
214            Currency::MDL => 100,
215        }
216    }
217}
218
219impl core::fmt::Display for Currency {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        return write!(f, "{}", self.code());
222    }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
226#[serde(rename_all = "PascalCase")]
227pub enum QRStatus {
228    Active,
229    Inactive,
230    Expired,
231    Paid,
232    Cancelled,
233}
234
235impl core::fmt::Display for QRStatus {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        match *self {
238            QRStatus::Active => write!(f, "Active"),
239            QRStatus::Inactive => write!(f, "Inactive"),
240            QRStatus::Expired => write!(f, "Expired"),
241            QRStatus::Paid => write!(f, "Paid"),
242            QRStatus::Cancelled => write!(f, "Cancelled"),
243        }
244    }
245}
246
247#[derive(Debug, serde::Deserialize, serde::Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct Notification {
250    pub(crate) amount: Decimal,
251    pub(crate) commission: Decimal,
252    pub(crate) currency: Currency,
253    pub(crate) executed_at: String,
254    pub(crate) extension_id: ExtensionId,
255    pub(crate) order_id: Option<String>,
256    pub(crate) pay_id: PaymentId,
257    pub(crate) payer_iban: String,
258    pub(crate) payer_name: String,
259    pub(crate) qr_id: QRId,
260    pub(crate) qr_status: QRStatus,
261    pub(crate) reference_id: String,
262    pub(crate) terminal_id: Option<String>,
263}
264
265impl Notification {
266    pub fn pay_id(&self) -> &PaymentId {
267        &self.pay_id
268    }
269}
270
271#[derive(Debug)]
272pub struct ValidSignatureNotification(pub Notification);
273
274#[derive(Debug, serde::Deserialize, serde::Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct NotificationPayload {
277    pub(crate) result: Notification,
278    pub(crate) signature: Signature,
279}
280
281impl NotificationPayload {
282    pub(crate) fn build_signature(&self, key: SignatureKey) -> Signature {
283        use base64::prelude::*;
284
285        let n = &self.result;
286        let mut this_signature = format!(
287            "{}:{}:{}:{}:{}",
288            n.amount, n.commission, n.currency, n.executed_at, n.extension_id
289        );
290
291        if let Some(ref order_id) = n.order_id {
292            this_signature = format!("{this_signature}:{order_id}");
293        }
294
295        this_signature = format!(
296            "{this_signature}:{}:{}:{}:{}:{}:{}",
297            n.pay_id, n.payer_iban, n.payer_name, n.qr_id, n.qr_status, n.reference_id
298        );
299
300        if let Some(ref terminal_id) = n.terminal_id {
301            this_signature = format!("{this_signature}:{terminal_id}");
302        }
303
304        this_signature = format!("{this_signature}:{}", key.0);
305
306        let sig_sha256 = sha2::Sha256::digest(&this_signature);
307        let encoded = hex::encode(sig_sha256);
308        let signature = Signature::new(BASE64_STANDARD.encode(encoded));
309
310        return signature;
311    }
312
313    /// Attempt to validate signature with provided key.
314    ///
315    /// If it is not valid, this will return [None].
316    pub fn validate_signature(self, key: SignatureKey) -> Option<ValidSignatureNotification> {
317        let signature = self.build_signature(key);
318
319        if signature.eq(&self.signature) {
320            return Some(ValidSignatureNotification(self.result));
321        }
322
323        return None;
324    }
325
326    pub fn notification(&self) -> &Notification {
327        &self.result
328    }
329}
330
331pub mod request {
332    use rust_decimal::Decimal;
333
334    use super::{ClientId, ClientSecret, Currency, PaymentType, QRType};
335
336    #[derive(Debug, serde::Serialize)]
337    #[serde(rename_all = "camelCase")]
338    pub struct GetAccessToken<'a> {
339        pub client_id: &'a ClientId,
340        pub client_secret: &'a ClientSecret,
341    }
342
343    #[derive(Debug, serde::Serialize)]
344    #[serde(rename_all = "camelCase")]
345    pub struct CreateQR<'a> {
346        pub r#type: super::QRType,
347        /// Date time when Dynamic QR expires.
348        ///
349        /// Must be a valid ISO 8601-1:2019 value.
350        pub expires_at: Option<&'a str>,
351        pub amount_type: super::PaymentType,
352
353        pub amount: rust_decimal::Decimal,
354        pub amount_min: Option<rust_decimal::Decimal>,
355        pub amount_max: Option<rust_decimal::Decimal>,
356
357        pub currency: super::Currency,
358        pub description: String,
359        pub order_id: Option<&'a str>,
360        pub callback_url: String,
361        pub redirect_url: String,
362        pub terminal_id: Option<String>,
363    }
364
365    impl<'a> CreateQR<'a> {
366        pub fn new_dynamic_with_fixed_amount(
367            amount: Decimal,
368            expires_at: &'a str,
369            description: String,
370            callback_url: String,
371            redirect_url: String,
372        ) -> Self {
373            return CreateQR {
374                r#type: QRType::Dynamic,
375                expires_at: Some(expires_at),
376                amount_type: PaymentType::Fixed,
377                amount,
378                amount_min: None,
379                amount_max: None,
380                currency: Currency::MDL,
381                description,
382                order_id: None,
383                callback_url,
384                redirect_url,
385                terminal_id: None,
386            };
387        }
388    }
389
390    #[derive(Debug, serde::Serialize)]
391    #[serde(rename_all = "camelCase")]
392    pub struct CancelQR {
393        pub reason: String,
394    }
395
396    #[derive(Debug, serde::Serialize)]
397    #[serde(rename_all = "camelCase")]
398    pub struct RefundPayment {
399        pub reason: String,
400    }
401}
402
403pub mod response {
404    use chrono::{DateTime, Utc};
405    use rust_decimal::Decimal;
406
407    use super::{Currency, ExtensionId, PaymentId, PaymentStatus, QRId};
408
409    #[derive(Debug, serde::Deserialize)]
410    pub struct ApiResponse<R> {
411        pub(crate) result: Option<R>,
412        pub(crate) errors: Option<Vec<crate::error::ApiError>>,
413    }
414
415    impl<R> From<ApiResponse<R>> for core::result::Result<R, crate::error::Error> {
416        fn from(value: ApiResponse<R>) -> Self {
417            if let Some(value) = value.result {
418                return Ok(value);
419            }
420
421            if let Some(value) = value.errors {
422                return Err(crate::error::Error::Api(value));
423            }
424
425            panic!();
426        }
427    }
428
429    #[derive(Debug, serde::Deserialize)]
430    #[serde(rename_all = "camelCase")]
431    pub struct AuthToken {
432        access_token: super::AccessToken,
433        expires_in: u64,
434        token_type: super::TokenType,
435    }
436
437    impl AuthToken {
438        /// Access token lifetime in seconds.
439        pub fn expires_in(&self) -> super::AccessTokenDuration {
440            return super::AccessTokenDuration(core::time::Duration::from_secs(self.expires_in));
441        }
442
443        pub fn access_token(&self) -> &super::AccessToken {
444            &self.access_token
445        }
446
447        pub fn take_access_token(self) -> super::AccessToken {
448            self.access_token
449        }
450
451        pub fn token_type(&self) -> super::TokenType {
452            self.token_type
453        }
454    }
455
456    #[derive(Debug, serde::Deserialize)]
457    #[serde(rename_all = "camelCase")]
458    pub struct CreateQRResponse {
459        pub qr_id: super::QRId,
460        pub order_id: Option<String>,
461        pub r#type: super::QRType,
462        pub url: String,
463        pub expires_at: String,
464    }
465
466    #[derive(Debug, serde::Deserialize)]
467    #[serde(rename_all = "camelCase")]
468    pub struct GetQRDetails {
469        pub qr_id: super::QRId,
470        pub order_id: Option<String>,
471        pub status: super::QRStatus,
472        pub r#type: super::QRType,
473        pub url: String,
474        pub amount_type: super::PaymentType,
475        pub currency: super::Currency,
476        pub amount: Decimal,
477        pub amount_min: Option<Decimal>,
478        pub amount_max: Option<Decimal>,
479        pub description: String,
480        pub callback_url: String,
481        pub redirect_url: String,
482        pub terminal_id: String,
483        pub created_at: DateTime<Utc>,
484        pub updated_at: DateTime<Utc>,
485        pub expires_at: DateTime<Utc>,
486    }
487
488    #[derive(Debug, serde::Deserialize)]
489    #[serde(rename_all = "camelCase")]
490    pub struct CancelQR {
491        pub qr_id: super::QRId,
492        pub status: super::QRStatus,
493    }
494
495    #[derive(Debug, serde::Deserialize)]
496    #[serde(rename_all = "camelCase")]
497    pub struct PaymentDetails {
498        pub pay_id: PaymentId,
499        pub reference_id: String,
500        pub qr_id: QRId,
501        pub extension_id: Option<ExtensionId>,
502        pub order_id: Option<String>,
503        pub amount: Decimal,
504        pub commission: Decimal,
505        pub currency: Currency,
506        pub description: String,
507        pub payer_name: String,
508        pub payer_iban: String,
509        pub status: PaymentStatus,
510        pub executed_at: String,
511        pub refunded_at: Option<String>,
512        pub terminal_id: Option<String>,
513    }
514
515    #[derive(Debug, serde::Deserialize)]
516    #[serde(rename_all = "camelCase")]
517    pub struct RefundPayment {
518        pub pay_id: PaymentId,
519        pub status: PaymentStatus,
520    }
521}