routex_models/
lib.rs

1use std::{fmt::Display, str::FromStr};
2
3use bytes::Bytes;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_with::base64::Base64;
7use uuid::Uuid;
8
9#[cfg(feature = "uniffi")]
10uniffi::setup_scaffolding!();
11
12#[cfg(feature = "uniffi")]
13uniffi::custom_type!(Bytes, Vec<u8>, {
14    remote,
15    try_lift: |val| Ok(val.into()),
16    lower: |obj| obj.into(),
17});
18
19#[cfg(feature = "uniffi")]
20uniffi::custom_type!(ConnectionId, String, {
21    try_lift: |val| Ok(val.parse()?),
22    lower: |obj| obj.to_string(),
23});
24
25#[cfg(feature = "uniffi")]
26uniffi::custom_type!(Decimal, String, {
27    remote,
28    try_lift: |val| Ok(val.parse()?),
29    lower: |obj| obj.to_string(),
30});
31
32/// Identifier of a specific service connection.
33///
34/// Supports serialization, comparison, and hashing for use e.g. in a `HashMap`.
35#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
36pub struct ConnectionId(String);
37
38impl Display for ConnectionId {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        self.0.fmt(f)
41    }
42}
43
44impl<'de> Deserialize<'de> for ConnectionId {
45    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46    where
47        D: serde::Deserializer<'de>,
48    {
49        String::deserialize(deserializer)?
50            .parse()
51            .map_err(serde::de::Error::custom)
52    }
53}
54
55impl From<Uuid> for ConnectionId {
56    fn from(value: Uuid) -> Self {
57        Self(format!("connection-{value}"))
58    }
59}
60
61impl From<&ConnectionId> for Uuid {
62    fn from(value: &ConnectionId) -> Self {
63        (value.0[11..]).parse().unwrap()
64    }
65}
66
67impl FromStr for ConnectionId {
68    type Err = uuid::Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        s.trim_start_matches("connection-")
72            .parse::<Uuid>()
73            .map(Into::into)
74    }
75}
76
77/// Requirements for user identifier and password.
78#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
79#[non_exhaustive]
80#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
81#[serde(rename_all = "camelCase")]
82pub struct CredentialsModel {
83    /// A full set of credentials may be provided to support fully embedded authentication (including scraped redirects).
84    pub full: bool,
85
86    /// Only a user identifier without a password may be provided.
87    /// This is typically the case for decoupled authentication where the user e.g. authorizes access in a mobile application.
88    /// Note that if password-less authentication fails (e.g. as no device for decoupled authentication is set up for the user and
89    /// a redirect is not supported), an error is returned and the transaction has to get restarted with a full set of credentials.
90    pub user_id: bool,
91
92    /// Credentials are not required. The user will provide them to the service provider during a redirect.
93    pub none: bool,
94}
95
96#[cfg(feature = "kitx")]
97impl CredentialsModel {
98    pub const FULL: CredentialsModel = CredentialsModel {
99        full: true,
100        user_id: false,
101        none: false,
102    };
103
104    pub const USER_ID: CredentialsModel = CredentialsModel {
105        full: false,
106        user_id: true,
107        none: false,
108    };
109
110    pub const NONE: CredentialsModel = CredentialsModel {
111        full: false,
112        user_id: false,
113        none: true,
114    };
115
116    pub const OPT_USER: CredentialsModel = CredentialsModel {
117        full: false,
118        user_id: true,
119        none: true,
120    };
121
122    pub const OPT_FULL: CredentialsModel = CredentialsModel {
123        full: true,
124        user_id: false,
125        none: true,
126    };
127}
128
129#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
130#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
131#[non_exhaustive]
132pub enum PaymentErrorCode {
133    LimitExceeded,
134    InsufficientFunds,
135}
136
137#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
138#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
139#[non_exhaustive]
140pub enum ProviderErrorCode {
141    Maintenance,
142}
143
144#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
145#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
146#[non_exhaustive]
147pub enum ServiceBlockedCode {
148    /// Something is not set up for the user, e.g., there are no TAN methods.
149    MissingSetup,
150    /// User attention is required via another channel. Typically the user needs to log into the Online Banking.
151    ActionRequired,
152}
153
154#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
155#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
156#[non_exhaustive]
157pub enum UnsupportedProductReason {
158    /// The amount is not allowed for the payment product.
159    Limit,
160    /// The recipient is not capable to receive the payment product.
161    Recipient,
162}
163
164#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
165#[serde(rename_all = "camelCase")]
166#[non_exhaustive]
167pub struct Account {
168    /// ISO 20022 IBAN2007Identifier.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub iban: Option<String>,
171
172    /// Account number that is not an IBAN, e.g. ISO 20022 BBANIdentifier or primary account number (PAN) of a card account.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub number: Option<String>,
175
176    /// ISO 20022 BICFIIdentifier.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub bic: Option<String>,
179
180    /// National bank code.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub bank_code: Option<String>,
183
184    /// ISO 4217 Alpha 3 currency code.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub currency: Option<String>,
187
188    /// Name of account, assigned by ASPSP.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub name: Option<String>,
191
192    /// Display name of account, assigned by PSU.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub display_name: Option<String>,
195
196    /// Legal account owner.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub owner_name: Option<String>,
199
200    /// Product name.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub product_name: Option<String>,
203
204    /// Account Status.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub status: Option<AccountStatus>,
207
208    #[serde(skip_serializing_if = "Option::is_none")]
209    #[serde(rename = "type")]
210    pub type_: Option<AccountType>,
211
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub capabilities: Option<Vec<Capability>>,
214}
215
216#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
217#[non_exhaustive]
218#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
219pub enum AccountStatus {
220    Available,
221    Terminated,
222    Blocked,
223}
224
225#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
226#[non_exhaustive]
227#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
228pub enum AccountType {
229    /// Account used to post debits and credits.
230    /// ISO 20022 ExternalCashAccountType1Code CACC.
231    Current,
232    /// Account used for credit card payments.
233    /// ISO 20022 ExternalCashAccountType1Code CARD.
234    Card,
235    /// Account used for savings.
236    /// ISO 20022 ExternalCashAccountType1Code SVGS.
237    Savings,
238    /// Account used for call money.
239    /// No dedicated ISO 20022 code (falls into SVGS).
240    CallMoney,
241    /// Account used for time deposits.
242    /// No dedicated ISO 20022 code (falls into SVGS).
243    TimeDeposit,
244    /// Account used for loans.
245    /// ISO 20022 ExternalCashAccountType1Code LOAN.
246    Loan,
247    Securities,
248    Insurance,
249    Commerce,
250    Rewards,
251}
252
253#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
254#[serde(rename_all = "camelCase")]
255#[non_exhaustive]
256#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
257pub struct Amount {
258    /// ISO 4217 Alpha 3 currency code.
259    pub currency: String,
260
261    pub amount: Decimal,
262}
263
264impl Amount {
265    pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
266        Self {
267            amount: amount.into(),
268            currency: currency.into(),
269        }
270    }
271}
272
273#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
274#[non_exhaustive]
275pub enum Capability {
276    Balances,
277    Documents,
278    Securities,
279    Transactions,
280    SinglePayment,
281    BulkPayment,
282    StandingOrders,
283    ScheduledTransfers,
284}
285
286#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
287#[non_exhaustive]
288#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
289pub enum TransactionStatus {
290    /// The transaction is expected / planned.
291    Pending,
292    /// The transaction is booked to the account. This is typically the final state for most accounts.
293    Booked,
294    /// The credit card transaction is booked and invoiced but not yet paid.
295    Invoiced,
296    /// The credit card transaction is paid. This is typically the final state for card accounts.
297    Paid,
298    /// The transaction has been canceled in some way.
299    Canceled,
300}
301
302#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
303#[serde(rename_all = "camelCase")]
304#[non_exhaustive]
305#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
306pub struct Fee {
307    /// Amount of the fee.
308    pub amount: Amount,
309
310    /// ISO 20022 `ExternalChargeType1Code` for the fee.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
313    pub kind: Option<String>,
314
315    /// ISO 20022 `BICFIIdentifier` of the agent to whom the charges are due.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
318    pub bic: Option<String>,
319}
320
321impl Fee {
322    pub fn new(amount: impl Into<Amount>) -> Self {
323        Self {
324            amount: amount.into(),
325            kind: None,
326            bic: None,
327        }
328    }
329}
330
331/// User dialog.
332///
333/// This is meant to be displayed as a dialog in some User Interface and consists of:
334///
335/// - A way to cancel the dialog (typically an X symbol and / or a "Cancel" button).
336/// - The display part:
337///   - The `message`.
338///   - An optional `image`.
339/// - The interactive part defined by `input`.
340///
341/// The [`DialogInput`] contains a context for continuing the
342/// process at the service that issued the dialog object.
343#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
344#[serde(rename_all = "camelCase")]
345#[non_exhaustive]
346pub struct Dialog<ConfirmationContext, InputContext> {
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub context: Option<DialogContext>,
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub message: Option<String>,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub image: Option<Image>,
353    #[serde(bound(
354        serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
355        deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
356    ))]
357    pub input: DialogInput<ConfirmationContext, InputContext>,
358}
359
360impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
361    pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
362        Self {
363            context: None,
364            message: None,
365            image: None,
366            input,
367        }
368    }
369}
370
371/// Context of a user dialog.
372#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
373#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
374#[non_exhaustive]
375pub enum DialogContext {
376    /// SCA or TAN process.
377    ///
378    /// There are multiple cases, distinguishable by the [`DialogInput`]:
379    /// - [`DialogInput::Confirmation`]: Decoupled process (e.g. confirmation in a SCA app).
380    /// - [`DialogInput::Selection`]: TAN method selection.
381    /// - [`DialogInput::Field`]: TAN entry.
382    Sca,
383
384    /// Account selection.
385    ///
386    /// A [`DialogInput::Selection`] gets returned with this context when an account has to be selected.
387    /// Note that there might be just a single option that may be chosen automatically without user interaction.
388    Accounts,
389
390    /// Pending redirect confirmation.
391    ///
392    /// A [`DialogInput::Confirmation`] gets returned with this context when a redirect got confirmed but no result is known yet.
393    Redirect,
394
395    /// Pending SCT Inst payment.
396    ///
397    /// A [`DialogInput::Confirmation`] gets returned with this context when an SCT Inst payment has been initialized and not reached the final status yet.
398    PaymentStatus,
399
400    /// Verification of Payee confirmation.
401    ///
402    /// A [`DialogInput::Confirmation`] gets returned with this context when an explicit confirmation of the creditor is required due to a name mismatch.
403    /// Note that this confirmation has legal implications, releasing the bank from liabilities in case of the transfer to an unintended receiver due to incorrect creditor data.
404    VopConfirmation,
405
406    /// Pending Verification of Payee check.
407    ///
408    /// A [`DialogInput::Confirmation`] gets returned with this context when a Verification of Payee check for a bulk payment is still pending.
409    VopCheck,
410}
411
412/// Data defining the interactive part of a user dialog.
413#[serde_with::serde_as]
414#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
415#[serde(rename_all_fields = "camelCase")]
416pub enum DialogInput<ConfirmationContext, InputContext> {
417    /// Just a primary action to confirm the dialog.
418    Confirmation {
419        /// Context object that can be used to confirm the dialog.
420        #[serde(bound(
421            serialize = "ConfirmationContext: AsRef<[u8]>",
422            deserialize = "ConfirmationContext: From<Vec<u8>>"
423        ))]
424        #[serde_as(as = "Base64")]
425        context: ConfirmationContext,
426
427        /// If polling is acceptable, a delay in seconds is specified for which the client has to wait before automatically confirming.
428        #[serde(skip_serializing_if = "Option::is_none")]
429        polling_delay_secs: Option<u32>,
430    },
431
432    /// A selection of options the user can choose from.
433    Selection {
434        /// Options are meant to be rendered e.g. as radio buttons where the user must select exactly
435        /// one to for a confirmation button to get enabled. Another example for an implementation is
436        /// one button per option that immediately confirms the selection.
437        options: Vec<DialogOption>,
438
439        /// Context object that can be used to respond to the dialog.
440        #[serde(bound(
441            serialize = "ConfirmationContext: AsRef<[u8]>",
442            deserialize = "ConfirmationContext: From<Vec<u8>>"
443        ))]
444        #[serde_as(as = "Base64")]
445        context: InputContext,
446    },
447
448    /// An input field.
449    Field {
450        /// Type that may be used for showing hints or dedicated keyboard layouts and for applying input restrictions or validation.
451        #[serde(rename = "type")]
452        type_: InputType,
453
454        /// Indicates if the input should be masked.
455        secrecy_level: SecrecyLevel,
456
457        /// Minimal length to allow.
458        #[serde(skip_serializing_if = "Option::is_none")]
459        min_length: Option<u32>,
460
461        /// Maximum length to allow.
462        #[serde(skip_serializing_if = "Option::is_none")]
463        max_length: Option<u32>,
464        #[serde(bound(
465            serialize = "InputContext: AsRef<[u8]>",
466            deserialize = "InputContext: From<Vec<u8>>"
467        ))]
468
469        /// Context object that can be used to respond to the dialog.
470        #[serde_as(as = "Base64")]
471        context: InputContext,
472    },
473}
474
475/// A dialog option.
476#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
477#[serde(rename_all = "camelCase")]
478#[non_exhaustive]
479#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
480pub struct DialogOption {
481    pub key: String,
482    pub label: String,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
485    pub explanation: Option<String>,
486}
487
488impl DialogOption {
489    pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
490        Self {
491            key: key.into(),
492            label: label.into(),
493            explanation: None,
494        }
495    }
496}
497
498/// Image data for a dialog.
499#[serde_with::serde_as]
500#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
501#[serde(rename_all = "camelCase")]
502#[non_exhaustive]
503#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
504pub struct Image {
505    pub mime_type: String,
506    /// Binary data in the format defined by `mime_type`.
507    #[serde_as(as = "Base64")]
508    pub data: Bytes,
509    #[allow(clippy::doc_markdown)]
510    /// HHD_UC data block
511    ///
512    /// In cases where the ASPSP provides HHD_UC data for optical coupling with a HandHeld-Device
513    /// for the generation of an OTP, especially for an HHD_OPT animated graphic, the raw HHD_UC
514    /// data stream is provided here.
515    ///
516    /// The publicly available document "HandHeld-Device (HHD) for the generation of an OTP HHD
517    /// enhancement for optical interfaces" describes how to implement the animated graphic for
518    /// HHD_OPT in section C. `data` provides a pre-rendered animated GIF
519    /// to be presented with a width of 62.5 mm.
520    #[serde_as(as = "Option<Base64>")]
521    #[serde(skip_serializing_if = "Option::is_none")]
522    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
523    pub hhd_uc_data: Option<Bytes>,
524}
525
526impl Image {
527    pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
528        Self {
529            mime_type: mime_type.into(),
530            data: data.into(),
531            hhd_uc_data: None,
532        }
533    }
534}
535
536/// Level of secrecy for an input field.
537#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
538#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
539pub enum SecrecyLevel {
540    /// The data is not a secret.
541    Plain,
542    /// The data is a one-time password. This can usually be treated as
543    /// no secret but the implementer might still choose to mask the input.
544    Otp,
545    /// The data is a secret password. Input must be masked.
546    Password,
547}
548
549/// Type of an input field.
550#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
551#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
552pub enum InputType {
553    Date,
554    Email,
555    Number,
556    Phone,
557    Text,
558}
559
560#[cfg(test)]
561mod tests {
562    use std::str::FromStr;
563
564    use uuid::Uuid;
565
566    use crate::ConnectionId;
567
568    #[test]
569    fn uuid_conversion() {
570        let uuid = Uuid::new_v4();
571        assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
572    }
573
574    #[test]
575    fn str_conversion() {
576        let uuid = Uuid::new_v4();
577        let s = format!("connection-{uuid}");
578
579        assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
580        assert_eq!(
581            s,
582            ConnectionId::from_str(&uuid.to_string())
583                .unwrap()
584                .to_string()
585        );
586    }
587
588    #[test]
589    fn deserialize_prefixed_connection_id() {
590        let _ = Uuid::from(
591            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
592                "connection-00000000-0000-0000-0000-000000000000".to_string(),
593            ))
594            .unwrap(),
595        );
596    }
597
598    #[test]
599    fn deserialize_stripped_connection_id() {
600        let _ = Uuid::from(
601            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
602                "00000000-0000-0000-0000-000000000000".to_string(),
603            ))
604            .unwrap(),
605        );
606    }
607
608    #[test]
609    fn deserialize_invalid_connection_id() {
610        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
611            .unwrap_err();
612    }
613
614    #[test]
615    fn deserialize_invalid_prefixed_connection_id() {
616        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
617            "connection-0000".to_string(),
618        ))
619        .unwrap_err();
620    }
621
622    #[cfg(feature = "uniffi")]
623    fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
624        use uniffi::FfiConverter;
625
626        <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
627            val.into(),
628        ))
629    }
630
631    #[cfg(feature = "uniffi")]
632    #[test]
633    fn convert_uuid_like_connection_id() {
634        let uuid = Uuid::new_v4();
635
636        assert_eq!(
637            try_lift(uuid.to_string()).unwrap(),
638            ConnectionId::from(uuid),
639        );
640    }
641
642    #[cfg(feature = "uniffi")]
643    #[test]
644    fn convert_prefixed_connection_id() {
645        let uuid = Uuid::new_v4();
646
647        assert_eq!(
648            try_lift(format!("connection-{uuid}")).unwrap(),
649            ConnectionId::from(uuid),
650        );
651    }
652
653    #[cfg(feature = "uniffi")]
654    #[test]
655    fn convert_connection_id() {
656        let connection_id = ConnectionId::from(Uuid::new_v4());
657
658        assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
659    }
660
661    #[cfg(feature = "uniffi")]
662    #[test]
663    fn convert_invalid_string() {
664        assert_eq!(
665            try_lift(String::new()).unwrap_err().to_string(),
666            "invalid length: expected length 32 for simple format, found 0"
667        );
668    }
669}