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#[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#[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 pub full: bool,
94
95 pub user_id: bool,
100
101 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 MissingSetup,
159 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 Limit,
169 Recipient,
171 Scheduled,
173}
174
175#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
176#[serde(rename_all = "camelCase")]
177#[non_exhaustive]
178pub struct Account {
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub iban: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub number: Option<String>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub masked_pan: Option<String>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub bic: Option<String>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub bank_code: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub currency: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub name: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub display_name: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub owner_name: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub product_name: Option<String>,
218
219 #[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 Current,
247 Card,
250 Savings,
253 CallMoney,
256 TimeDeposit,
259 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 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 Pending,
307 Booked,
309 Invoiced,
311 Paid,
313 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,
322 FollowingWithinAMonth,
324 Preceding,
326 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 Days(u32),
337 Months(u32),
339 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 pub amount: Amount,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
399 #[cfg_attr(feature = "uniffi", uniffi(default))]
400 pub kind: Option<String>,
401
402 #[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#[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#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
460#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
461#[non_exhaustive]
462pub enum DialogContext {
463 Sca,
470
471 Accounts,
476
477 Redirect,
481
482 PaymentStatus,
486
487 VopConfirmation,
492
493 VopCheck,
497}
498
499#[serde_with::serde_as]
501#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
502#[serde(rename_all_fields = "camelCase")]
503pub enum DialogInput<ConfirmationContext, InputContext> {
504 Confirmation {
506 #[serde(bound(
508 serialize = "ConfirmationContext: AsRef<[u8]>",
509 deserialize = "ConfirmationContext: From<Vec<u8>>"
510 ))]
511 #[serde_as(as = "Base64")]
512 context: ConfirmationContext,
513
514 #[serde(skip_serializing_if = "Option::is_none")]
516 polling_delay_secs: Option<u32>,
517 },
518
519 Selection {
521 options: Vec<DialogOption>,
525
526 #[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 Field {
537 #[serde(rename = "type")]
539 type_: InputType,
540
541 secrecy_level: SecrecyLevel,
543
544 #[serde(skip_serializing_if = "Option::is_none")]
546 min_length: Option<u32>,
547
548 #[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 #[serde_as(as = "Base64")]
558 context: InputContext,
559 },
560}
561
562#[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#[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 #[serde_as(as = "Base64")]
595 pub data: Bytes,
596 #[allow(clippy::doc_markdown)]
597 #[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#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
625#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
626pub enum SecrecyLevel {
627 Plain,
629 Otp,
632 Password,
634}
635
636#[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 SepaCreditTransfer,
653
654 SepaInstantCreditTransfer,
656
657 DefaultSepaCreditTransfer,
662
663 CrossBorderCreditTransfer,
665
666 DomesticCreditTransfer,
668
669 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}