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