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 bic: Option<String>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub bank_code: Option<String>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub currency: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub name: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub display_name: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub owner_name: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub product_name: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub status: Option<AccountStatus>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
220 #[serde(rename = "type")]
221 pub type_: Option<AccountType>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub capabilities: Option<Vec<Capability>>,
225}
226
227#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
228#[non_exhaustive]
229#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
230pub enum AccountStatus {
231 Available,
232 Terminated,
233 Blocked,
234}
235
236#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
237#[non_exhaustive]
238#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
239pub enum AccountType {
240 Current,
243 Card,
246 Savings,
249 CallMoney,
252 TimeDeposit,
255 Loan,
258 Securities,
259 Insurance,
260 Commerce,
261 Rewards,
262}
263
264#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
265#[serde(rename_all = "camelCase")]
266#[non_exhaustive]
267#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
268pub struct Amount {
269 pub currency: String,
271
272 pub amount: Decimal,
273}
274
275impl Amount {
276 pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
277 Self {
278 amount: amount.into(),
279 currency: currency.into(),
280 }
281 }
282}
283
284#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
285#[non_exhaustive]
286pub enum Capability {
287 Balances,
288 Documents,
289 Securities,
290 Transactions,
291 SinglePayment,
292 BulkPayment,
293 StandingOrders,
294 ScheduledTransfers,
295}
296
297#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
298#[non_exhaustive]
299#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
300pub enum TransactionStatus {
301 Pending,
303 Booked,
305 Invoiced,
307 Paid,
309 Canceled,
311}
312
313#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
314#[serde(rename_all = "camelCase")]
315#[non_exhaustive]
316#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
317pub struct Fee {
318 pub amount: Amount,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 #[cfg_attr(feature = "uniffi", uniffi(default))]
324 pub kind: Option<String>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
328 #[cfg_attr(feature = "uniffi", uniffi(default))]
329 pub bic: Option<String>,
330}
331
332impl Fee {
333 pub fn new(amount: impl Into<Amount>) -> Self {
334 Self {
335 amount: amount.into(),
336 kind: None,
337 bic: None,
338 }
339 }
340}
341
342#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct Dialog<ConfirmationContext, InputContext> {
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub context: Option<DialogContext>,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub message: Option<String>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub image: Option<Image>,
364 #[serde(bound(
365 serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
366 deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
367 ))]
368 pub input: DialogInput<ConfirmationContext, InputContext>,
369}
370
371impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
372 pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
373 Self {
374 context: None,
375 message: None,
376 image: None,
377 input,
378 }
379 }
380}
381
382#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
384#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
385#[non_exhaustive]
386pub enum DialogContext {
387 Sca,
394
395 Accounts,
400
401 Redirect,
405
406 PaymentStatus,
410
411 VopConfirmation,
416
417 VopCheck,
421}
422
423#[serde_with::serde_as]
425#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
426#[serde(rename_all_fields = "camelCase")]
427pub enum DialogInput<ConfirmationContext, InputContext> {
428 Confirmation {
430 #[serde(bound(
432 serialize = "ConfirmationContext: AsRef<[u8]>",
433 deserialize = "ConfirmationContext: From<Vec<u8>>"
434 ))]
435 #[serde_as(as = "Base64")]
436 context: ConfirmationContext,
437
438 #[serde(skip_serializing_if = "Option::is_none")]
440 polling_delay_secs: Option<u32>,
441 },
442
443 Selection {
445 options: Vec<DialogOption>,
449
450 #[serde(bound(
452 serialize = "ConfirmationContext: AsRef<[u8]>",
453 deserialize = "ConfirmationContext: From<Vec<u8>>"
454 ))]
455 #[serde_as(as = "Base64")]
456 context: InputContext,
457 },
458
459 Field {
461 #[serde(rename = "type")]
463 type_: InputType,
464
465 secrecy_level: SecrecyLevel,
467
468 #[serde(skip_serializing_if = "Option::is_none")]
470 min_length: Option<u32>,
471
472 #[serde(skip_serializing_if = "Option::is_none")]
474 max_length: Option<u32>,
475 #[serde(bound(
476 serialize = "InputContext: AsRef<[u8]>",
477 deserialize = "InputContext: From<Vec<u8>>"
478 ))]
479
480 #[serde_as(as = "Base64")]
482 context: InputContext,
483 },
484}
485
486#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
488#[serde(rename_all = "camelCase")]
489#[non_exhaustive]
490#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
491pub struct DialogOption {
492 pub key: String,
493 pub label: String,
494 #[serde(skip_serializing_if = "Option::is_none")]
495 #[cfg_attr(feature = "uniffi", uniffi(default))]
496 pub explanation: Option<String>,
497}
498
499impl DialogOption {
500 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
501 Self {
502 key: key.into(),
503 label: label.into(),
504 explanation: None,
505 }
506 }
507}
508
509#[serde_with::serde_as]
511#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
512#[serde(rename_all = "camelCase")]
513#[non_exhaustive]
514#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
515pub struct Image {
516 pub mime_type: String,
517 #[serde_as(as = "Base64")]
519 pub data: Bytes,
520 #[allow(clippy::doc_markdown)]
521 #[serde_as(as = "Option<Base64>")]
532 #[serde(skip_serializing_if = "Option::is_none")]
533 #[cfg_attr(feature = "uniffi", uniffi(default))]
534 pub hhd_uc_data: Option<Bytes>,
535}
536
537impl Image {
538 pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
539 Self {
540 mime_type: mime_type.into(),
541 data: data.into(),
542 hhd_uc_data: None,
543 }
544 }
545}
546
547#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
549#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
550pub enum SecrecyLevel {
551 Plain,
553 Otp,
556 Password,
558}
559
560#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
562#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
563pub enum InputType {
564 Date,
565 Email,
566 Number,
567 Phone,
568 Text,
569}
570
571#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
572#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
573#[non_exhaustive]
574pub enum PaymentProduct {
575 SepaCreditTransfer,
577
578 SepaInstantCreditTransfer,
580
581 DefaultSepaCreditTransfer,
586
587 CrossBorderCreditTransfer,
589
590 DomesticCreditTransfer,
592
593 DomesticInstantCreditTransfer,
595}
596
597#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
598#[serde(untagged)]
599pub enum ISODateTimeOrDate {
600 Date(NaiveDate),
601 NaiveDateTime(NaiveDateTime),
602 OffsetDateTime(DateTime<FixedOffset>),
603}
604
605impl ISODateTimeOrDate {
606 pub fn date(&self, tz: &impl TimeZone) -> NaiveDate {
607 match self {
608 Self::Date(d) => *d,
609 Self::NaiveDateTime(dt) => dt.date(),
610 Self::OffsetDateTime(dt) => dt.with_timezone(tz).date_naive(),
611 }
612 }
613}
614
615#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
616#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
617pub enum ChargeBearer {
618 #[serde(rename = "DEBT")]
619 BorneByDebtor,
620 #[serde(rename = "CRED")]
621 BorneByCreditor,
622 #[serde(rename = "SHAR")]
623 Shared,
624 #[serde(rename = "SLEV")]
625 FollowingServiceLevel,
626}
627
628impl Display for ChargeBearer {
629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630 self.serialize(f)
631 }
632}
633
634#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
635#[serde(rename_all = "camelCase")]
636#[non_exhaustive]
637#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
638pub struct CreditorAddress {
639 pub town_name: String,
640 pub country: CountryCode,
641}
642
643impl CreditorAddress {
644 #[must_use]
645 pub fn new(town_name: String, country: CountryCode) -> Self {
646 Self { town_name, country }
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use std::str::FromStr;
653
654 use uuid::Uuid;
655
656 use crate::ConnectionId;
657
658 #[test]
659 fn uuid_conversion() {
660 let uuid = Uuid::new_v4();
661 assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
662 }
663
664 #[test]
665 fn str_conversion() {
666 let uuid = Uuid::new_v4();
667 let s = format!("connection-{uuid}");
668
669 assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
670 assert_eq!(
671 s,
672 ConnectionId::from_str(&uuid.to_string())
673 .unwrap()
674 .to_string()
675 );
676 }
677
678 #[test]
679 fn deserialize_prefixed_connection_id() {
680 let _ = Uuid::from(
681 &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
682 "connection-00000000-0000-0000-0000-000000000000".to_string(),
683 ))
684 .unwrap(),
685 );
686 }
687
688 #[test]
689 fn deserialize_stripped_connection_id() {
690 let _ = Uuid::from(
691 &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
692 "00000000-0000-0000-0000-000000000000".to_string(),
693 ))
694 .unwrap(),
695 );
696 }
697
698 #[test]
699 fn deserialize_invalid_connection_id() {
700 serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
701 .unwrap_err();
702 }
703
704 #[test]
705 fn deserialize_invalid_prefixed_connection_id() {
706 serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
707 "connection-0000".to_string(),
708 ))
709 .unwrap_err();
710 }
711
712 #[cfg(feature = "uniffi")]
713 fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
714 use uniffi::FfiConverter;
715
716 <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
717 val.into(),
718 ))
719 }
720
721 #[cfg(feature = "uniffi")]
722 #[test]
723 fn convert_uuid_like_connection_id() {
724 let uuid = Uuid::new_v4();
725
726 assert_eq!(
727 try_lift(uuid.to_string()).unwrap(),
728 ConnectionId::from(uuid),
729 );
730 }
731
732 #[cfg(feature = "uniffi")]
733 #[test]
734 fn convert_prefixed_connection_id() {
735 let uuid = Uuid::new_v4();
736
737 assert_eq!(
738 try_lift(format!("connection-{uuid}")).unwrap(),
739 ConnectionId::from(uuid),
740 );
741 }
742
743 #[cfg(feature = "uniffi")]
744 #[test]
745 fn convert_connection_id() {
746 let connection_id = ConnectionId::from(Uuid::new_v4());
747
748 assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
749 }
750
751 #[cfg(feature = "uniffi")]
752 #[test]
753 fn convert_invalid_string() {
754 assert_eq!(
755 try_lift(String::new()).unwrap_err().to_string(),
756 "Lifting custom type `routex_models::ConnectionId` from FFI type `alloc::string::String` failed at routex-models/src/lib.rs:22"
757 );
758 }
759}