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 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#[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 Static,
203
204 Dynamic,
206
207 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 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 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 pub struct QRExpiresAt<'a>(&'a str);
406
407 #[derive(Debug, serde::Serialize)]
408 #[serde(rename_all = "camelCase")]
409 pub struct CancelQR {
410 pub reason: String,
411 }
412
413 #[derive(Debug, serde::Serialize)]
414 #[serde(rename_all = "camelCase")]
415 pub struct RefundPayment {
416 pub reason: String,
417 }
418}
419
420pub mod response {
421 use chrono::{DateTime, Utc};
422 use rust_decimal::Decimal;
423
424 use super::{Currency, ExtensionId, PaymentId, PaymentStatus, QRId};
425
426 #[derive(Debug, serde::Deserialize)]
427 pub struct ApiResponse<R> {
428 pub(crate) result: Option<R>,
429 pub(crate) errors: Option<Vec<crate::error::ApiError>>,
430 }
431
432 impl<R> From<ApiResponse<R>> for core::result::Result<R, crate::error::Error> {
433 fn from(value: ApiResponse<R>) -> Self {
434 if let Some(value) = value.result {
435 return Ok(value);
436 }
437
438 if let Some(value) = value.errors {
439 return Err(crate::error::Error::Api(value));
440 }
441
442 panic!();
443 }
444 }
445
446 #[derive(Debug, serde::Deserialize)]
447 #[serde(rename_all = "camelCase")]
448 pub struct AuthToken {
449 access_token: super::AccessToken,
450 expires_in: u64,
451 token_type: super::TokenType,
452 }
453
454 impl AuthToken {
455 pub fn expires_in(&self) -> super::AccessTokenDuration {
457 return super::AccessTokenDuration(core::time::Duration::from_secs(self.expires_in));
458 }
459
460 pub fn access_token(&self) -> &super::AccessToken {
461 &self.access_token
462 }
463
464 pub fn take_access_token(self) -> super::AccessToken {
465 self.access_token
466 }
467
468 pub fn token_type(&self) -> super::TokenType {
469 self.token_type
470 }
471 }
472
473 #[derive(Debug, serde::Deserialize)]
474 #[serde(rename_all = "camelCase")]
475 pub struct CreateQRResponse {
476 pub qr_id: super::QRId,
477 pub order_id: Option<String>,
478 pub r#type: super::QRType,
479 pub url: String,
480 pub expires_at: String,
481 }
482
483 #[derive(Debug, serde::Deserialize)]
484 #[serde(rename_all = "camelCase")]
485 pub struct GetQRDetails {
486 pub qr_id: super::QRId,
487 pub order_id: Option<String>,
488 pub status: super::QRStatus,
489 pub r#type: super::QRType,
490 pub url: String,
491 pub amount_type: super::PaymentType,
492 pub currency: super::Currency,
493 pub amount: Decimal,
494 pub amount_min: Option<Decimal>,
495 pub amount_max: Option<Decimal>,
496 pub description: String,
497 pub callback_url: String,
498 pub redirect_url: String,
499 pub terminal_id: String,
500 pub created_at: DateTime<Utc>,
501 pub updated_at: DateTime<Utc>,
502 pub expires_at: DateTime<Utc>,
503 }
504
505 #[derive(Debug, serde::Deserialize)]
506 #[serde(rename_all = "camelCase")]
507 pub struct CancelQR {
508 pub qr_id: super::QRId,
509 pub status: super::QRStatus,
510 }
511
512 #[derive(Debug, serde::Deserialize)]
513 #[serde(rename_all = "camelCase")]
514 pub struct PaymentDetails {
515 pub pay_id: PaymentId,
516 pub reference_id: String,
517 pub qr_id: QRId,
518 pub extension_id: Option<ExtensionId>,
519 pub order_id: Option<String>,
520 pub amount: Decimal,
521 pub commission: Decimal,
522 pub currency: Currency,
523 pub description: String,
524 pub payer_name: String,
525 pub payer_iban: String,
526 pub status: PaymentStatus,
527 pub executed_at: String,
528 pub refunded_at: Option<String>,
529 pub terminal_id: Option<String>,
530 }
531
532 #[derive(Debug, serde::Deserialize)]
533 #[serde(rename_all = "camelCase")]
534 pub struct RefundPayment {
535 pub pay_id: PaymentId,
536 pub status: PaymentStatus,
537 }
538}