Skip to main content

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