Skip to main content

routex_api/
lib.rs

1#[cfg(feature = "uniffi")]
2use chrono::NaiveDate;
3use chrono::Utc;
4use heck::ToKebabCase;
5#[cfg(feature = "uniffi")]
6use isocountry::CountryCode;
7use jsonwebtoken::dangerous::insecure_decode;
8use jsonwebtoken::{EncodingKey, Header, decode_header, encode};
9pub use routex_models::{
10    AccountStatus, AccountType, Amount, ConnectionId, CredentialsModel, DialogContext,
11    DialogOption, Image, InputType, PaymentErrorCode, ProviderErrorCode, SecrecyLevel,
12    ServiceBlockedCode, UnsupportedProductReason,
13};
14#[cfg(feature = "uniffi")]
15use rust_decimal::Decimal;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use serde_with::base64::Base64;
19use std::fmt;
20use std::marker::PhantomData;
21use std::str::FromStr;
22use url::Url;
23
24#[cfg(feature = "uniffi")]
25uniffi::setup_scaffolding!();
26
27#[cfg(feature = "uniffi")]
28uniffi::custom_type!(ConnectionData, Vec<u8>, {
29    try_lift: |val| Ok(val.into()),
30    lower: |obj| obj.into(),
31});
32
33#[cfg(feature = "uniffi")]
34uniffi::custom_type!(Session, Vec<u8>, {
35    try_lift: |val| Ok(val.into()),
36    lower: |obj| obj.into(),
37});
38
39#[cfg(feature = "uniffi")]
40uniffi::custom_type!(CountryCode, String, {
41    remote,
42    try_lift: |val| Ok(Self::for_alpha2(&val)?),
43    lower: |obj| obj.alpha2().to_string(),
44});
45
46#[cfg(feature = "uniffi")]
47uniffi::use_remote_type!(routex_models::Decimal);
48
49#[cfg(feature = "uniffi")]
50uniffi::custom_type!(NaiveDate, String, {
51    remote,
52    try_lift: |val| Ok(val.parse()?),
53    lower: |obj| obj.to_string(),
54});
55
56type DateTime = chrono::DateTime<Utc>;
57
58#[cfg(feature = "uniffi")]
59uniffi::custom_type!(DateTime, String, {
60    remote,
61    try_lift: |val| Ok(val.parse()?),
62    lower: |obj| obj.to_string(),
63});
64
65pub type Dialog<S> = routex_models::Dialog<ConfirmationContext<S>, InputContext<S>>;
66pub type DialogInput<S> = routex_models::DialogInput<ConfirmationContext<S>, InputContext<S>>;
67
68/// Representation of an API path
69///
70/// Use [`to_url`](Path::to_url) to build requests on clients,
71/// [`to_string`](ToString::to_string) to get for service routes.
72/// Note that the [`Display`](fmt::Display) implementation does NOT escape
73/// the segments, so that e.g. axum / matchit captures can be used.
74#[must_use]
75pub struct Path(Vec<String>);
76
77impl Path {
78    fn new<I>(segments: I) -> Self
79    where
80        I: IntoIterator,
81        I::Item: ToString,
82    {
83        Self(segments.into_iter().map(|s| s.to_string()).collect())
84    }
85
86    #[must_use]
87    /// Build a URL from a base and the path.
88    ///
89    /// # Panics
90    ///
91    /// Panics if the given URL is cannot-be-a-base.
92    pub fn to_url(&self, base: &Url) -> Url {
93        let mut url = base.clone();
94        url.path_segments_mut()
95            .expect("cannot be base")
96            .extend(&self.0);
97        url
98    }
99}
100
101impl fmt::Display for Path {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        for segment in &self.0 {
104            f.write_str("/")?;
105            f.write_str(segment)?;
106        }
107
108        Ok(())
109    }
110}
111
112pub mod headers {
113    use http::HeaderName;
114
115    pub static SESSION_ID: HeaderName = HeaderName::from_static("yaxi-session-id");
116
117    pub static TICKET: HeaderName = HeaderName::from_static("yaxi-ticket");
118
119    pub static TICKET_ID: HeaderName = HeaderName::from_static("yaxi-ticket-id");
120
121    pub static REDIRECT_URI: HeaderName = HeaderName::from_static("yaxi-redirect-uri");
122
123    pub static TRACE_ID: HeaderName = HeaderName::from_static("yaxi-trace-id");
124}
125
126pub const CURRENT_MEDIA_TYPE: &str = "application/vnd.yaxi.v5";
127
128pub mod keys {
129    use super::Path;
130    use clerk_report::PublishedVersionEntry;
131    use serde::{Deserialize, Serialize};
132    use serde_with::base64::Base64;
133
134    /// Run a TEE-attested key settlement
135    ///
136    /// Method: POST<br>
137    /// Request: [`Request`]<br>
138    /// Response: [`Response`]
139    pub fn settlement_path() -> Path {
140        Path::new(["key-settlement"])
141    }
142
143    #[serde_with::serde_as]
144    #[derive(Serialize, Deserialize, Clone, Debug)]
145    #[serde(rename_all = "camelCase")]
146    pub struct Request {
147        /// The client's Curve25519 public key (Montgomery form, binary)
148        #[serde_as(as = "Base64")]
149        pub public_key: [u8; 32],
150    }
151
152    /// The contents of a key settlement's sealed box
153    #[serde_with::serde_as]
154    #[derive(Serialize, Deserialize)]
155    #[serde(rename_all = "camelCase")]
156    pub struct SettlementBoxMessage {
157        #[serde_as(as = "Base64")]
158        pub public_key: [u8; 32],
159        #[serde_as(as = "Base64")]
160        pub session_id: [u8; 32],
161    }
162
163    /// Response to a request for a TEE-attested key settlement.
164    #[serde_with::serde_as]
165    #[derive(Serialize, Deserialize, Clone, Debug)]
166    #[serde(rename_all = "camelCase")]
167    pub struct Response {
168        /// TEE attestation report which also authenticates `chacha_box`
169        #[serde_as(as = "Base64")]
170        pub attestation_report: Vec<u8>,
171        /// Versioned Chip Endorsement Key, AMD SEV Key and AMD Root Key as chain of PEM-encoded certificates
172        pub vcek: String,
173        /// Sealed box containing [`SettlementBoxMessage`]
174        #[serde_as(as = "Base64")]
175        pub chacha_box: Vec<u8>,
176        /// The TEE's version, signed by YAXI. The contained measurement needs to match the
177        /// attestation report's measurement.
178        pub system_version: PublishedVersionEntry,
179    }
180}
181
182pub mod traces {
183    use super::Path;
184
185    /// Retrieve trace data
186    ///
187    /// Method: GET<br>
188    /// Response: Trace data string
189    pub fn path(trace_id: &str) -> Path {
190        Path::new(["traces", trace_id])
191    }
192}
193
194pub mod redirects {
195    use super::Path;
196    use serde::{Deserialize, Serialize};
197    use url::Url;
198
199    /// Register redirects
200    ///
201    /// Method: POST<br>
202    /// Request: [`Request`]<br>
203    /// Response: [`Response`]
204    pub fn path() -> Path {
205        Path::new(["redirects"])
206    }
207
208    #[derive(Serialize, Deserialize, Clone, Debug)]
209    #[serde(rename_all = "camelCase")]
210    pub struct Request {
211        pub handle: String,
212        pub redirect_uri: String,
213    }
214
215    #[derive(Serialize, Deserialize, Clone, Debug)]
216    #[serde(rename_all = "camelCase")]
217    pub struct Response {
218        pub redirect_url: Url,
219    }
220}
221
222pub mod info {
223    use super::Path;
224    pub use isocountry::CountryCode;
225    use routex_models::{ConnectionId, CredentialsModel};
226    use serde::{Deserialize, Serialize};
227    #[cfg(feature = "server")]
228    use serde_with::base64::Base64;
229
230    /// Search for connections (service provider integrations)
231    ///
232    /// Method: GET<br>
233    /// Request: [`Request`]<br>
234    /// Response: [`Vec<[ConnectionInfo]>`](ConnectionInfo)
235    pub fn search_path() -> Path {
236        Path::new(["search"])
237    }
238
239    #[derive(Serialize, Deserialize, Clone, Debug)]
240    #[serde(rename_all = "camelCase")]
241    pub struct Request {
242        pub filters: Vec<SearchFilter>,
243        pub iban_detection: bool,
244        #[serde(skip_serializing_if = "Option::is_none")]
245        pub limit: Option<usize>,
246    }
247
248    /// Filters for the connection lookup
249    ///
250    /// String filters look for the given value anywhere in the related field, case-insensitive.
251    #[serde_with::serde_as]
252    #[derive(Serialize, Deserialize, Clone, Debug)]
253    #[serde(rename_all = "camelCase")]
254    #[serde(rename_all_fields = "camelCase")]
255    pub enum SearchFilter {
256        /// List of [`ConnectionType`]s to consider.
257        Types(Vec<ConnectionType>),
258        /// List of [`CountryCode`]s to consider.
259        Countries(Vec<CountryCode>),
260        /// String filter for the provider / product name or any alias.
261        Name(String),
262        /// String filter for the BIC.
263        Bic(String),
264        /// String filter for the (national) bank code.
265        BankCode(String),
266        /// String filter for any of those fields.
267        Term(String),
268        #[cfg(feature = "server")]
269        EncryptedIban(#[serde_as(as = "Base64")] Vec<u8>),
270    }
271
272    /// Type of connections to consider when searching
273    #[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Copy, Clone, Debug)]
274    #[non_exhaustive]
275    #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
276    pub enum ConnectionType {
277        /// Production connections.
278        Production,
279        /// Sandboxes connections, especially test systems provided by third-parties.
280        Sandboxes,
281    }
282
283    /// Query connection information
284    ///
285    /// Method: GET<br>
286    /// Response: [`ConnectionInfo`]
287    pub fn fetch_path(connection_id: &str) -> Path {
288        Path::new(["info", connection_id])
289    }
290
291    /// Connection meta data
292    #[allow(clippy::module_name_repetitions)]
293    #[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug)]
294    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
295    #[serde(rename_all = "camelCase")]
296    #[cfg_attr(not(feature = "server"), non_exhaustive)]
297    pub struct ConnectionInfo {
298        /// Unique identifier.
299        pub id: ConnectionId,
300
301        /// ISO 3166-1 ALPHA-2 country codes.
302        pub countries: Vec<CountryCode>,
303
304        /// Display name.
305        pub display_name: String,
306
307        /// Credentials model.
308        pub credentials: CredentialsModel,
309
310        /// Human-friendly label for the user identifier if relevant.
311        #[serde(skip_serializing_if = "Option::is_none")]
312        #[cfg_attr(feature = "uniffi", uniffi(default))]
313        pub user_id: Option<String>,
314
315        /// Advice for the credentials to be displayed.
316        #[serde(skip_serializing_if = "Option::is_none")]
317        #[cfg_attr(feature = "uniffi", uniffi(default))]
318        pub advice: Option<String>,
319
320        /// Logo identifier.
321        pub logo_id: String,
322    }
323}
324
325macro_rules! context {
326    ($name:ident) => {
327        #[serde_with::serde_as]
328        #[derive(Serialize, Deserialize)]
329        #[serde(transparent)]
330        pub struct $name<S>(#[serde_as(as = "Base64")] Vec<u8>, PhantomData<fn(S)>);
331
332        impl<S> PartialEq for $name<S> {
333            fn eq(&self, other: &Self) -> bool {
334                self.0 == other.0
335            }
336        }
337
338        impl<S> Eq for $name<S> {}
339
340        impl<S> Clone for $name<S> {
341            fn clone(&self) -> Self {
342                Self(self.0.clone(), self.1)
343            }
344        }
345
346        impl<S> fmt::Debug for $name<S> {
347            fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
348                f.debug_tuple("$name")
349                    .field(&self.0)
350                    .field(&self.1)
351                    .finish()
352            }
353        }
354
355        impl<S> From<Vec<u8>> for $name<S> {
356            fn from(value: Vec<u8>) -> Self {
357                Self(value, PhantomData)
358            }
359        }
360
361        impl<S> From<$name<S>> for Vec<u8> {
362            fn from(value: $name<S>) -> Self {
363                value.0
364            }
365        }
366
367        impl<S> AsRef<[u8]> for $name<S> {
368            fn as_ref(&self) -> &[u8] {
369                &self.0
370            }
371        }
372    };
373}
374
375context!(InputContext);
376
377context!(ConfirmationContext);
378
379/// Data authenticated with an HMAC
380///
381/// Use [`to_data()`](Authenticated::to_data) to access the data locally.
382///
383/// Serialization is supported for transfer to a remote system as [JSON Web Token](https://jwt.io/).
384/// The remote system can verify and read the data from the `data` claim:
385/// ```
386/// # let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjpudWxsLCJleHAiOjIwMDAwMDAwMDB9.mWSPWTs-N4npIQpnybpFHbRiLW4D8xS36mX3CmRlRCM";
387/// # type T = ();
388/// # let hmac_key = b"a";
389/// use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
390/// use serde::Deserialize;
391///
392/// #[derive(Deserialize, Clone, Debug)]
393/// struct Claims {
394///     data: T,
395///     exp: u64,
396/// }
397///
398/// let data = decode::<Claims>(
399///     &jwt,
400///     &DecodingKey::from_secret(hmac_key),
401///     &Validation::new(Algorithm::HS256),
402/// )?.claims.data;
403/// # Ok::<(), Box<dyn std::error::Error>>(())
404#[derive(Serialize, PartialEq, Eq, Clone, Debug)]
405#[serde(transparent)]
406pub struct Authenticated<T> {
407    jwt: String,
408    _phantom: PhantomData<T>,
409}
410
411impl<'de, T> Deserialize<'de> for Authenticated<T>
412where
413    T: for<'t> Deserialize<'t> + Clone,
414{
415    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
416    where
417        D: serde::Deserializer<'de>,
418    {
419        String::deserialize(deserializer)?
420            .parse()
421            .map_err(serde::de::Error::custom)
422    }
423}
424
425impl<T> Authenticated<T>
426where
427    T: Serialize,
428{
429    #[must_use]
430    /// Initialize authenticated data.
431    ///
432    /// # Panics
433    ///
434    /// Panics if [`jsonwebtoken::encode`] fails.
435    pub fn new(data: T, key_id: impl Into<String>, key: &[u8]) -> Self {
436        let header = Header {
437            kid: Some(key_id.into()),
438            ..Default::default()
439        };
440
441        let jwt = encode(
442            &header,
443            &Claims {
444                data,
445                // 2050-07-07 14:00 CEST
446                exp: 2_540_808_000,
447            },
448            &EncodingKey::from_secret(key),
449        )
450        .expect("Encoding should work");
451
452        Self {
453            jwt,
454            _phantom: PhantomData,
455        }
456    }
457}
458
459impl<T> FromStr for Authenticated<T>
460where
461    T: for<'de> Deserialize<'de> + Clone,
462{
463    type Err = jsonwebtoken::errors::Error;
464
465    fn from_str(jwt: &str) -> Result<Self, Self::Err> {
466        // Syntax check
467        decode::<T>(jwt)?;
468        Ok(Self {
469            jwt: jwt.to_string(),
470            _phantom: PhantomData,
471        })
472    }
473}
474
475impl<T> Authenticated<T>
476where
477    T: for<'de> Deserialize<'de> + Clone,
478{
479    #[must_use]
480    pub fn as_str(&self) -> &str {
481        &self.jwt
482    }
483
484    #[must_use]
485    /// Get the key identifier from the token header.
486    ///
487    /// # Panics
488    ///
489    /// Panics if the token header cannot get decoded. This is unexpected as decoding is tested in [`FromStr`].
490    pub fn key_id(&self) -> Option<String> {
491        decode_header(&self.jwt).unwrap().kid
492    }
493
494    #[cfg(feature = "client")]
495    #[must_use]
496    #[allow(clippy::missing_panics_doc)]
497    pub fn to_data(&self) -> T {
498        decode(&self.jwt).unwrap()
499    }
500
501    #[cfg(feature = "server")]
502    /// Return contained data, applying a decoder.
503    ///
504    /// # Errors
505    ///
506    /// Forwards errors returned by [`Decoder::decode`].
507    pub fn decode<D: Decoder>(&self, decoder: &D) -> Result<T, D::Error> {
508        decoder.decode::<T>(&self.jwt).map(|d| d.data)
509    }
510}
511
512#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
513#[serde(rename_all = "camelCase")]
514pub struct Claims<T> {
515    pub data: T,
516    pub exp: u64,
517}
518
519#[cfg(feature = "server")]
520pub trait Decoder {
521    type Error;
522
523    /// Verifies `mac` for `data`.
524    ///
525    /// # Errors
526    ///
527    /// Returns `Self::Error` if verification fails.
528    fn decode<T>(&self, jwt: &str) -> Result<Claims<T>, Self::Error>
529    where
530        T: for<'de> Deserialize<'de> + Clone;
531}
532
533/// Response from YAXI Open Banking services.
534///
535/// The response either carries an [authenticated](Authenticated) [result](OBResult)
536/// or an interrupt (i.e. a dialog or redirect for the user).
537#[derive(Serialize, Deserialize, Debug)]
538pub enum OBResponse<S: Service> {
539    #[serde(bound = "S:")]
540    Result(
541        Authenticated<OBResult<S::Output>>,
542        Option<Session>,
543        Option<ConnectionData>,
544    ),
545    #[serde(bound = "S:")]
546    Dialog(Dialog<S>),
547    #[serde(bound = "S:")]
548    Redirect(Redirect<S>),
549    #[serde(bound = "S:")]
550    RedirectHandle(RedirectHandle<S>),
551}
552
553impl<S: Service> Clone for OBResponse<S> {
554    fn clone(&self) -> Self {
555        match self {
556            OBResponse::Result(authenticated, session, connection_data) => OBResponse::Result(
557                authenticated.clone(),
558                session.clone(),
559                connection_data.clone(),
560            ),
561            OBResponse::Dialog(dialog) => OBResponse::Dialog(dialog.clone()),
562            OBResponse::Redirect(redirect) => OBResponse::Redirect(redirect.clone()),
563            OBResponse::RedirectHandle(redirect_handle) => {
564                OBResponse::RedirectHandle(redirect_handle.clone())
565            }
566        }
567    }
568}
569
570#[derive(Serialize, Deserialize, Debug)]
571#[cfg_attr(not(feature = "server"), non_exhaustive)]
572pub struct NonInteractiveResponse<S: Service> {
573    #[serde(bound = "S:")]
574    pub result: S::Output,
575    pub connection_data: Option<ConnectionData>,
576}
577
578/// A value returned by YAXI Open Banking services.
579///
580/// Besides the value itself, it contains a timestamp and a ticket identifier
581/// (bound to known input parameters and service type).
582#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
583#[serde(rename_all = "camelCase")]
584pub struct OBResult<T> {
585    pub data: T,
586    pub ticket_id: String,
587    pub timestamp: DateTime,
588}
589
590#[serde_with::serde_as]
591#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
592pub struct Session(#[serde_as(as = "Base64")] Vec<u8>);
593
594impl From<Vec<u8>> for Session {
595    fn from(value: Vec<u8>) -> Self {
596        Session(value)
597    }
598}
599
600impl From<Session> for Vec<u8> {
601    fn from(session: Session) -> Self {
602        session.0
603    }
604}
605
606impl AsRef<[u8]> for Session {
607    fn as_ref(&self) -> &[u8] {
608        &self.0
609    }
610}
611
612#[serde_with::serde_as]
613#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
614#[serde(transparent)]
615pub struct ConnectionData(#[serde_as(as = "Base64")] Vec<u8>);
616
617impl From<Vec<u8>> for ConnectionData {
618    fn from(value: Vec<u8>) -> Self {
619        ConnectionData(value)
620    }
621}
622
623impl From<ConnectionData> for Vec<u8> {
624    fn from(connection_data: ConnectionData) -> Self {
625        connection_data.0
626    }
627}
628
629impl AsRef<[u8]> for ConnectionData {
630    fn as_ref(&self) -> &[u8] {
631        &self.0
632    }
633}
634
635/// User redirect.
636///
637/// The user is meant to get sent to the [`url`](Self::url) and the [`context`](Self::context) can
638/// be used for continuing the process at the service that issued the redirect object afterward.
639///
640/// A web application needs to direct the user agent to the returned URL.
641/// A desktop or mobile application could either open it in a browser or inside an element like a WebView.
642#[derive(Serialize, Deserialize)]
643#[serde(rename_all = "camelCase")]
644pub struct Redirect<S> {
645    pub url: Url,
646    #[serde(bound = "S:")]
647    pub context: ConfirmationContext<S>,
648}
649
650impl<S> PartialEq for Redirect<S> {
651    fn eq(&self, other: &Self) -> bool {
652        let Self { url, context } = self;
653        url == &other.url && context == &other.context
654    }
655}
656
657impl<S> Eq for Redirect<S> {}
658
659impl<S> Clone for Redirect<S> {
660    fn clone(&self) -> Self {
661        Self {
662            url: self.url.clone(),
663            context: self.context.clone(),
664        }
665    }
666}
667
668impl<S> fmt::Debug for Redirect<S> {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        f.debug_struct("Redirect")
671            .field("url", &self.url)
672            .field("context", &self.context)
673            .finish()
674    }
675}
676
677/// Incomplete user redirect.
678///
679/// A final redirect URI needs to get registered, using the handle, to receive the URL to send the user to.
680#[derive(Serialize, Deserialize)]
681#[serde(rename_all = "camelCase")]
682pub struct RedirectHandle<S> {
683    pub handle: String,
684    #[serde(bound = "S:")]
685    pub context: ConfirmationContext<S>,
686}
687
688impl<S> PartialEq for RedirectHandle<S> {
689    fn eq(&self, other: &Self) -> bool {
690        let Self { handle, context } = self;
691        handle == &other.handle && context == &other.context
692    }
693}
694
695impl<S> Eq for RedirectHandle<S> {}
696
697impl<S> Clone for RedirectHandle<S> {
698    fn clone(&self) -> Self {
699        Self {
700            handle: self.handle.clone(),
701            context: self.context.clone(),
702        }
703    }
704}
705
706impl<S> fmt::Debug for RedirectHandle<S> {
707    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708        f.debug_struct("RedirectHandle")
709            .field("handle", &self.handle)
710            .field("context", &self.context)
711            .finish()
712    }
713}
714
715/// Decode a JWT without any validation or verification.
716fn decode<T>(jwt: &str) -> jsonwebtoken::errors::Result<T>
717where
718    T: for<'de> Deserialize<'de> + Clone,
719{
720    insecure_decode::<Claims<T>>(jwt).map(|data| data.claims.data)
721}
722
723#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
724pub enum ServiceId {
725    Accounts,
726    CollectPayment,
727    Transactions,
728    Balances,
729}
730
731impl fmt::Display for ServiceId {
732    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
733        self.serialize(f)
734    }
735}
736
737/// Ticket to use a service.
738///
739/// This must be issued by a trusted backend for use in the (untrusted) frontend.
740///
741/// It carries
742/// * an identifier of the service that is intended to be used,
743/// * an arbitrary ticket identifier for verifying the results later on and
744/// * any critical input data for the service.
745///
746/// The ticket data gets wrapped in a [JSON Web Token](https://jwt.io/) as claim `data`.
747/// The token must also contain an expiry in the claim `exp` and the key identifier in the `kid` header field.
748///
749/// Example issuance of an `Authenticated<Ticket<accounts::Service>>` for the accounts service:
750///
751/// ```
752/// # const HMAC_KEY: &[u8] = b"foo";
753/// # const HMAC_KEY_ID: &str = "foo";
754/// use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
755/// use serde::Serialize;
756/// use std::time::{SystemTime, UNIX_EPOCH};
757/// use uuid::Uuid;
758///
759/// let ticket_id = Uuid::new_v4().to_string();
760///
761/// #[derive(Serialize, Debug)]
762/// struct Ticket<T> {
763///     service: String,
764///     id: String,
765///     data: T,
766/// }
767///
768/// #[derive(Serialize, Debug)]
769/// struct Claims<T> {
770///     data: Ticket<T>,
771///     exp: u64,
772/// }
773///
774/// let header = Header {
775///     alg: Algorithm::HS256,
776///     kid: Some(HMAC_KEY_ID.to_string()),
777///     ..Default::default()
778/// };
779///
780/// let authenticated_ticket = encode(
781///     &header,
782///     &Claims {
783///         data: Ticket {
784///             service: "Accounts".to_string(),
785///             id: ticket_id,
786///             data: (),
787///         },
788///         exp: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() + 600,
789///     },
790///     &EncodingKey::from_secret(HMAC_KEY),
791/// )?;
792/// # let auth: routex_api::Authenticated<routex_api::Ticket<routex_api::accounts::Service>> = authenticated_ticket.parse().unwrap();
793/// # auth.to_data();
794/// # auth.key_id().unwrap();
795/// # Ok::<(), Box<dyn std::error::Error>>(())
796#[derive(Serialize, Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub struct Ticket<S: Service> {
799    pub service: ServiceId,
800    pub id: String,
801    pub data: S::TicketData,
802}
803
804impl<S: Service> PartialEq for Ticket<S> {
805    fn eq(&self, other: &Self) -> bool {
806        let Self { service, id, data } = self;
807        service == &other.service && id == &other.id && data == &other.data
808    }
809}
810
811impl<S: Service> Eq for Ticket<S> {}
812
813impl<S: Service> fmt::Debug for Ticket<S> {
814    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815        f.debug_struct("Ticket")
816            .field("service", &self.service)
817            .field("id", &self.id)
818            .field("data", &self.data)
819            .finish()
820    }
821}
822
823impl<S: Service> Clone for Ticket<S> {
824    fn clone(&self) -> Self {
825        Self {
826            service: self.service,
827            id: self.id.clone(),
828            data: self.data.clone(),
829        }
830    }
831}
832
833pub trait Service {
834    const ID: ServiceId;
835
836    type TicketData: Serialize
837        + for<'de> Deserialize<'de>
838        + PartialEq
839        + Eq
840        + Clone
841        + fmt::Debug
842        + Send
843        + Sync;
844
845    type RequestData: Serialize + for<'de> Deserialize<'de> + Send + Sync + Clone;
846
847    type Output: Serialize + for<'de> Deserialize<'de> + Send + fmt::Debug + Clone;
848
849    /// Use the service
850    ///
851    /// Method: POST<br>
852    /// Request: `Self::Request`<br>
853    /// Response: [`OBResponse<Self>`](OBResponse)
854    fn path() -> Path {
855        Path::new([&Self::ID.to_string().to_kebab_case(), "service"])
856    }
857
858    fn response_path() -> Path {
859        Path::new([&Self::ID.to_string().to_kebab_case(), "response"])
860    }
861
862    fn confirmation_path() -> Path {
863        Path::new([&Self::ID.to_string().to_kebab_case(), "confirmation"])
864    }
865}
866
867#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
868#[serde(tag = "ticketStatus")]
869pub enum TicketStatus<T> {
870    Unfinished,
871    Error(ErrorKind),
872    Success(T),
873}
874
875#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
876#[serde(tag = "error")]
877pub enum ErrorKind {
878    UnexpectedError,
879    Canceled,
880    InvalidCredentials,
881    ServiceBlocked,
882    Unauthorized,
883    ConsentExpired,
884    AccessExceeded,
885    PeriodOutOfBounds,
886    UnsupportedProduct,
887    PaymentFailed,
888    UnexpectedValue,
889    TicketError,
890    ProviderError,
891    NotFound,
892}
893
894#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
895#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
896#[serde(rename_all = "camelCase")]
897pub struct Credentials {
898    pub connection_id: ConnectionId,
899    #[serde(skip_serializing_if = "Option::is_none")]
900    #[cfg_attr(feature = "uniffi", uniffi(default))]
901    pub user_id: Option<String>,
902    #[serde(skip_serializing_if = "Option::is_none")]
903    #[cfg_attr(feature = "uniffi", uniffi(default))]
904    pub password: Option<String>,
905    #[serde(skip_serializing_if = "Option::is_none")]
906    #[cfg_attr(feature = "uniffi", uniffi(default))]
907    pub connection_data: Option<ConnectionData>,
908}
909
910#[derive(Serialize, Deserialize)]
911#[serde(rename_all = "camelCase")]
912pub struct ResponseData<S> {
913    #[serde(bound = "S:")]
914    pub context: InputContext<S>,
915    pub response: String,
916}
917
918impl<S> PartialEq for ResponseData<S> {
919    fn eq(&self, other: &Self) -> bool {
920        let Self { context, response } = self;
921        context == &other.context && response == &other.response
922    }
923}
924
925impl<S> Eq for ResponseData<S> {}
926
927impl<S> Clone for ResponseData<S> {
928    fn clone(&self) -> Self {
929        Self {
930            context: self.context.clone(),
931            response: self.response.clone(),
932        }
933    }
934}
935
936impl<S> fmt::Debug for ResponseData<S> {
937    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938        f.debug_struct("ResponseData")
939            .field("context", &self.context)
940            .field("response", &self.response)
941            .finish()
942    }
943}
944
945#[derive(Serialize, Deserialize)]
946#[serde(rename_all = "camelCase")]
947pub struct ConfirmationData<S> {
948    #[serde(bound = "S:")]
949    pub context: ConfirmationContext<S>,
950}
951
952impl<S> PartialEq for ConfirmationData<S> {
953    fn eq(&self, other: &Self) -> bool {
954        let Self { context } = self;
955        context == &other.context
956    }
957}
958
959impl<S> Eq for ConfirmationData<S> {}
960
961impl<S> Clone for ConfirmationData<S> {
962    fn clone(&self) -> Self {
963        Self {
964            context: self.context.clone(),
965        }
966    }
967}
968
969impl<S> fmt::Debug for ConfirmationData<S> {
970    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
971        f.debug_struct("ConfirmationData")
972            .field("context", &self.context)
973            .finish()
974    }
975}
976
977#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
978#[serde(rename_all_fields = "camelCase")]
979#[cfg_attr(not(feature = "exhaustive-error"), non_exhaustive)]
980pub enum Error {
981    #[cfg_attr(not(feature = "server"), non_exhaustive)]
982    UnexpectedError {
983        #[serde(skip_serializing_if = "Option::is_none")]
984        user_message: Option<String>,
985    },
986    #[cfg_attr(not(feature = "server"), non_exhaustive)]
987    Canceled {},
988    #[cfg_attr(not(feature = "server"), non_exhaustive)]
989    InvalidCredentials {
990        #[serde(skip_serializing_if = "Option::is_none")]
991        user_message: Option<String>,
992    },
993    #[cfg_attr(not(feature = "server"), non_exhaustive)]
994    ServiceBlocked {
995        #[serde(skip_serializing_if = "Option::is_none")]
996        user_message: Option<String>,
997        #[serde(skip_serializing_if = "Option::is_none")]
998        code: Option<ServiceBlockedCode>,
999    },
1000    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1001    Unauthorized {
1002        #[serde(skip_serializing_if = "Option::is_none")]
1003        user_message: Option<String>,
1004    },
1005    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1006    ConsentExpired {
1007        #[serde(skip_serializing_if = "Option::is_none")]
1008        user_message: Option<String>,
1009    },
1010    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1011    AccessExceeded {
1012        #[serde(skip_serializing_if = "Option::is_none")]
1013        user_message: Option<String>,
1014    },
1015    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1016    PeriodOutOfBounds {
1017        #[serde(skip_serializing_if = "Option::is_none")]
1018        user_message: Option<String>,
1019    },
1020    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1021    UnsupportedProduct {
1022        #[serde(skip_serializing_if = "Option::is_none")]
1023        reason: Option<UnsupportedProductReason>,
1024        #[serde(skip_serializing_if = "Option::is_none")]
1025        user_message: Option<String>,
1026    },
1027    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1028    PaymentFailed {
1029        #[serde(skip_serializing_if = "Option::is_none")]
1030        code: Option<PaymentErrorCode>,
1031        #[serde(skip_serializing_if = "Option::is_none")]
1032        user_message: Option<String>,
1033    },
1034    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1035    UnexpectedValue { error: String },
1036    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1037    TicketError {
1038        error: String,
1039        code: TicketErrorCode,
1040    },
1041    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1042    ProviderError {
1043        #[serde(skip_serializing_if = "Option::is_none")]
1044        code: Option<ProviderErrorCode>,
1045        #[serde(skip_serializing_if = "Option::is_none")]
1046        user_message: Option<String>,
1047    },
1048    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1049    InterruptError {},
1050}
1051
1052#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
1053#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1054#[cfg_attr(not(feature = "server"), non_exhaustive)]
1055pub enum TicketErrorCode {
1056    /// Missing "yaxi-ticket" header
1057    Missing,
1058    /// Invalid ticket
1059    Invalid,
1060    /// Ticket token lacks "kid"
1061    MissingKey,
1062    /// Unknown key
1063    UnknownKey,
1064    /// Ticket does not match service
1065    Mismatch,
1066    /// Ticket is expired
1067    Expired,
1068    /// Ticket lifetime is too long
1069    InvalidLifetime,
1070    /// Expired key
1071    ExpiredKey,
1072    /// Environment mismatch between key and routex
1073    KeyEnvironmentMismatch,
1074}
1075
1076#[derive(Serialize, Deserialize, Clone, Debug)]
1077#[cfg_attr(not(feature = "server"), non_exhaustive)]
1078pub enum Filter<F> {
1079    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1080    Eq(F, Value),
1081    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1082    NotEq(F, Value),
1083    #[cfg(feature = "server")]
1084    Contains(F, Value),
1085    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1086    And(Box<Self>, Box<Self>),
1087    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1088    Or(Box<Self>, Box<Self>),
1089    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1090    Supports(SupportedService),
1091}
1092
1093#[derive(Serialize, Deserialize, Clone, Debug)]
1094#[cfg_attr(not(feature = "server"), non_exhaustive)]
1095#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1096pub enum SupportedService {
1097    CollectPayment,
1098}
1099
1100impl<F> Filter<F> {
1101    #[must_use]
1102    /// Add an additional filter.
1103    pub fn and(self, other: Filter<F>) -> Filter<F> {
1104        Filter::And(Box::new(self), Box::new(other))
1105    }
1106
1107    #[must_use]
1108    /// Add an alternative filter.
1109    pub fn or(self, other: Filter<F>) -> Filter<F> {
1110        Filter::Or(Box::new(self), Box::new(other))
1111    }
1112}
1113
1114pub struct Field<F, T> {
1115    field: F,
1116    phantom: PhantomData<T>,
1117}
1118
1119impl<F, T> Field<F, T>
1120where
1121    F: Copy,
1122    T: Serialize,
1123{
1124    /// Filter for equality with a given value.
1125    ///
1126    /// # Panics
1127    ///
1128    /// Panics if [`serde_json::to_value`] for the value fails.
1129    pub fn eq(&self, value: T) -> Filter<F> {
1130        Filter::Eq(self.field, serde_json::to_value(&value).unwrap())
1131    }
1132
1133    /// Filter for inequality with a given value.
1134    ///
1135    /// # Panics
1136    ///
1137    /// Panics if [`serde_json::to_value`] for the value fails.
1138    pub fn not_eq(&self, value: T) -> Filter<F> {
1139        Filter::NotEq(self.field, serde_json::to_value(&value).unwrap())
1140    }
1141}
1142
1143pub trait GetValue {
1144    type Model;
1145
1146    fn get(&self, model: &Self::Model) -> Value;
1147}
1148
1149macro_rules! fields {
1150    {
1151        $model:ty, $enum_name:ident
1152        $(($const_name:ident, $variant_name:ident, $field_name:ident, $($type:ty)+))+
1153        $(
1154            server:
1155            $(($server_variant_name:ident, $server_field_name:ident))+
1156        )?
1157    } => {
1158        #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
1159        #[non_exhaustive]
1160        #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1161        pub enum $enum_name {
1162            $($variant_name,)+
1163            $($(
1164                #[cfg(feature = "server")]
1165                $server_variant_name,
1166            )+)?
1167        }
1168
1169        impl $enum_name {
1170            $(
1171                pub const $const_name: crate::Field<$enum_name, $($type)+> = crate::Field {
1172                    field: $enum_name::$variant_name,
1173                    phantom: std::marker::PhantomData,
1174                };
1175            )+
1176        }
1177
1178        impl crate::GetValue for $enum_name {
1179            type Model = $model;
1180
1181            fn get(&self, model: &$model) -> serde_json::Value {
1182                match self {
1183                    $($enum_name::$variant_name => serde_json::to_value(&model.$field_name),)+
1184                    $($(
1185                        #[cfg(feature = "server")]
1186                        $enum_name::$server_variant_name => serde_json::to_value(&model.$server_field_name),
1187                    )+)?
1188                }.expect("Serialization should work")
1189            }
1190        }
1191    }
1192}
1193
1194#[derive(Serialize, Deserialize)]
1195#[serde(rename_all = "camelCase")]
1196pub struct ServiceRequest<S: Service> {
1197    pub credentials: Credentials,
1198    pub session: Option<Session>,
1199    #[serde(default, skip_serializing_if = "is_false")]
1200    pub recurring_consents: bool,
1201    #[serde(flatten, bound = "S:")]
1202    pub data: <S as Service>::RequestData,
1203}
1204
1205impl<S: Service> Clone for ServiceRequest<S> {
1206    fn clone(&self) -> Self {
1207        Self {
1208            credentials: self.credentials.clone(),
1209            session: self.session.clone(),
1210            recurring_consents: self.recurring_consents,
1211            data: self.data.clone(),
1212        }
1213    }
1214}
1215
1216#[derive(Serialize, Deserialize)]
1217#[serde(rename_all = "camelCase")]
1218pub struct NonInteractiveRequest<S: Service> {
1219    pub connection_data: ConnectionData,
1220    #[serde(flatten, bound = "S:")]
1221    pub data: <S as Service>::RequestData,
1222}
1223
1224#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
1225#[serde(rename_all = "camelCase")]
1226#[non_exhaustive]
1227#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1228pub struct Account {
1229    /// ISO 20022 IBAN2007Identifier.
1230    #[serde(skip_serializing_if = "Option::is_none")]
1231    #[cfg_attr(feature = "uniffi", uniffi(default))]
1232    pub iban: Option<String>,
1233
1234    /// Account number that is not an IBAN, e.g. ISO 20022 BBANIdentifier or primary account number (PAN) of a card account.
1235    #[serde(skip_serializing_if = "Option::is_none")]
1236    #[cfg_attr(feature = "uniffi", uniffi(default))]
1237    pub number: Option<String>,
1238
1239    /// ISO 20022 BICFIIdentifier.
1240    #[serde(skip_serializing_if = "Option::is_none")]
1241    #[cfg_attr(feature = "uniffi", uniffi(default))]
1242    pub bic: Option<String>,
1243
1244    /// National bank code.
1245    #[serde(skip_serializing_if = "Option::is_none")]
1246    #[cfg_attr(feature = "uniffi", uniffi(default))]
1247    pub bank_code: Option<String>,
1248
1249    /// ISO 4217 Alpha 3 currency code.
1250    #[serde(skip_serializing_if = "Option::is_none")]
1251    #[cfg_attr(feature = "uniffi", uniffi(default))]
1252    pub currency: Option<String>,
1253
1254    /// Name of account, assigned by ASPSP.
1255    #[serde(skip_serializing_if = "Option::is_none")]
1256    #[cfg_attr(feature = "uniffi", uniffi(default))]
1257    pub name: Option<String>,
1258
1259    /// Display name of account, assigned by PSU.
1260    #[serde(skip_serializing_if = "Option::is_none")]
1261    #[cfg_attr(feature = "uniffi", uniffi(default))]
1262    pub display_name: Option<String>,
1263
1264    /// Legal account owner.
1265    #[serde(skip_serializing_if = "Option::is_none")]
1266    #[cfg_attr(feature = "uniffi", uniffi(default))]
1267    pub owner_name: Option<String>,
1268
1269    /// Product name.
1270    #[serde(skip_serializing_if = "Option::is_none")]
1271    #[cfg_attr(feature = "uniffi", uniffi(default))]
1272    pub product_name: Option<String>,
1273
1274    /// Account status.
1275    #[serde(skip_serializing_if = "Option::is_none")]
1276    #[cfg_attr(feature = "uniffi", uniffi(default))]
1277    pub status: Option<AccountStatus>,
1278
1279    /// Account type.
1280    #[serde(rename = "type")]
1281    #[serde(skip_serializing_if = "Option::is_none")]
1282    #[cfg_attr(feature = "uniffi", uniffi(default))]
1283    pub type_: Option<AccountType>,
1284}
1285
1286pub mod accounts {
1287    use super::{Account, AccountStatus, AccountType, Filter, ServiceId, SupportedService};
1288    use serde::{Deserialize, Serialize};
1289
1290    #[derive(Clone, Debug)]
1291    pub struct Service {}
1292
1293    impl super::Service for Service {
1294        const ID: ServiceId = ServiceId::Accounts;
1295
1296        type TicketData = ();
1297
1298        type RequestData = RequestData;
1299
1300        type Output = Vec<Account>;
1301    }
1302
1303    fields! {
1304        routex_models::Account, AccountField
1305        (IBAN, Iban, iban, Option<String>)
1306        (NUMBER, Number, number, Option<String>)
1307        (BIC, Bic, bic, Option<String>)
1308        (BANK_CODE, BankCode, bank_code, Option<String>)
1309        (CURRENCY, Currency, currency, String)
1310        (NAME, Name, name, Option<String>)
1311        (DISPLAY_NAME, DisplayName, display_name, Option<String>)
1312        (OWNER_NAME, OwnerName, owner_name, Option<String>)
1313        (PRODUCT_NAME, ProductName, product_name, Option<String>)
1314        (STATUS, Status, status, Option<AccountStatus>)
1315        (TYPE, Type, type_, Option<AccountType>)
1316        server:
1317        (Capabilities, capabilities)
1318    }
1319
1320    impl Account {
1321        #[must_use]
1322        /// Filter for possible support of YAXI Open Banking services.
1323        pub fn supports(service: SupportedService) -> Filter<AccountField> {
1324            Filter::Supports(service)
1325        }
1326    }
1327
1328    #[derive(Serialize, Deserialize, Clone)]
1329    #[serde(rename_all = "camelCase")]
1330    pub struct RequestData {
1331        pub fields: Vec<AccountField>,
1332        pub filter: Option<Filter<AccountField>>,
1333    }
1334}
1335
1336#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1337#[serde(rename_all = "camelCase")]
1338#[serde(rename_all_fields = "camelCase")]
1339#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1340pub enum AccountIdentifier {
1341    /// ISO 20022 IBAN2007Identifier.
1342    Iban(String),
1343}
1344
1345#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1346#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1347#[serde(rename_all = "camelCase")]
1348pub struct AccountReference {
1349    #[serde(flatten)]
1350    pub id: AccountIdentifier,
1351    #[serde(skip_serializing_if = "Option::is_none")]
1352    #[cfg_attr(feature = "uniffi", uniffi(default))]
1353    pub currency: Option<String>,
1354}
1355
1356pub mod balances {
1357    pub use rust_decimal::Decimal;
1358    use serde::{Deserialize, Serialize};
1359
1360    use crate::{AccountReference, Path, ServiceId};
1361
1362    pub fn non_interactive_path() -> Path {
1363        Path::new(["balances", "non-interactive"])
1364    }
1365
1366    #[derive(Clone, Debug)]
1367    pub struct Service {}
1368
1369    impl super::Service for Service {
1370        const ID: ServiceId = ServiceId::Balances;
1371
1372        type TicketData = ();
1373
1374        type RequestData = RequestData;
1375
1376        type Output = Balances;
1377    }
1378
1379    #[derive(Serialize, Deserialize, Clone)]
1380    #[serde(rename_all = "camelCase")]
1381    pub struct RequestData {
1382        pub accounts: Vec<AccountReference>,
1383    }
1384
1385    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1386    #[serde(rename_all = "camelCase")]
1387    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1388    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1389    pub struct Balances {
1390        /// List of balances for accounts.
1391        pub balances: Vec<AccountBalances>,
1392
1393        /// Accounts that were requested in [`Request::accounts`] but not found.
1394        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1395        #[cfg_attr(feature = "uniffi", uniffi(default))]
1396        pub missing_accounts: Vec<AccountReference>,
1397    }
1398
1399    #[allow(clippy::module_name_repetitions)]
1400    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1401    #[serde(rename_all = "camelCase")]
1402    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1403    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1404    pub struct AccountBalances {
1405        pub account: AccountReference,
1406
1407        /// List of balances (of different types) for the account.
1408        pub balances: Vec<Balance>,
1409    }
1410
1411    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1412    #[serde(rename_all = "camelCase")]
1413    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1414    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1415    pub struct Balance {
1416        pub amount: Decimal,
1417        pub currency: String,
1418        pub balance_type: BalanceType,
1419    }
1420
1421    #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
1422    #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1423    pub enum BalanceType {
1424        /// Balance from booked transactions.
1425        Booked,
1426
1427        /// Balance from booked transactions and pending debits.
1428        Available,
1429
1430        /// Expected balance from booked and pending transactions.
1431        Expected,
1432    }
1433}
1434
1435pub mod transactions {
1436    use chrono::NaiveDate;
1437    use isocountry::CountryCode;
1438    pub use routex_models::{Amount, Fee, TransactionStatus};
1439    use rust_decimal::Decimal;
1440    use serde::{Deserialize, Serialize};
1441    use url::Url;
1442
1443    use crate::{AccountReference, Path, ServiceId};
1444
1445    pub fn non_interactive_path() -> Path {
1446        Path::new(["transactions", "non-interactive"])
1447    }
1448
1449    #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1450    #[serde(rename_all = "camelCase")]
1451    pub struct TicketData {
1452        pub account: AccountReference,
1453        pub range: Range,
1454        pub webhook: Option<Url>,
1455    }
1456
1457    #[derive(Clone, Debug)]
1458    pub struct Service {}
1459
1460    impl super::Service for Service {
1461        const ID: ServiceId = ServiceId::Transactions;
1462
1463        type TicketData = TicketData;
1464
1465        type RequestData = RequestData;
1466
1467        type Output = Option<Vec<Transaction>>;
1468    }
1469
1470    #[derive(Serialize, Deserialize, Clone)]
1471    #[serde(rename_all = "camelCase")]
1472    pub struct RequestData {}
1473
1474    #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1475    #[serde(rename_all = "camelCase")]
1476    #[serde(rename_all_fields = "camelCase")]
1477    pub enum Range {
1478        Reference(String),
1479        #[serde(untagged)]
1480        Period {
1481            /// Earliest date (inclusive)
1482            from: NaiveDate,
1483            /// Latest date (inclusive)
1484            to: Option<NaiveDate>,
1485        },
1486    }
1487
1488    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1489    #[serde(rename_all = "camelCase")]
1490    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1491    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1492    pub struct Transaction {
1493        /// Identifier used for delta requests.
1494        #[serde(skip_serializing_if = "Option::is_none")]
1495        #[cfg_attr(feature = "uniffi", uniffi(default))]
1496        pub entry_reference: Option<String>,
1497
1498        #[serde(skip_serializing_if = "Option::is_none")]
1499        #[cfg_attr(feature = "uniffi", uniffi(default))]
1500        pub batch: Option<BatchData>,
1501
1502        /// Booking date (ASPSP's books).
1503        #[serde(skip_serializing_if = "Option::is_none")]
1504        #[cfg_attr(feature = "uniffi", uniffi(default))]
1505        pub booking_date: Option<NaiveDate>,
1506
1507        /// Value date. Expected / requested value date in case of pending entries.
1508        #[serde(skip_serializing_if = "Option::is_none")]
1509        #[cfg_attr(feature = "uniffi", uniffi(default))]
1510        pub value_date: Option<NaiveDate>,
1511
1512        /// Date of the actual transaction, e.g. a card payment.
1513        #[serde(skip_serializing_if = "Option::is_none")]
1514        #[cfg_attr(feature = "uniffi", uniffi(default))]
1515        pub transaction_date: Option<NaiveDate>,
1516
1517        /// Transaction status.
1518        pub status: TransactionStatus,
1519
1520        /// Unique reference assigned by the account servicer.
1521        #[serde(skip_serializing_if = "Option::is_none")]
1522        #[cfg_attr(feature = "uniffi", uniffi(default))]
1523        pub account_servicer_reference: Option<String>,
1524
1525        /// Unique identifier assigned by the sending party.
1526        #[serde(skip_serializing_if = "Option::is_none")]
1527        #[cfg_attr(feature = "uniffi", uniffi(default))]
1528        pub payment_id: Option<String>,
1529
1530        /// Unique identifier assigned by the first instructing agent.
1531        #[serde(skip_serializing_if = "Option::is_none")]
1532        #[cfg_attr(feature = "uniffi", uniffi(default))]
1533        pub transaction_id: Option<String>,
1534
1535        /// Unique end-to-end identifier assigned by the initiating party.
1536        #[serde(skip_serializing_if = "Option::is_none")]
1537        #[cfg_attr(feature = "uniffi", uniffi(default))]
1538        pub end_to_end_id: Option<String>,
1539
1540        /// Mandate identifier.
1541        #[serde(skip_serializing_if = "Option::is_none")]
1542        #[cfg_attr(feature = "uniffi", uniffi(default))]
1543        pub mandate_id: Option<String>,
1544
1545        /// SEPA creditor identifier.
1546        #[serde(skip_serializing_if = "Option::is_none")]
1547        #[cfg_attr(feature = "uniffi", uniffi(default))]
1548        pub creditor_id: Option<String>,
1549
1550        /// Transaction amount as billed to the account.
1551        pub amount: Amount,
1552
1553        /// Indicator for reversals.
1554        #[serde(default, skip_serializing_if = "super::is_false")]
1555        #[cfg_attr(feature = "uniffi", uniffi(default))]
1556        pub reversal: bool,
1557
1558        /// Original amount of the transaction.
1559        #[serde(skip_serializing_if = "Option::is_none")]
1560        #[cfg_attr(feature = "uniffi", uniffi(default))]
1561        pub original_amount: Option<Amount>,
1562
1563        /// Exchange rates.
1564        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1565        #[cfg_attr(feature = "uniffi", uniffi(default))]
1566        pub exchanges: Vec<ExchangeRate>,
1567
1568        /// Any fees related to the transaction.
1569        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1570        #[cfg_attr(feature = "uniffi", uniffi(default))]
1571        pub fees: Vec<Fee>,
1572
1573        /// Creditor data. In case of reversals this refers to the initial transaction.
1574        #[serde(skip_serializing_if = "Option::is_none")]
1575        #[cfg_attr(feature = "uniffi", uniffi(default))]
1576        pub creditor: Option<Party>,
1577
1578        /// Debtor data. In case of reversals this refers to the initial transaction.
1579        #[serde(skip_serializing_if = "Option::is_none")]
1580        #[cfg_attr(feature = "uniffi", uniffi(default))]
1581        pub debtor: Option<Party>,
1582
1583        /// Remittance (purpose).
1584        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1585        #[cfg_attr(feature = "uniffi", uniffi(default))]
1586        pub remittance_information: Vec<String>,
1587
1588        /// ISO 20022 ExternalPurpose1Code.
1589        #[serde(skip_serializing_if = "Option::is_none")]
1590        #[cfg_attr(feature = "uniffi", uniffi(default))]
1591        pub purpose_code: Option<String>,
1592
1593        /// Bank Transaction Codes.
1594        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1595        #[cfg_attr(feature = "uniffi", uniffi(default))]
1596        pub bank_transaction_codes: Vec<BankTransactionCode>,
1597
1598        /// Additional information attached to the transaction.
1599        ///
1600        /// This might be a proprietary, localized, human-readable long text corresponding to some machine-readable bank transaction code that is not directly provided by the bank.
1601        #[serde(skip_serializing_if = "Option::is_none")]
1602        #[cfg_attr(feature = "uniffi", uniffi(default))]
1603        pub additional_information: Option<String>,
1604    }
1605
1606    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1607    #[serde(rename_all = "camelCase")]
1608    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1609    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1610    pub struct BatchData {
1611        /// Number of transactions in the batch, if known.
1612        #[serde(skip_serializing_if = "Option::is_none")]
1613        pub number_of_transactions: Option<u32>,
1614
1615        /// Details of transactions in the batch.
1616        ///
1617        /// Note that this does not necessarily match a given number of transactions.
1618        /// It could be e.g. empty as no details are given or a single entry with common details on all transactions in the batch.
1619        pub transactions: Vec<BatchTransactionDetails>,
1620    }
1621
1622    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1623    #[serde(rename_all = "camelCase")]
1624    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1625    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1626    pub struct BatchTransactionDetails {
1627        /// Unique reference assigned by the account servicer.
1628        #[serde(skip_serializing_if = "Option::is_none")]
1629        #[cfg_attr(feature = "uniffi", uniffi(default))]
1630        pub account_servicer_reference: Option<String>,
1631
1632        /// Unique identifier assigned by the sending party.
1633        #[serde(skip_serializing_if = "Option::is_none")]
1634        #[cfg_attr(feature = "uniffi", uniffi(default))]
1635        pub payment_id: Option<String>,
1636
1637        /// Unique identifier assigned by the first instructing agent.
1638        #[serde(skip_serializing_if = "Option::is_none")]
1639        #[cfg_attr(feature = "uniffi", uniffi(default))]
1640        pub transaction_id: Option<String>,
1641
1642        /// Unique end-to-end identifier assigned by the initiating party.
1643        #[serde(skip_serializing_if = "Option::is_none")]
1644        #[cfg_attr(feature = "uniffi", uniffi(default))]
1645        pub end_to_end_id: Option<String>,
1646
1647        /// Mandate identifier.
1648        #[serde(skip_serializing_if = "Option::is_none")]
1649        #[cfg_attr(feature = "uniffi", uniffi(default))]
1650        pub mandate_id: Option<String>,
1651
1652        /// SEPA creditor identifier.
1653        #[serde(skip_serializing_if = "Option::is_none")]
1654        #[cfg_attr(feature = "uniffi", uniffi(default))]
1655        pub creditor_id: Option<String>,
1656
1657        /// Transaction amount as billed to the account.
1658        #[serde(skip_serializing_if = "Option::is_none")]
1659        #[cfg_attr(feature = "uniffi", uniffi(default))]
1660        pub amount: Option<Amount>,
1661
1662        /// Indicator for reversals.
1663        #[serde(default, skip_serializing_if = "super::is_false")]
1664        #[cfg_attr(feature = "uniffi", uniffi(default))]
1665        pub reversal: bool,
1666
1667        /// Original amount of the transaction.
1668        #[serde(skip_serializing_if = "Option::is_none")]
1669        #[cfg_attr(feature = "uniffi", uniffi(default))]
1670        pub original_amount: Option<Amount>,
1671
1672        /// Exchange rates.
1673        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1674        #[cfg_attr(feature = "uniffi", uniffi(default))]
1675        pub exchanges: Vec<ExchangeRate>,
1676
1677        /// Any fees related to the transaction.
1678        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1679        #[cfg_attr(feature = "uniffi", uniffi(default))]
1680        pub fees: Vec<Fee>,
1681
1682        /// Creditor data. In case of reversals this refers to the initial transaction.
1683        #[serde(skip_serializing_if = "Option::is_none")]
1684        #[cfg_attr(feature = "uniffi", uniffi(default))]
1685        pub creditor: Option<Party>,
1686
1687        /// Debtor data. In case of reversals this refers to the initial transaction.
1688        #[serde(skip_serializing_if = "Option::is_none")]
1689        #[cfg_attr(feature = "uniffi", uniffi(default))]
1690        pub debtor: Option<Party>,
1691
1692        /// Remittance (purpose).
1693        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1694        #[cfg_attr(feature = "uniffi", uniffi(default))]
1695        pub remittance_information: Vec<String>,
1696
1697        /// ISO 20022 ExternalPurpose1Code.
1698        #[serde(skip_serializing_if = "Option::is_none")]
1699        #[cfg_attr(feature = "uniffi", uniffi(default))]
1700        pub purpose_code: Option<String>,
1701
1702        /// Bank Transaction Codes.
1703        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1704        #[cfg_attr(feature = "uniffi", uniffi(default))]
1705        pub bank_transaction_codes: Vec<BankTransactionCode>,
1706
1707        /// Additional information attached to the transaction.
1708        ///
1709        /// This might be a proprietary, localized, human-readable long text corresponding to some machine-readable bank transaction code that is not directly provided by the bank.
1710        #[serde(skip_serializing_if = "Option::is_none")]
1711        #[cfg_attr(feature = "uniffi", uniffi(default))]
1712        pub additional_information: Option<String>,
1713    }
1714
1715    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1716    #[serde(rename_all = "camelCase")]
1717    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1718    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1719    pub struct ExchangeRate {
1720        /// ISO 4217 Alpha 3 currency code of the source currency that gets converted.
1721        pub source_currency: String,
1722        /// ISO 4217 Alpha 3 currency code of the target currency that
1723        /// the source currency gets converted into.
1724        #[serde(skip_serializing_if = "Option::is_none")]
1725        #[cfg_attr(feature = "uniffi", uniffi(default))]
1726        pub target_currency: Option<String>,
1727        /// ISO 4217 Alpha 3 currency code of the unit currency for the exchange rate.
1728        #[serde(skip_serializing_if = "Option::is_none")]
1729        #[cfg_attr(feature = "uniffi", uniffi(default))]
1730        pub unit_currency: Option<String>,
1731        pub exchange_rate: Decimal,
1732    }
1733
1734    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1735    #[serde(rename_all = "camelCase")]
1736    #[cfg_attr(not(feature = "server"), non_exhaustive)]
1737    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1738    pub struct Party {
1739        /// Creditor / debtor name.
1740        #[serde(skip_serializing_if = "Option::is_none")]
1741        #[cfg_attr(feature = "uniffi", uniffi(default))]
1742        pub name: Option<String>,
1743
1744        /// ISO 20022 IBAN2007Identifier for the creditor / debtor account.
1745        #[serde(skip_serializing_if = "Option::is_none")]
1746        #[cfg_attr(feature = "uniffi", uniffi(default))]
1747        pub iban: Option<String>,
1748
1749        /// ISO 20022 BICFIIdentifier for the creditor / debtor agent.
1750        #[serde(skip_serializing_if = "Option::is_none")]
1751        #[cfg_attr(feature = "uniffi", uniffi(default))]
1752        pub bic: Option<String>,
1753
1754        /// Ultimate creditor / debtor (name).
1755        #[serde(skip_serializing_if = "Option::is_none")]
1756        #[cfg_attr(feature = "uniffi", uniffi(default))]
1757        pub ultimate: Option<String>,
1758    }
1759
1760    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1761    #[serde(rename_all = "camelCase")]
1762    #[serde(rename_all_fields = "camelCase")]
1763    #[non_exhaustive]
1764    #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1765    pub enum BankTransactionCode {
1766        /// ISO 20022 Bank Transaction Code.
1767        Iso {
1768            /// ISO 20022 ExternalBankTransactionDomain1Code.
1769            domain: String,
1770
1771            /// ISO 20022 ExternalBankTransactionFamily1Code.
1772            family: String,
1773
1774            /// ISO 20022 ExternalBankTransactionSubFamily1Code.
1775            sub_family: String,
1776        },
1777        /// SWIFT transaction code.
1778        Swift(String),
1779        /// BAI2 transaction code.
1780        Bai(String),
1781        /// National transaction code, e.g. German GVC.
1782        National { code: String, country: CountryCode },
1783        /// Unspecified transaction codes, possibly with an issuer information.
1784        Other {
1785            code: String,
1786            #[serde(skip_serializing_if = "Option::is_none")]
1787            issuer: Option<String>,
1788        },
1789    }
1790}
1791
1792#[allow(clippy::trivially_copy_pass_by_ref)]
1793fn is_false(val: &bool) -> bool {
1794    !*val
1795}
1796
1797#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
1798#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1799#[non_exhaustive]
1800pub enum PaymentStatus {
1801    /// The payment was received and is getting processed.
1802    ///
1803    /// Especially without realtime bookings this is often the final status reported by the ASPSP.
1804    Accepted,
1805
1806    /// The payment was partially accepted, i.e. only some of the transactions of a bulk payment or the payment needs further authorization.
1807    PartiallyAccepted,
1808
1809    /// Settlement on the debtor's account has been completed.
1810    CompletedDebtor,
1811
1812    /// Settlement on the creditor's account has been completed.
1813    CompletedCreditor,
1814}
1815
1816pub mod collect_payment {
1817    pub use routex_models::Amount;
1818    use serde::{Deserialize, Serialize};
1819    #[cfg(feature = "server")]
1820    use serde_with::base64::Base64;
1821
1822    #[cfg(not(feature = "server"))]
1823    pub use super::{
1824        AccountIdentifier as DebtorAccountIdentifier, AccountReference as DebtorAccountReference,
1825    };
1826    use super::{AccountIdentifier, Path, PaymentStatus, ServiceId};
1827
1828    /// Query ticket status, possibly polling pending confirmations
1829    ///
1830    /// Method: POST<br>
1831    /// Response: [`Option<TicketStatus<SuccessStatusData>>`](super::TicketStatus)
1832    pub fn status_path(ticket_id: &str) -> Path {
1833        Path::new(["collect-payment", "status", ticket_id])
1834    }
1835
1836    #[derive(Serialize, Deserialize, PartialEq, Eq, Default, Clone, Debug)]
1837    #[serde(rename_all = "camelCase")]
1838    #[non_exhaustive]
1839    pub struct SuccessStatusData {
1840        #[serde(skip_serializing_if = "Option::is_none")]
1841        pub payment_status: Option<PaymentStatus>,
1842    }
1843
1844    #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1845    #[serde(rename_all = "camelCase")]
1846    pub struct TicketData {
1847        pub amount: Amount,
1848        pub creditor_account: AccountIdentifier,
1849        pub creditor_name: String,
1850        pub remittance: String,
1851        #[serde(skip_serializing_if = "Option::is_none")]
1852        pub instant: Option<bool>,
1853        #[serde(skip_serializing_if = "Option::is_none")]
1854        pub fields: Option<Vec<Field>>,
1855    }
1856
1857    #[derive(Clone, Debug)]
1858    pub struct Service {}
1859
1860    impl super::Service for Service {
1861        const ID: ServiceId = ServiceId::CollectPayment;
1862
1863        type TicketData = TicketData;
1864
1865        type RequestData = RequestData;
1866
1867        type Output = PaymentInitiation;
1868    }
1869
1870    #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
1871    #[serde(rename_all = "camelCase")]
1872    #[non_exhaustive]
1873    #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1874    pub enum Field {
1875        DebtorIban,
1876        DebtorName,
1877    }
1878
1879    #[cfg(feature = "server")]
1880    #[serde_with::serde_as]
1881    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
1882    #[serde(rename_all = "camelCase")]
1883    #[serde(rename_all_fields = "camelCase")]
1884    #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1885    pub enum DebtorAccountIdentifier {
1886        /// ISO 20022 IBAN2007Identifier.
1887        #[serde(alias = "Iban")]
1888        Iban(String),
1889        EncryptedIban(#[serde_as(as = "Base64")] Vec<u8>),
1890    }
1891
1892    #[cfg(feature = "server")]
1893    #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1894    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1895    #[serde(rename_all = "camelCase")]
1896    pub struct DebtorAccountReference {
1897        #[serde(flatten)]
1898        pub id: DebtorAccountIdentifier,
1899        #[serde(skip_serializing_if = "Option::is_none")]
1900        #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1901        pub currency: Option<String>,
1902    }
1903
1904    #[derive(Serialize, Deserialize, Clone)]
1905    #[serde(rename_all = "camelCase")]
1906    pub struct RequestData {
1907        pub account: Option<DebtorAccountReference>,
1908    }
1909
1910    #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
1911    #[serde(rename_all = "camelCase")]
1912    #[non_exhaustive]
1913    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1914    pub struct PaymentInitiation {
1915        #[serde(skip_serializing_if = "Option::is_none")]
1916        pub status: Option<PaymentStatus>,
1917        #[serde(skip_serializing_if = "Option::is_none")]
1918        pub debtor_name: Option<String>,
1919        #[serde(skip_serializing_if = "Option::is_none")]
1920        pub debtor_iban: Option<String>,
1921    }
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926    use std::fmt::Debug;
1927
1928    use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
1929    use serde::{Deserialize, Serialize};
1930
1931    use crate::{
1932        Authenticated, Claims, ConfirmationContext, ConfirmationData, Dialog, DialogInput,
1933        InputContext, OBResult, Redirect, RedirectHandle, ResponseData, ServiceId, Session, Ticket,
1934    };
1935
1936    #[test]
1937    fn read_response() {
1938        let serialized = serde_json::to_string(
1939            &encode(
1940                &Header::new(Algorithm::HS256),
1941                &Claims {
1942                    data: "data",
1943                    exp: 2_540_808_000,
1944                },
1945                &EncodingKey::from_secret(b"does_not_matter"),
1946            )
1947            .unwrap(),
1948        )
1949        .unwrap();
1950
1951        let response = serde_json::from_str::<Authenticated<String>>(&serialized).unwrap();
1952
1953        assert_eq!(&super::decode::<String>(&response.jwt).unwrap(), "data");
1954    }
1955
1956    struct Service;
1957
1958    impl crate::Service for Service {
1959        const ID: ServiceId = ServiceId::Accounts;
1960
1961        type TicketData = ();
1962
1963        type RequestData = ();
1964
1965        type Output = ();
1966    }
1967
1968    #[allow(
1969        dead_code,
1970        unconditional_recursion,
1971        clippy::extra_unused_type_parameters
1972    )]
1973    fn test_bounds<T: Serialize + for<'de> Deserialize<'de> + Clone + Eq + PartialEq + Debug>() {
1974        test_bounds::<InputContext<Service>>();
1975        test_bounds::<ConfirmationContext<Service>>();
1976        test_bounds::<DialogInput<Service>>();
1977        test_bounds::<Dialog<Service>>();
1978        test_bounds::<Redirect<Service>>();
1979        test_bounds::<RedirectHandle<Service>>();
1980        test_bounds::<OBResult<()>>();
1981        test_bounds::<Session>();
1982
1983        test_bounds::<Authenticated<Ticket<Service>>>();
1984        test_bounds::<Ticket<Service>>();
1985        test_bounds::<ResponseData<Service>>();
1986        test_bounds::<ConfirmationData<Service>>();
1987    }
1988
1989    fn test_eq_<T: PartialEq + Debug>(o: &T) {
1990        assert!(o.eq(o));
1991    }
1992
1993    #[test]
1994    fn test_eq() {
1995        let ic = InputContext::<Service>::from(vec![]);
1996        let cc = ConfirmationContext::<Service>::from(vec![]);
1997
1998        test_eq_(&ic);
1999        test_eq_(&cc);
2000        test_eq_(&Redirect {
2001            url: "url:".parse().unwrap(),
2002            context: cc.clone(),
2003        });
2004        test_eq_(&RedirectHandle {
2005            handle: String::new(),
2006            context: cc.clone(),
2007        });
2008        test_eq_(&Ticket::<Service> {
2009            service: ServiceId::Accounts,
2010            id: String::new(),
2011            data: (),
2012        });
2013        test_eq_(&ResponseData {
2014            context: ic,
2015            response: String::new(),
2016        });
2017        test_eq_(&ConfirmationData { context: cc });
2018    }
2019}