Skip to main content

routex_models/
lib.rs

1use std::{fmt::Display, str::FromStr};
2
3use bytes::Bytes;
4use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
5use isocountry::CountryCode;
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use serde_with::base64::Base64;
9use uuid::Uuid;
10
11#[cfg(feature = "uniffi")]
12uniffi::setup_scaffolding!();
13
14#[cfg(feature = "uniffi")]
15uniffi::custom_type!(Bytes, Vec<u8>, {
16    remote,
17    try_lift: |val| Ok(val.into()),
18    lower: |obj| obj.into(),
19});
20
21#[cfg(feature = "uniffi")]
22uniffi::custom_type!(ConnectionId, String, {
23    try_lift: |val| Ok(val.parse()?),
24    lower: |obj| obj.to_string(),
25});
26
27#[cfg(feature = "uniffi")]
28uniffi::custom_type!(Decimal, String, {
29    remote,
30    try_lift: |val| Ok(val.parse()?),
31    lower: |obj| obj.to_string(),
32});
33
34#[cfg(feature = "uniffi")]
35uniffi::custom_type!(CountryCode, String, {
36    remote,
37    try_lift: |val| Ok(Self::for_alpha2(&val)?),
38    lower: |obj| obj.alpha2().to_string(),
39});
40
41/// Identifier of a specific service connection.
42///
43/// Supports serialization, comparison, and hashing for use e.g. in a `HashMap`.
44#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
45pub struct ConnectionId(String);
46
47impl Display for ConnectionId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        self.0.fmt(f)
50    }
51}
52
53impl<'de> Deserialize<'de> for ConnectionId {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: serde::Deserializer<'de>,
57    {
58        String::deserialize(deserializer)?
59            .parse()
60            .map_err(serde::de::Error::custom)
61    }
62}
63
64impl From<Uuid> for ConnectionId {
65    fn from(value: Uuid) -> Self {
66        Self(format!("connection-{value}"))
67    }
68}
69
70impl From<&ConnectionId> for Uuid {
71    fn from(value: &ConnectionId) -> Self {
72        (value.0[11..]).parse().unwrap()
73    }
74}
75
76impl FromStr for ConnectionId {
77    type Err = uuid::Error;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
80        s.trim_start_matches("connection-")
81            .parse::<Uuid>()
82            .map(Into::into)
83    }
84}
85
86/// Requirements for user identifier and password.
87#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
88#[non_exhaustive]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90#[serde(rename_all = "camelCase")]
91pub struct CredentialsModel {
92    /// A full set of credentials may be provided to support fully embedded authentication (including scraped redirects).
93    pub full: bool,
94
95    /// Only a user identifier without a password may be provided.
96    /// This is typically the case for decoupled authentication where the user e.g. authorizes access in a mobile application.
97    /// Note that if password-less authentication fails (e.g. as no device for decoupled authentication is set up for the user and
98    /// a redirect is not supported), an error is returned and the transaction has to get restarted with a full set of credentials.
99    pub user_id: bool,
100
101    /// Credentials are not required. The user will provide them to the service provider during a redirect.
102    pub none: bool,
103}
104
105#[cfg(feature = "kitx")]
106impl CredentialsModel {
107    pub const FULL: CredentialsModel = CredentialsModel {
108        full: true,
109        user_id: false,
110        none: false,
111    };
112
113    pub const USER_ID: CredentialsModel = CredentialsModel {
114        full: false,
115        user_id: true,
116        none: false,
117    };
118
119    pub const NONE: CredentialsModel = CredentialsModel {
120        full: false,
121        user_id: false,
122        none: true,
123    };
124
125    pub const OPT_USER: CredentialsModel = CredentialsModel {
126        full: false,
127        user_id: true,
128        none: true,
129    };
130
131    pub const OPT_FULL: CredentialsModel = CredentialsModel {
132        full: true,
133        user_id: false,
134        none: true,
135    };
136}
137
138#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
139#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
140#[non_exhaustive]
141pub enum PaymentErrorCode {
142    LimitExceeded,
143    InsufficientFunds,
144}
145
146#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
147#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
148#[non_exhaustive]
149pub enum ProviderErrorCode {
150    Maintenance,
151}
152
153#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
154#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
155#[non_exhaustive]
156pub enum ServiceBlockedCode {
157    /// Something is not set up for the user, e.g., there are no TAN methods.
158    MissingSetup,
159    /// User attention is required via another channel. Typically the user needs to log into the Online Banking.
160    ActionRequired,
161}
162
163#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
164#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
165#[non_exhaustive]
166pub enum UnsupportedProductReason {
167    /// The amount is not allowed for the payment product.
168    Limit,
169    /// The recipient is not capable to receive the payment product.
170    Recipient,
171    /// Scheduled payments are not supported.
172    Scheduled,
173}
174
175#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
176#[serde(rename_all = "camelCase")]
177#[non_exhaustive]
178pub struct Account {
179    /// ISO 20022 IBAN2007Identifier.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub iban: Option<String>,
182
183    /// Account number that is not an IBAN, e.g. ISO 20022 BBANIdentifier or primary account number (PAN) of a card account.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub number: Option<String>,
186
187    /// Masked payment card number
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub masked_pan: Option<String>,
190
191    /// ISO 20022 BICFIIdentifier.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub bic: Option<String>,
194
195    /// National bank code.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub bank_code: Option<String>,
198
199    /// ISO 4217 Alpha 3 currency code.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub currency: Option<String>,
202
203    /// Name of account, assigned by ASPSP.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub name: Option<String>,
206
207    /// Display name of account, assigned by PSU.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub display_name: Option<String>,
210
211    /// Legal account owner.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub owner_name: Option<String>,
214
215    /// Product name.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub product_name: Option<String>,
218
219    /// Account Status.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub status: Option<AccountStatus>,
222
223    #[serde(skip_serializing_if = "Option::is_none")]
224    #[serde(rename = "type")]
225    pub type_: Option<AccountType>,
226
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub capabilities: Option<Vec<Capability>>,
229}
230
231#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
232#[non_exhaustive]
233#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
234pub enum AccountStatus {
235    Available,
236    Terminated,
237    Blocked,
238}
239
240#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
241#[non_exhaustive]
242#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
243pub enum AccountType {
244    /// Account used to post debits and credits.
245    /// ISO 20022 ExternalCashAccountType1Code CACC.
246    Current,
247    /// Account used for credit card payments.
248    /// ISO 20022 ExternalCashAccountType1Code CARD.
249    Card,
250    /// Account used for savings.
251    /// ISO 20022 ExternalCashAccountType1Code SVGS.
252    Savings,
253    /// Account used for call money.
254    /// No dedicated ISO 20022 code (falls into SVGS).
255    CallMoney,
256    /// Account used for time deposits.
257    /// No dedicated ISO 20022 code (falls into SVGS).
258    TimeDeposit,
259    /// Account used for loans.
260    /// ISO 20022 ExternalCashAccountType1Code LOAN.
261    Loan,
262    Securities,
263    Insurance,
264    Commerce,
265    Rewards,
266}
267
268#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
269#[serde(rename_all = "camelCase")]
270#[non_exhaustive]
271#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
272pub struct Amount {
273    /// ISO 4217 Alpha 3 currency code.
274    pub currency: String,
275
276    pub amount: Decimal,
277}
278
279impl Amount {
280    pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
281        Self {
282            amount: amount.into(),
283            currency: currency.into(),
284        }
285    }
286}
287
288#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
289#[non_exhaustive]
290pub enum Capability {
291    Balances,
292    Documents,
293    Securities,
294    Transactions,
295    SinglePayment,
296    BulkPayment,
297    StandingOrders,
298    ScheduledTransfers,
299}
300
301#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
302#[non_exhaustive]
303#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
304pub enum TransactionStatus {
305    /// The transaction is expected / planned.
306    Pending,
307    /// The transaction is booked to the account. This is typically the final state for most accounts.
308    Booked,
309    /// The credit card transaction is booked and invoiced but not yet paid.
310    Invoiced,
311    /// The credit card transaction is paid. This is typically the final state for card accounts.
312    Paid,
313    /// The transaction has been canceled in some way.
314    Canceled,
315}
316
317#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
318#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
319pub enum ExecutionRule {
320    /// Following working day.
321    Following,
322    /// Following working day unless it would fall into the next month.
323    FollowingWithinAMonth,
324    /// Preceding working day.
325    Preceding,
326    /// Preceding working day unless it would fall into the previous month.
327    PrecedingWithinAMonth,
328}
329
330#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
331#[serde(rename_all = "camelCase")]
332#[non_exhaustive]
333#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
334pub enum Frequency {
335    /// Frequency in days.
336    Days(u32),
337    /// Frequency in months.
338    Months(u32),
339    /// Specific months that the standing order is scheduled for.
340    VariableMonths(Vec<Month>),
341}
342
343#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
344#[serde(try_from = "u8", into = "u8")]
345#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
346pub enum Month {
347    January = 1,
348    February = 2,
349    March = 3,
350    April = 4,
351    May = 5,
352    June = 6,
353    July = 7,
354    August = 8,
355    September = 9,
356    October = 10,
357    November = 11,
358    December = 12,
359}
360
361impl TryFrom<u8> for Month {
362    type Error = &'static str;
363
364    fn try_from(number: u8) -> Result<Self, Self::Error> {
365        Ok(match number {
366            1 => Self::January,
367            2 => Self::February,
368            3 => Self::March,
369            4 => Self::April,
370            5 => Self::May,
371            6 => Self::June,
372            7 => Self::July,
373            8 => Self::August,
374            9 => Self::September,
375            10 => Self::October,
376            11 => Self::November,
377            12 => Self::December,
378            _ => return Err("month must be between 1 and 12"),
379        })
380    }
381}
382
383impl From<Month> for u8 {
384    fn from(month: Month) -> Self {
385        month as u8
386    }
387}
388
389#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
390#[serde(rename_all = "camelCase")]
391#[non_exhaustive]
392#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
393pub struct Fee {
394    /// Amount of the fee.
395    pub amount: Amount,
396
397    /// ISO 20022 `ExternalChargeType1Code` for the fee.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    #[cfg_attr(feature = "uniffi", uniffi(default))]
400    pub kind: Option<String>,
401
402    /// ISO 20022 `BICFIIdentifier` of the agent to whom the charges are due.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    #[cfg_attr(feature = "uniffi", uniffi(default))]
405    pub bic: Option<String>,
406}
407
408impl Fee {
409    pub fn new(amount: impl Into<Amount>) -> Self {
410        Self {
411            amount: amount.into(),
412            kind: None,
413            bic: None,
414        }
415    }
416}
417
418/// User dialog.
419///
420/// This is meant to be displayed as a dialog in some User Interface and consists of:
421///
422/// - A way to cancel the dialog (typically an X symbol and / or a "Cancel" button).
423/// - The display part:
424///   - The `message`.
425///   - An optional `image`.
426/// - The interactive part defined by `input`.
427///
428/// The [`DialogInput`] contains a context for continuing the
429/// process at the service that issued the dialog object.
430#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
431#[serde(rename_all = "camelCase")]
432#[non_exhaustive]
433pub struct Dialog<ConfirmationContext, InputContext> {
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub context: Option<DialogContext>,
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub message: Option<String>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub image: Option<Image>,
440    #[serde(bound(
441        serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
442        deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
443    ))]
444    pub input: DialogInput<ConfirmationContext, InputContext>,
445}
446
447impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
448    pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
449        Self {
450            context: None,
451            message: None,
452            image: None,
453            input,
454        }
455    }
456}
457
458/// Context of a user dialog.
459#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
460#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
461#[non_exhaustive]
462pub enum DialogContext {
463    /// SCA or TAN process.
464    ///
465    /// There are multiple cases, distinguishable by the [`DialogInput`]:
466    /// - [`DialogInput::Confirmation`]: Decoupled process (e.g. confirmation in a SCA app).
467    /// - [`DialogInput::Selection`]: TAN method selection.
468    /// - [`DialogInput::Field`]: TAN entry.
469    Sca,
470
471    /// Account selection.
472    ///
473    /// A [`DialogInput::Selection`] gets returned with this context when an account has to be selected.
474    /// Note that there might be just a single option that may be chosen automatically without user interaction.
475    Accounts,
476
477    /// Pending redirect confirmation.
478    ///
479    /// A [`DialogInput::Confirmation`] gets returned with this context when a redirect got confirmed but no result is known yet.
480    Redirect,
481
482    /// Pending SCT Inst payment.
483    ///
484    /// A [`DialogInput::Confirmation`] gets returned with this context when an SCT Inst payment has been initialized and not reached the final status yet.
485    PaymentStatus,
486
487    /// Verification of Payee confirmation.
488    ///
489    /// A [`DialogInput::Confirmation`] gets returned with this context when an explicit confirmation of the creditor is required due to a name mismatch.
490    /// 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.
491    VopConfirmation,
492
493    /// Pending Verification of Payee check.
494    ///
495    /// A [`DialogInput::Confirmation`] gets returned with this context when a Verification of Payee check is still pending.
496    VopCheck,
497}
498
499/// Data defining the interactive part of a user dialog.
500#[serde_with::serde_as]
501#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
502#[serde(rename_all_fields = "camelCase")]
503pub enum DialogInput<ConfirmationContext, InputContext> {
504    /// Just a primary action to confirm the dialog.
505    Confirmation {
506        /// Context object that can be used to confirm the dialog.
507        #[serde(bound(
508            serialize = "ConfirmationContext: AsRef<[u8]>",
509            deserialize = "ConfirmationContext: From<Vec<u8>>"
510        ))]
511        #[serde_as(as = "Base64")]
512        context: ConfirmationContext,
513
514        /// If polling is acceptable, a delay in seconds is specified for which the client has to wait before automatically confirming.
515        #[serde(skip_serializing_if = "Option::is_none")]
516        polling_delay_secs: Option<u32>,
517    },
518
519    /// A selection of options the user can choose from.
520    Selection {
521        /// Options are meant to be rendered e.g. as radio buttons where the user must select exactly
522        /// one to for a confirmation button to get enabled. Another example for an implementation is
523        /// one button per option that immediately confirms the selection.
524        options: Vec<DialogOption>,
525
526        /// Context object that can be used to respond to the dialog.
527        #[serde(bound(
528            serialize = "ConfirmationContext: AsRef<[u8]>",
529            deserialize = "ConfirmationContext: From<Vec<u8>>"
530        ))]
531        #[serde_as(as = "Base64")]
532        context: InputContext,
533    },
534
535    /// An input field.
536    Field {
537        /// Type that may be used for showing hints or dedicated keyboard layouts and for applying input restrictions or validation.
538        #[serde(rename = "type")]
539        type_: InputType,
540
541        /// Indicates if the input should be masked.
542        secrecy_level: SecrecyLevel,
543
544        /// Minimal length to allow.
545        #[serde(skip_serializing_if = "Option::is_none")]
546        min_length: Option<u32>,
547
548        /// Maximum length to allow.
549        #[serde(skip_serializing_if = "Option::is_none")]
550        max_length: Option<u32>,
551        #[serde(bound(
552            serialize = "InputContext: AsRef<[u8]>",
553            deserialize = "InputContext: From<Vec<u8>>"
554        ))]
555
556        /// Context object that can be used to respond to the dialog.
557        #[serde_as(as = "Base64")]
558        context: InputContext,
559    },
560}
561
562/// A dialog option.
563#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
564#[serde(rename_all = "camelCase")]
565#[non_exhaustive]
566#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
567pub struct DialogOption {
568    pub key: String,
569    pub label: String,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    #[cfg_attr(feature = "uniffi", uniffi(default))]
572    pub explanation: Option<String>,
573}
574
575impl DialogOption {
576    pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
577        Self {
578            key: key.into(),
579            label: label.into(),
580            explanation: None,
581        }
582    }
583}
584
585/// Image data for a dialog.
586#[serde_with::serde_as]
587#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
588#[serde(rename_all = "camelCase")]
589#[non_exhaustive]
590#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
591pub struct Image {
592    pub mime_type: String,
593    /// Binary data in the format defined by `mime_type`.
594    #[serde_as(as = "Base64")]
595    pub data: Bytes,
596    #[allow(clippy::doc_markdown)]
597    /// HHD_UC data block
598    ///
599    /// In cases where the ASPSP provides HHD_UC data for optical coupling with a HandHeld-Device
600    /// for the generation of an OTP, especially for an HHD_OPT animated graphic, the raw HHD_UC
601    /// data stream is provided here.
602    ///
603    /// The publicly available document "HandHeld-Device (HHD) for the generation of an OTP HHD
604    /// enhancement for optical interfaces" describes how to implement the animated graphic for
605    /// HHD_OPT in section C. `data` provides a pre-rendered animated GIF
606    /// to be presented with a width of 62.5 mm.
607    #[serde_as(as = "Option<Base64>")]
608    #[serde(skip_serializing_if = "Option::is_none")]
609    #[cfg_attr(feature = "uniffi", uniffi(default))]
610    pub hhd_uc_data: Option<Bytes>,
611}
612
613impl Image {
614    pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
615        Self {
616            mime_type: mime_type.into(),
617            data: data.into(),
618            hhd_uc_data: None,
619        }
620    }
621}
622
623/// Level of secrecy for an input field.
624#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
625#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
626pub enum SecrecyLevel {
627    /// The data is not a secret.
628    Plain,
629    /// The data is a one-time password. This can usually be treated as
630    /// no secret but the implementer might still choose to mask the input.
631    Otp,
632    /// The data is a secret password. Input must be masked.
633    Password,
634}
635
636/// Type of an input field.
637#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
638#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
639pub enum InputType {
640    Date,
641    Email,
642    Number,
643    Phone,
644    Text,
645}
646
647#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
648#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
649#[non_exhaustive]
650pub enum PaymentProduct {
651    /// SEPA Credit Transfer (SCT) in EUR
652    SepaCreditTransfer,
653
654    /// SEPA Instant Credit Transfer (SCT Inst) in EUR
655    SepaInstantCreditTransfer,
656
657    /// Default SEPA Credit Transfer in EUR
658    ///
659    /// Tries SCT Inst with a fallback to SCT if this is supported.
660    /// Otherwise, SCT is used.
661    DefaultSepaCreditTransfer,
662
663    /// International credit transfer outside of SEPA (typically SWIFT)
664    CrossBorderCreditTransfer,
665
666    /// Domestic credit transfer in the domestic, non-EUR currency
667    DomesticCreditTransfer,
668
669    /// Instant domestic credit transfer in the domestic, non-EUR currency
670    DomesticInstantCreditTransfer,
671}
672
673#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
674#[serde(untagged)]
675pub enum ISODateTimeOrDate {
676    Date(NaiveDate),
677    NaiveDateTime(NaiveDateTime),
678    OffsetDateTime(DateTime<FixedOffset>),
679}
680
681impl ISODateTimeOrDate {
682    pub fn date(&self, tz: &impl TimeZone) -> NaiveDate {
683        match self {
684            Self::Date(d) => *d,
685            Self::NaiveDateTime(dt) => dt.date(),
686            Self::OffsetDateTime(dt) => dt.with_timezone(tz).date_naive(),
687        }
688    }
689}
690
691#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
692#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
693pub enum ChargeBearer {
694    #[serde(rename = "DEBT")]
695    BorneByDebtor,
696    #[serde(rename = "CRED")]
697    BorneByCreditor,
698    #[serde(rename = "SHAR")]
699    Shared,
700    #[serde(rename = "SLEV")]
701    FollowingServiceLevel,
702}
703
704impl Display for ChargeBearer {
705    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
706        self.serialize(f)
707    }
708}
709
710#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
711#[serde(rename_all = "camelCase")]
712#[non_exhaustive]
713#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
714pub struct CreditorAddress {
715    pub town_name: String,
716    pub country: CountryCode,
717}
718
719impl CreditorAddress {
720    #[must_use]
721    pub fn new(town_name: String, country: CountryCode) -> Self {
722        Self { town_name, country }
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use std::str::FromStr;
729
730    use serde_json::json;
731    use uuid::Uuid;
732
733    use crate::{ConnectionId, Frequency, Month};
734
735    #[test]
736    fn frequency_months_as_numbers() {
737        let frequency =
738            Frequency::VariableMonths(vec![Month::January, Month::April, Month::October]);
739        let value = json!({"variableMonths": [1, 4, 10]});
740
741        assert_eq!(serde_json::to_value(&frequency).unwrap(), value);
742        assert_eq!(
743            serde_json::from_value::<Frequency>(value).unwrap(),
744            frequency
745        );
746        assert!(serde_json::from_value::<Frequency>(json!({"variableMonths": [0]})).is_err());
747        assert!(serde_json::from_value::<Frequency>(json!({"variableMonths": [13]})).is_err());
748    }
749
750    #[test]
751    fn uuid_conversion() {
752        let uuid = Uuid::new_v4();
753        assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
754    }
755
756    #[test]
757    fn str_conversion() {
758        let uuid = Uuid::new_v4();
759        let s = format!("connection-{uuid}");
760
761        assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
762        assert_eq!(
763            s,
764            ConnectionId::from_str(&uuid.to_string())
765                .unwrap()
766                .to_string()
767        );
768    }
769
770    #[test]
771    fn deserialize_prefixed_connection_id() {
772        let _ = Uuid::from(
773            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
774                "connection-00000000-0000-0000-0000-000000000000".to_string(),
775            ))
776            .unwrap(),
777        );
778    }
779
780    #[test]
781    fn deserialize_stripped_connection_id() {
782        let _ = Uuid::from(
783            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
784                "00000000-0000-0000-0000-000000000000".to_string(),
785            ))
786            .unwrap(),
787        );
788    }
789
790    #[test]
791    fn deserialize_invalid_connection_id() {
792        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
793            .unwrap_err();
794    }
795
796    #[test]
797    fn deserialize_invalid_prefixed_connection_id() {
798        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
799            "connection-0000".to_string(),
800        ))
801        .unwrap_err();
802    }
803
804    #[cfg(feature = "uniffi")]
805    fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
806        use uniffi::FfiConverter;
807
808        <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
809            val.into(),
810        ))
811    }
812
813    #[cfg(feature = "uniffi")]
814    #[test]
815    fn convert_uuid_like_connection_id() {
816        let uuid = Uuid::new_v4();
817
818        assert_eq!(
819            try_lift(uuid.to_string()).unwrap(),
820            ConnectionId::from(uuid),
821        );
822    }
823
824    #[cfg(feature = "uniffi")]
825    #[test]
826    fn convert_prefixed_connection_id() {
827        let uuid = Uuid::new_v4();
828
829        assert_eq!(
830            try_lift(format!("connection-{uuid}")).unwrap(),
831            ConnectionId::from(uuid),
832        );
833    }
834
835    #[cfg(feature = "uniffi")]
836    #[test]
837    fn convert_connection_id() {
838        let connection_id = ConnectionId::from(Uuid::new_v4());
839
840        assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
841    }
842
843    #[cfg(feature = "uniffi")]
844    #[test]
845    fn convert_invalid_string() {
846        assert_eq!(
847            try_lift(String::new()).unwrap_err().to_string(),
848            "Lifting custom type `routex_models::ConnectionId` from FFI type `alloc::string::String` failed at routex-models/src/lib.rs:22"
849        );
850    }
851}