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 }