libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

payto.rs (6057B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use compact_str::CompactString;
     21 use sqlx::{Row as _, postgres::PgRow};
     22 use taler_api::{
     23     db::TypeHelper as _,
     24     error::{ApiResult, bad_request},
     25 };
     26 use taler_common::types::{
     27     iban::{BIC, IBAN},
     28     payto::{
     29         BankID, FullIbanPayto, FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI,
     30         TransferPayto,
     31     },
     32 };
     33 
     34 const X_TALER_BANK: &str = "x-taler-bank";
     35 
     36 #[derive(Debug, Clone, PartialEq, Eq)]
     37 pub struct XTalerBank {
     38     pub hostname: CompactString,
     39     pub username: CompactString,
     40 }
     41 
     42 impl PaytoImpl for XTalerBank {
     43     fn as_uri(&self) -> PaytoURI {
     44         PaytoURI::from_parts(
     45             X_TALER_BANK,
     46             format_args!("/{}/{}", self.hostname, self.username),
     47         )
     48     }
     49 
     50     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
     51         let url = raw.as_ref();
     52         if url.domain() != Some(X_TALER_BANK) {
     53             return Err(PaytoErr::UnsupportedKind(
     54                 X_TALER_BANK,
     55                 url.domain().unwrap_or_default().into(),
     56             ));
     57         }
     58         let Some(mut segments) = url.path_segments() else {
     59             return Err(PaytoErr::MissingSegment("hostname"));
     60         };
     61         let Some(hostname) = segments.next() else {
     62             return Err(PaytoErr::MissingSegment("hostname"));
     63         };
     64         let Some(username) = segments.next() else {
     65             return Err(PaytoErr::MissingSegment("username"));
     66         };
     67 
     68         Ok(Self {
     69             hostname: hostname.into(),
     70             username: username.into(),
     71         })
     72     }
     73 }
     74 
     75 #[derive(Debug, Clone, PartialEq, Eq)]
     76 pub enum LibeufinId {
     77     IBAN(BankID),
     78     XTalerBank(XTalerBank),
     79 }
     80 
     81 pub type BankPayto = Payto<LibeufinId>;
     82 pub type FullBankPayto = FullPayto<LibeufinId>;
     83 pub type TransferBankPayto = TransferPayto<LibeufinId>;
     84 
     85 impl LibeufinId {
     86     pub fn canonical(&self) -> String {
     87         match self {
     88             LibeufinId::IBAN(BankID { iban, .. }) => format!("payto://iban/{iban}"),
     89             LibeufinId::XTalerBank(XTalerBank { username, .. }) => {
     90                 format!("payto://{X_TALER_BANK}/localhost/{username}")
     91             }
     92         }
     93     }
     94 
     95     pub fn bank(mut self, ctx: &PaytoCtx) -> BankPayto {
     96         match &mut self {
     97             LibeufinId::IBAN(bank_id) => bank_id.bic = ctx.bic,
     98             LibeufinId::XTalerBank(xtaler_bank) => xtaler_bank.hostname = ctx.hostname.clone(),
     99         };
    100         Payto::new(self)
    101     }
    102 
    103     pub fn expect_iban(&self) -> ApiResult<&BankID> {
    104         match self {
    105             LibeufinId::IBAN(bank_id) => Ok(bank_id),
    106             LibeufinId::XTalerBank(_) => Err(bad_request(format_args!(
    107                 "expected an IBAN payto URI got '{X_TALER_BANK}'"
    108             ))),
    109         }
    110     }
    111 
    112     pub fn expect_xtaler_bank(&self) -> ApiResult<&XTalerBank> {
    113         match self {
    114             LibeufinId::IBAN(_) => Err(bad_request(format_args!(
    115                 "expected a {X_TALER_BANK} payto URI got 'iban'"
    116             ))),
    117             LibeufinId::XTalerBank(xtaler_bank) => Ok(xtaler_bank),
    118         }
    119     }
    120 }
    121 
    122 impl PaytoImpl for LibeufinId {
    123     fn as_uri(&self) -> PaytoURI {
    124         match self {
    125             LibeufinId::IBAN(bank_id) => bank_id.as_uri(),
    126             LibeufinId::XTalerBank(x_taler_bank) => x_taler_bank.as_uri(),
    127         }
    128     }
    129 
    130     fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr> {
    131         let url = uri.as_ref();
    132 
    133         Ok(if url.domain() == Some(X_TALER_BANK) {
    134             Self::XTalerBank(XTalerBank::parse(uri)?)
    135         } else {
    136             Self::IBAN(BankID::parse(uri)?)
    137         })
    138     }
    139 }
    140 
    141 impl From<BankID> for LibeufinId {
    142     fn from(value: BankID) -> Self {
    143         Self::IBAN(value)
    144     }
    145 }
    146 
    147 impl From<IBAN> for LibeufinId {
    148     fn from(value: IBAN) -> Self {
    149         Self::IBAN(BankID {
    150             iban: value,
    151             bic: None,
    152         })
    153     }
    154 }
    155 
    156 impl From<XTalerBank> for LibeufinId {
    157     fn from(value: XTalerBank) -> Self {
    158         Self::XTalerBank(value)
    159     }
    160 }
    161 
    162 pub struct PaytoCtx {
    163     pub bic: Option<BIC>,
    164     pub hostname: CompactString,
    165 }
    166 
    167 pub fn sql_bank_simple_payto(
    168     r: &PgRow,
    169     ctx: &PaytoCtx,
    170     payto_idx: &str,
    171 ) -> sqlx::Result<BankPayto> {
    172     let payto: BankPayto = r.try_get_parse(payto_idx)?;
    173     Ok(payto.into_inner().bank(ctx))
    174 }
    175 
    176 pub fn sql_bank_payto(
    177     r: &PgRow,
    178     ctx: &PaytoCtx,
    179     payto_idx: &str,
    180     name_idx: &str,
    181 ) -> sqlx::Result<FullBankPayto> {
    182     let payto: BankPayto = r.try_get_parse(payto_idx)?;
    183     let name = r.try_get(name_idx)?;
    184     let payto = payto.into_inner().bank(ctx);
    185     Ok(payto.into_inner().full(name))
    186 }
    187 
    188 pub fn sql_opt_bank_payto(
    189     r: &PgRow,
    190     ctx: &PaytoCtx,
    191     payto_idx: &str,
    192     name_idx: &str,
    193 ) -> sqlx::Result<Option<FullBankPayto>> {
    194     let payto: Option<BankPayto> = r.try_get_opt_parse(payto_idx)?;
    195     if let Some(payto) = payto {
    196         let name = r.try_get(name_idx)?;
    197         let payto = payto.into_inner().bank(ctx);
    198         Ok(Some(payto.into_inner().full(name)))
    199     } else {
    200         Ok(None)
    201     }
    202 }
    203 
    204 pub fn sql_opt_iban_payto(
    205     r: &PgRow,
    206     payto_idx: &str,
    207     name_idx: &str,
    208 ) -> sqlx::Result<Option<FullIbanPayto>> {
    209     let payto: Option<IbanPayto> = r.try_get_opt_parse(payto_idx)?;
    210     if let Some(payto) = payto {
    211         let name = r.try_get(name_idx)?;
    212         Ok(Some(FullPayto::new(payto.into_inner(), name)))
    213     } else {
    214         Ok(None)
    215     }
    216 }