taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

payto.rs (20694B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     fmt::{Debug, Display},
     19     ops::{Deref, DerefMut},
     20     str::FromStr,
     21 };
     22 
     23 use compact_str::CompactString;
     24 use serde::{Deserialize, Serialize, de::DeserializeOwned};
     25 use serde_with::{DeserializeFromStr, SerializeDisplay};
     26 use url::Url;
     27 
     28 use super::{
     29     amount::Amount,
     30     iban::{BIC, IBAN},
     31 };
     32 use crate::types::ach::{AccountNumber, RoutingNumber};
     33 
     34 /// Parse a payto URI, panic if malformed
     35 pub fn payto(url: impl AsRef<str>) -> PaytoURI {
     36     url.as_ref().parse().expect("invalid payto")
     37 }
     38 
     39 pub trait PaytoImpl: Sized {
     40     fn full(self, name: &str) -> FullPayto<Self> {
     41         FullPayto::new(self, name)
     42     }
     43 
     44     fn transfer(
     45         self,
     46         name: &str,
     47         amount: Option<Amount>,
     48         subject: Option<&str>,
     49     ) -> TransferPayto<Self> {
     50         TransferPayto::new(self, name, amount, subject)
     51     }
     52 
     53     fn as_uri(&self) -> PaytoURI;
     54     fn as_full_uri(&self, name: &str) -> PaytoURI {
     55         self.as_uri().as_full_payto(name)
     56     }
     57     fn as_transfer_uri(
     58         &self,
     59         name: &str,
     60         amount: Option<&Amount>,
     61         subject: Option<&str>,
     62     ) -> PaytoURI {
     63         self.as_uri().as_transfer_payto(name, amount, subject)
     64     }
     65     fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>;
     66 }
     67 
     68 /// A generic RFC 8905 payto URI
     69 #[derive(
     70     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     71 )]
     72 pub struct PaytoURI(Url);
     73 
     74 impl PaytoURI {
     75     pub unsafe fn from_raw(str: &str) -> Self {
     76         Self(Url::from_str(str).unwrap())
     77     }
     78 
     79     pub fn raw(&self) -> &str {
     80         self.0.as_str()
     81     }
     82 
     83     pub fn from_parts(domain: &str, path: impl Display) -> Self {
     84         payto(format!("payto://{domain}{path}"))
     85     }
     86 
     87     pub fn as_full_payto(self, name: &str) -> PaytoURI {
     88         self.with_query([("receiver-name", name)])
     89     }
     90 
     91     pub fn as_transfer_payto(
     92         self,
     93         name: &str,
     94         amount: Option<&Amount>,
     95         subject: Option<&str>,
     96     ) -> PaytoURI {
     97         self.as_full_payto(name)
     98             .with_query([("amount", amount)])
     99             .with_query([("message", subject)])
    100     }
    101 
    102     pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> {
    103         let query = self.0.query().unwrap_or_default().as_bytes();
    104         let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query));
    105         serde_path_to_error::deserialize(de).map_err(PaytoErr::Query)
    106     }
    107 
    108     fn with_query(mut self, query: impl Serialize) -> Self {
    109         let mut urlencoder = self.0.query_pairs_mut();
    110         query
    111             .serialize(serde_urlencoded::Serializer::new(&mut urlencoder))
    112             .unwrap();
    113         let _ = urlencoder.finish();
    114         drop(urlencoder);
    115         self
    116     }
    117 }
    118 
    119 impl AsRef<Url> for PaytoURI {
    120     fn as_ref(&self) -> &Url {
    121         &self.0
    122     }
    123 }
    124 
    125 impl std::fmt::Display for PaytoURI {
    126     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    127         std::fmt::Display::fmt(self.raw(), f)
    128     }
    129 }
    130 
    131 #[derive(Debug, thiserror::Error)]
    132 pub enum PaytoErr {
    133     #[error("invalid payto URI: {0}")]
    134     Url(#[from] url::ParseError),
    135     #[error("malformed payto URI query: {0}")]
    136     Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>),
    137     #[error("expected a payto URI got {0}")]
    138     NotPayto(CompactString),
    139     #[error("unsupported payto kind, expected {0} got {1}")]
    140     UnsupportedKind(&'static str, CompactString),
    141     #[error("to many path segments for a {0} payto uri")]
    142     TooLong(&'static str),
    143     #[error("missing segment {0} in path")]
    144     MissingSegment(&'static str),
    145     #[error("malformed segment {0}: {1}")]
    146     MalformedSegment(
    147         &'static str,
    148         Box<dyn std::error::Error + Sync + Send + 'static>,
    149     ),
    150 }
    151 
    152 impl PaytoErr {
    153     pub fn malformed_segment<E: std::error::Error + Sync + Send + 'static>(
    154         segment: &'static str,
    155         e: E,
    156     ) -> Self {
    157         Self::MalformedSegment(segment, Box::new(e))
    158     }
    159 }
    160 
    161 impl FromStr for PaytoURI {
    162     type Err = PaytoErr;
    163 
    164     fn from_str(s: &str) -> Result<Self, Self::Err> {
    165         // Parse url
    166         let url: Url = s.parse()?;
    167         // Check scheme
    168         if url.scheme() != "payto" {
    169             return Err(PaytoErr::NotPayto(url.scheme().into()));
    170         }
    171         Ok(Self(url))
    172     }
    173 }
    174 
    175 pub type IbanPayto = Payto<BankID>;
    176 pub type FullIbanPayto = FullPayto<BankID>;
    177 pub type TransferIbanPayto = TransferPayto<BankID>;
    178 
    179 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    180 pub struct BankID {
    181     pub iban: IBAN,
    182     pub bic: Option<BIC>,
    183 }
    184 
    185 const IBAN: &str = "iban";
    186 
    187 impl PaytoImpl for BankID {
    188     fn as_uri(&self) -> PaytoURI {
    189         PaytoURI::from_parts(
    190             IBAN,
    191             format_args!(
    192                 "/{}",
    193                 std::fmt::from_fn(|f| {
    194                     if let Some(bic) = &self.bic {
    195                         write!(f, "{bic}/")?;
    196                     }
    197                     write!(f, "{}", self.iban)
    198                 })
    199             ),
    200         )
    201     }
    202 
    203     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    204         let url = raw.as_ref();
    205         if url.domain() != Some(IBAN) {
    206             return Err(PaytoErr::UnsupportedKind(
    207                 IBAN,
    208                 url.domain().unwrap_or_default().into(),
    209             ));
    210         }
    211         let Some(mut segments) = url.path_segments() else {
    212             return Err(PaytoErr::MissingSegment(IBAN));
    213         };
    214         let Some(first) = segments.next() else {
    215             return Err(PaytoErr::MissingSegment(IBAN));
    216         };
    217         let (iban, bic) = match segments.next() {
    218             Some(second) => (second, Some(first)),
    219             None => (first, None),
    220         };
    221 
    222         Ok(Self {
    223             iban: iban
    224                 .parse()
    225                 .map_err(|e| PaytoErr::malformed_segment(IBAN, e))?,
    226             bic: bic
    227                 .map(|bic| {
    228                     bic.parse()
    229                         .map_err(|e| PaytoErr::malformed_segment("bic", e))
    230                 })
    231                 .transpose()?,
    232         })
    233     }
    234 }
    235 
    236 impl PaytoImpl for IBAN {
    237     fn as_uri(&self) -> PaytoURI {
    238         PaytoURI::from_parts(IBAN, format_args!("/{self}"))
    239     }
    240 
    241     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    242         raw.as_ref().path_segments().unwrap_or("".split('/'));
    243         let payto = BankID::parse(raw)?;
    244         Ok(payto.iban)
    245     }
    246 }
    247 
    248 pub type AchPayto = Payto<ACH>;
    249 pub type FullAchPayto = FullPayto<ACH>;
    250 pub type TransferAchPayto = TransferPayto<ACH>;
    251 
    252 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    253 pub struct ACH {
    254     pub routing_number: RoutingNumber,
    255     pub account_number: AccountNumber,
    256 }
    257 
    258 const ACH: &str = "ach";
    259 
    260 impl PaytoImpl for ACH {
    261     fn as_uri(&self) -> PaytoURI {
    262         PaytoURI::from_parts(
    263             ACH,
    264             format_args!("/{}/{}", self.routing_number, self.account_number),
    265         )
    266     }
    267 
    268     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    269         let url = raw.as_ref();
    270         if url.domain() != Some(ACH) {
    271             return Err(PaytoErr::UnsupportedKind(
    272                 ACH,
    273                 url.domain().unwrap_or_default().into(),
    274             ));
    275         }
    276         let Some(mut segments) = url.path_segments() else {
    277             return Err(PaytoErr::MissingSegment("routing number"));
    278         };
    279         let Some(routing_number) = segments.next() else {
    280             return Err(PaytoErr::MissingSegment("routing number"));
    281         };
    282         let Some(account_number) = segments.next() else {
    283             return Err(PaytoErr::MissingSegment("account number"));
    284         };
    285         Ok(Self {
    286             routing_number: routing_number
    287                 .parse()
    288                 .map_err(|e| PaytoErr::malformed_segment("routing number", e))?,
    289             account_number: account_number
    290                 .parse()
    291                 .map_err(|e| PaytoErr::malformed_segment("account number", e))?,
    292         })
    293     }
    294 }
    295 
    296 /// Full payto query
    297 #[derive(Debug, Clone, Deserialize)]
    298 pub struct FullQuery {
    299     #[serde(rename = "receiver-name")]
    300     receiver_name: CompactString,
    301 }
    302 
    303 /// Transfer payto query
    304 #[derive(Debug, Clone, Deserialize)]
    305 pub struct TransferQuery {
    306     #[serde(rename = "receiver-name")]
    307     receiver_name: CompactString,
    308     amount: Option<Amount>,
    309     message: Option<CompactString>,
    310 }
    311 
    312 /// Parsed payto query
    313 #[derive(Debug, Clone, Deserialize)]
    314 pub struct ParsedQuery {
    315     #[serde(rename = "receiver-name")]
    316     receiver_name: Option<CompactString>,
    317     amount: Option<Amount>,
    318     message: Option<CompactString>,
    319     #[serde(rename = "ch-qrr")]
    320     ch_qrr: Option<CompactString>,
    321 }
    322 
    323 #[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    324 pub struct Payto<P> {
    325     inner: P,
    326 }
    327 
    328 impl<P> Payto<P> {
    329     pub fn convert<T: From<P>>(self) -> Payto<T> {
    330         Payto {
    331             inner: self.inner.into(),
    332         }
    333     }
    334 }
    335 
    336 impl<P: PaytoImpl> Payto<P> {
    337     pub fn new(inner: P) -> Self {
    338         Self { inner }
    339     }
    340 
    341     pub fn as_uri(&self) -> PaytoURI {
    342         self.inner.as_uri()
    343     }
    344 
    345     pub fn into_inner(self) -> P {
    346         self.inner
    347     }
    348 }
    349 
    350 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> {
    351     type Error = PaytoErr;
    352 
    353     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    354         Ok(Self::new(P::parse(value)?))
    355     }
    356 }
    357 
    358 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> {
    359     fn from(value: FullPayto<P>) -> Payto<P> {
    360         Self::new(value.inner)
    361     }
    362 }
    363 
    364 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> {
    365     fn from(value: TransferPayto<P>) -> Payto<P> {
    366         Self::new(value.inner)
    367     }
    368 }
    369 
    370 impl<P: PaytoImpl> std::fmt::Display for Payto<P> {
    371     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    372         std::fmt::Display::fmt(&self.as_uri(), f)
    373     }
    374 }
    375 
    376 impl<P: PaytoImpl> FromStr for Payto<P> {
    377     type Err = PaytoErr;
    378 
    379     fn from_str(s: &str) -> Result<Self, Self::Err> {
    380         let payto: PaytoURI = s.parse()?;
    381         Self::try_from(&payto)
    382     }
    383 }
    384 
    385 impl<P: PaytoImpl> Deref for Payto<P> {
    386     type Target = P;
    387 
    388     fn deref(&self) -> &Self::Target {
    389         &self.inner
    390     }
    391 }
    392 
    393 impl<P: PaytoImpl> DerefMut for Payto<P> {
    394     fn deref_mut(&mut self) -> &mut Self::Target {
    395         &mut self.inner
    396     }
    397 }
    398 
    399 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    400 pub struct FullPayto<P> {
    401     inner: P,
    402     pub name: CompactString,
    403 }
    404 
    405 impl<P: PaytoImpl> FullPayto<P> {
    406     pub fn new(inner: P, name: &str) -> Self {
    407         Self {
    408             inner,
    409             name: CompactString::new(name),
    410         }
    411     }
    412 
    413     pub fn as_uri(&self) -> PaytoURI {
    414         self.inner.as_full_uri(&self.name)
    415     }
    416 
    417     pub fn into_inner(self) -> (P, CompactString) {
    418         (self.inner, self.name)
    419     }
    420 }
    421 
    422 impl<P> FullPayto<P> {
    423     pub fn convert<T: From<P>>(self) -> FullPayto<T> {
    424         FullPayto {
    425             inner: self.inner.into(),
    426             name: self.name,
    427         }
    428     }
    429 }
    430 
    431 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> {
    432     type Error = PaytoErr;
    433 
    434     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    435         let payto = P::parse(value)?;
    436         let query: FullQuery = value.query()?;
    437         Ok(Self {
    438             inner: payto,
    439             name: query.receiver_name,
    440         })
    441     }
    442 }
    443 
    444 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> {
    445     fn from(value: TransferPayto<P>) -> FullPayto<P> {
    446         FullPayto {
    447             inner: value.inner,
    448             name: value.name,
    449         }
    450     }
    451 }
    452 
    453 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> {
    454     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    455         std::fmt::Display::fmt(&self.as_uri(), f)
    456     }
    457 }
    458 
    459 impl<P: PaytoImpl> FromStr for FullPayto<P> {
    460     type Err = PaytoErr;
    461 
    462     fn from_str(s: &str) -> Result<Self, Self::Err> {
    463         let raw: PaytoURI = s.parse()?;
    464         Self::try_from(&raw)
    465     }
    466 }
    467 
    468 impl<P: PaytoImpl> Deref for FullPayto<P> {
    469     type Target = P;
    470 
    471     fn deref(&self) -> &Self::Target {
    472         &self.inner
    473     }
    474 }
    475 
    476 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    477 pub struct TransferPayto<P> {
    478     inner: P,
    479     pub name: CompactString,
    480     pub amount: Option<Amount>,
    481     pub subject: Option<CompactString>,
    482 }
    483 
    484 impl<P: PaytoImpl> TransferPayto<P> {
    485     pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self {
    486         Self {
    487             inner,
    488             name: CompactString::new(name),
    489             amount,
    490             subject: subject.map(CompactString::new),
    491         }
    492     }
    493 
    494     pub fn as_uri(&self) -> PaytoURI {
    495         self.inner
    496             .as_transfer_uri(&self.name, self.amount.as_ref(), self.subject.as_deref())
    497     }
    498 
    499     pub fn into_inner(self) -> P {
    500         self.inner
    501     }
    502 }
    503 
    504 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> {
    505     type Error = PaytoErr;
    506 
    507     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    508         let payto = P::parse(value)?;
    509         let query: TransferQuery = value.query()?;
    510         Ok(Self {
    511             inner: payto,
    512             name: query.receiver_name,
    513             amount: query.amount,
    514             subject: query.message,
    515         })
    516     }
    517 }
    518 
    519 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> {
    520     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    521         std::fmt::Display::fmt(&self.as_uri(), f)
    522     }
    523 }
    524 
    525 impl<P: PaytoImpl> FromStr for TransferPayto<P> {
    526     type Err = PaytoErr;
    527 
    528     fn from_str(s: &str) -> Result<Self, Self::Err> {
    529         let raw: PaytoURI = s.parse()?;
    530         Self::try_from(&raw)
    531     }
    532 }
    533 
    534 impl<P: PaytoImpl> Deref for TransferPayto<P> {
    535     type Target = P;
    536 
    537     fn deref(&self) -> &Self::Target {
    538         &self.inner
    539     }
    540 }
    541 
    542 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    543 pub struct ParsedPayto<P> {
    544     inner: P,
    545     pub name: Option<CompactString>,
    546     pub amount: Option<Amount>,
    547     pub subject: Option<CompactString>,
    548     pub ch_qrr: Option<CompactString>,
    549 }
    550 
    551 impl<P: PaytoImpl> ParsedPayto<P> {
    552     pub fn new(
    553         inner: P,
    554         name: Option<&str>,
    555         amount: Option<Amount>,
    556         subject: Option<&str>,
    557         ch_qrr: Option<&str>,
    558     ) -> Self {
    559         Self {
    560             inner,
    561             name: name.map(CompactString::new),
    562             amount,
    563             subject: subject.map(CompactString::new),
    564             ch_qrr: ch_qrr.map(CompactString::new),
    565         }
    566     }
    567 
    568     pub fn as_uri(&self) -> PaytoURI {
    569         self.inner
    570             .as_uri()
    571             .with_query([("receiver-name", &self.name)])
    572             .with_query([("amount", self.amount)])
    573             .with_query([("message", &self.subject)])
    574             .with_query([("ch-qrr", &self.ch_qrr)])
    575     }
    576 
    577     pub fn into_inner(self) -> P {
    578         self.inner
    579     }
    580 }
    581 
    582 impl<P: PaytoImpl> TryFrom<&PaytoURI> for ParsedPayto<P> {
    583     type Error = PaytoErr;
    584 
    585     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    586         let payto = P::parse(value)?;
    587         let query: ParsedQuery = value.query()?;
    588         Ok(Self {
    589             inner: payto,
    590             name: query.receiver_name,
    591             amount: query.amount,
    592             subject: query.message,
    593             ch_qrr: query.ch_qrr,
    594         })
    595     }
    596 }
    597 
    598 impl<P: PaytoImpl> std::fmt::Display for ParsedPayto<P> {
    599     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    600         std::fmt::Display::fmt(&self.as_uri(), f)
    601     }
    602 }
    603 
    604 impl<P: PaytoImpl> FromStr for ParsedPayto<P> {
    605     type Err = PaytoErr;
    606 
    607     fn from_str(s: &str) -> Result<Self, Self::Err> {
    608         let raw: PaytoURI = s.parse()?;
    609         Self::try_from(&raw)
    610     }
    611 }
    612 
    613 impl<P: PaytoImpl> Deref for ParsedPayto<P> {
    614     type Target = P;
    615 
    616     fn deref(&self) -> &Self::Target {
    617         &self.inner
    618     }
    619 }
    620 
    621 #[cfg(test)]
    622 mod test {
    623     use std::str::FromStr as _;
    624 
    625     use crate::types::{
    626         amount::amount,
    627         iban::IBAN,
    628         payto::{FullPayto, ParsedPayto, Payto, TransferPayto},
    629     };
    630 
    631     #[test]
    632     pub fn parse() {
    633         let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap();
    634 
    635         // Simple payto
    636         let simple_payto = Payto::new(iban);
    637         assert_eq!(
    638             simple_payto,
    639             Payto::from_str(&format!("payto://iban/{iban}")).unwrap()
    640         );
    641         assert_eq!(
    642             simple_payto,
    643             Payto::try_from(&simple_payto.as_uri()).unwrap()
    644         );
    645         assert_eq!(
    646             simple_payto,
    647             Payto::from_str(&simple_payto.as_uri().to_string()).unwrap()
    648         );
    649 
    650         // Full payto
    651         let full_payto = FullPayto::new(iban, "John Smith");
    652         assert_eq!(
    653             full_payto,
    654             FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap()
    655         );
    656         assert_eq!(
    657             full_payto,
    658             FullPayto::try_from(&full_payto.as_uri()).unwrap()
    659         );
    660         assert_eq!(
    661             full_payto,
    662             FullPayto::from_str(&full_payto.as_uri().to_string()).unwrap()
    663         );
    664         assert_eq!(simple_payto, full_payto.clone().into());
    665 
    666         // Transfer simple payto
    667         let transfer_payto = TransferPayto::new(iban, "John Smith", None, None);
    668         assert_eq!(
    669             transfer_payto,
    670             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith"))
    671                 .unwrap()
    672         );
    673         assert_eq!(
    674             transfer_payto,
    675             TransferPayto::try_from(&transfer_payto.as_uri()).unwrap()
    676         );
    677         assert_eq!(
    678             transfer_payto,
    679             TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap()
    680         );
    681         assert_eq!(full_payto, transfer_payto.clone().into());
    682 
    683         // Transfer full payto
    684         let transfer_payto = TransferPayto::new(
    685             iban,
    686             "John Smith",
    687             Some(amount("EUR:12")),
    688             Some("Wire transfer subject"),
    689         );
    690         assert_eq!(
    691             transfer_payto,
    692             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject"))
    693                 .unwrap()
    694         );
    695         assert_eq!(
    696             transfer_payto,
    697             TransferPayto::try_from(&transfer_payto.as_uri()).unwrap()
    698         );
    699         assert_eq!(
    700             transfer_payto,
    701             TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap()
    702         );
    703         assert_eq!(full_payto, transfer_payto.clone().into());
    704 
    705         // Parsed simple payto
    706         let transfer_payto = ParsedPayto::new(iban, None, None, None, None);
    707         assert_eq!(
    708             transfer_payto,
    709             ParsedPayto::from_str(&format!("payto://iban/{iban}")).unwrap()
    710         );
    711         assert_eq!(
    712             transfer_payto,
    713             ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap()
    714         );
    715         assert_eq!(
    716             transfer_payto,
    717             ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap()
    718         );
    719 
    720         // Parsed full payto
    721         let transfer_payto = ParsedPayto::new(
    722             iban,
    723             Some("John Smith"),
    724             Some(amount("EUR:12")),
    725             Some("Wire transfer subject"),
    726             Some("REFERENCE"),
    727         );
    728         assert_eq!(
    729             transfer_payto,
    730             ParsedPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject&ch-qrr=REFERENCE"))
    731                 .unwrap()
    732         );
    733         assert_eq!(
    734             transfer_payto,
    735             ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap()
    736         );
    737         assert_eq!(
    738             transfer_payto,
    739             ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap()
    740         );
    741 
    742         // Malformed
    743         let malformed = FullPayto::<IBAN>::from_str(
    744             "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA",
    745         )
    746         .unwrap();
    747         assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557");
    748         assert_eq!(malformed.name, "NYM Technologies%SA");
    749     }
    750 }