taler-rust

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

payto.rs (18996B)


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