taler-rust

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

payto.rs (19094B)


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