testing.rs (11855B)
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 std::io::{Cursor, Read}; 21 22 use anyhow::{anyhow, bail}; 23 use compact_str::CompactString; 24 use jiff::{Timestamp, civil::Date, tz::TimeZone}; 25 use libeufin_ebics::{ 26 ebics::{ 27 EbicsErrKind, 28 order::{BTF, Order, OrderDoc}, 29 tx_check, 30 }, 31 iso20022::model::{InId, InTx}, 32 ws::listen_for_notification, 33 }; 34 use taler_common::{ 35 config::Config, 36 types::{ 37 amount::Amount, 38 iban::{Country, IBAN}, 39 payto::{BankID, ParsedPayto, PaytoImpl}, 40 }, 41 }; 42 use tracing::debug; 43 44 use crate::{ 45 EbicsClient, EbicsLogs, config::NexusCfg, db::pool, fetch::register_incoming, list::ListCmd, 46 rand_ebics_id, 47 }; 48 49 #[derive(clap::Subcommand, Debug)] 50 pub enum IbanCmd { 51 /// Generate fake IBANs for testing 52 Gen { 53 #[arg(long)] 54 country: Country, 55 }, 56 } 57 58 impl IbanCmd { 59 pub fn run(self) -> anyhow::Result<()> { 60 match self { 61 IbanCmd::Gen { country } => { 62 println!("{}", IBAN::random(country)) 63 } 64 } 65 Ok(()) 66 } 67 } 68 69 /// Testing helper commands 70 #[derive(clap::Subcommand, Debug)] 71 pub enum TestingCmd { 72 /// List incoming transactions 73 #[clap(subcommand)] 74 Iban(IbanCmd), 75 /// Genere a fake incoming payment 76 FakeIncoming { 77 /// The amount to transfer, payto 'amount' parameter takes the precedence 78 #[arg(long)] 79 amount: Option<Amount>, 80 81 /// The payment credit fee 82 #[arg(long)] 83 credit_fee: Option<Amount>, 84 85 /// The payment subject, payto 'message' parameter takes the precedence 86 #[arg(long)] 87 subject: Option<CompactString>, 88 89 /// The payment reference, credit-payto 'ch-qrr' parameter takes precedence 90 #[arg(long)] 91 ch_qrr: Option<CompactString>, 92 93 /// The credited account IBAN payto URI 94 #[arg(long)] 95 credit_payto: Option<ParsedPayto<BankID>>, 96 97 /// The debited account IBAN payto URI 98 #[arg(long)] 99 debit_payto: Option<ParsedPayto<BankID>>, 100 101 /// The debited account IBAN payto URI 102 payto: Option<ParsedPayto<BankID>>, 103 }, 104 #[clap(subcommand)] 105 List(ListCmd), 106 /// Perform EBICS requests 107 EbicsBtd { 108 #[arg(long("type"), default_value_t = CompactString::const_new("BTD"))] 109 ty: CompactString, 110 #[arg(long)] 111 name: CompactString, 112 #[arg(long)] 113 scope: Option<CompactString>, 114 #[arg(long)] 115 message_name: CompactString, 116 #[arg(long)] 117 message_version: Option<CompactString>, 118 #[arg(long)] 119 container: Option<CompactString>, 120 #[arg(long)] 121 option: Option<CompactString>, 122 #[clap(flatten)] 123 logs: EbicsLogs, 124 /// Erliest timestamp of the downloaded documents 125 #[arg(long, value_name = "YYYY-MM-DD")] 126 pinned_start: Option<Date>, 127 /// Do not consume fetched documents 128 #[arg(long)] 129 peek: bool, 130 #[arg(long)] 131 dry_run: bool, 132 }, 133 /// Check transaction semantic 134 TxCheck { 135 #[clap(flatten)] 136 logs: EbicsLogs, 137 }, 138 /// Listen to EBICS instant notification over websocket 139 Wss { 140 #[clap(flatten)] 141 logs: EbicsLogs, 142 }, 143 } 144 145 impl TestingCmd { 146 pub async fn run(self, cfg: &Config) -> anyhow::Result<()> { 147 match self { 148 TestingCmd::Iban(cmd) => cmd.run()?, 149 TestingCmd::FakeIncoming { 150 amount, 151 credit_fee, 152 subject, 153 ch_qrr, 154 credit_payto, 155 debit_payto, 156 payto, 157 } => { 158 let cfg = NexusCfg::parse(cfg)?; 159 let ebics_cfg = cfg.ebics()?; 160 let db = pool(&cfg.db_cfg).await?; 161 let amount = credit_payto 162 .as_ref() 163 .and_then(|p| p.amount) 164 .or_else(|| payto.as_ref().and_then(|p| p.amount)) 165 .or(amount) 166 .ok_or(anyhow!("Missing amount"))?; 167 let subject = credit_payto 168 .as_ref() 169 .and_then(|p| p.subject.clone()) 170 .or_else(|| payto.as_ref().and_then(|p| p.subject.clone())) 171 .or_else(|| subject.clone()); 172 let reference = credit_payto 173 .as_ref() 174 .and_then(|p| p.ch_qrr.clone()) 175 .or_else(|| payto.as_ref().and_then(|p| p.ch_qrr.clone())) 176 .or_else(|| ch_qrr.clone()); 177 178 if cfg.currency != amount.currency { 179 bail!( 180 "Wrong currency: expected {} got {}", 181 cfg.currency, 182 amount.currency 183 ); 184 } 185 186 if let Some(ref credit_payto) = credit_payto { 187 if reference.is_some() { 188 if let Some(qr_iban) = ebics_cfg.qr_iban { 189 if credit_payto.iban != qr_iban { 190 bail!( 191 "Creditor must be the exchange QRR account expected {qr_iban} got {}", 192 credit_payto.iban 193 ) 194 } 195 } else { 196 bail!("QR reference is not supported without a configured QR IBAN") 197 } 198 } else { 199 if credit_payto.iban != ebics_cfg.account.iban { 200 bail!( 201 "Creditor must be the exchange expected {} got {}", 202 ebics_cfg.account.iban, 203 credit_payto.iban 204 ) 205 } 206 } 207 } 208 209 register_incoming( 210 &db, 211 &cfg.ingest()?, 212 &InTx { 213 id: InId::new(None, Some(rand_ebics_id()), None), 214 amount, 215 credit_fee: credit_fee.unwrap_or(Amount::zero(&cfg.currency)), 216 subject: reference.or(subject).map(|it| it.into_string()), 217 execution_time: Timestamp::now(), 218 debtor: Some( 219 debit_payto 220 .map(|it| it.as_uri()) 221 .or(payto.map(|it| it.as_uri())) 222 .unwrap_or_else(|| { 223 IBAN::random(ebics_cfg.account.iban.country()) 224 .as_full_uri("Testing Account") 225 }), 226 ), 227 }, 228 ) 229 .await?; 230 } 231 TestingCmd::List(list_cmd) => { 232 let cfg = NexusCfg::parse(cfg)?; 233 let db = pool(&cfg.db_cfg).await?; 234 list_cmd.run(&db, &cfg.currency).await?; 235 } 236 TestingCmd::EbicsBtd { 237 ty, 238 name, 239 scope, 240 message_name, 241 message_version, 242 container, 243 option, 244 logs, 245 pinned_start, 246 peek, 247 dry_run, 248 } => { 249 let cfg = NexusCfg::parse(cfg)?; 250 let db = pool(&cfg.db_cfg).await?; 251 let order = Order::from_parts( 252 &ty, 253 Some(BTF { 254 service: name, 255 scope, 256 option, 257 container: container.clone(), 258 msg: message_name, 259 version: message_version, 260 }), 261 ) 262 .ok_or(anyhow!("Unknown ebics order"))?; 263 let (client, bank) = cfg.expect_full_keys()?; 264 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?; 265 ebics 266 .download( 267 &db, 268 &client, 269 &bank, 270 &order, 271 &pinned_start.map(|date| { 272 ( 273 date.to_zoned(TimeZone::UTC).unwrap().timestamp(), 274 Timestamp::now(), 275 ) 276 }), 277 peek, 278 async move |content| { 279 if container.as_deref() == Some("ZIP") { 280 let mut z = zip::ZipArchive::new(Cursor::new(content))?; 281 for i in 0..z.len() { 282 let mut file = z.by_index(i)?; 283 let mut buf = Vec::new(); 284 file.read_to_end(&mut buf)?; 285 println!("{}\n{}", file.name(), String::from_utf8_lossy(&buf)); 286 } 287 } else { 288 println!("{}", String::from_utf8_lossy(&content)); 289 } 290 if dry_run { 291 Err(EbicsErrKind::Custom("dry run".into())) 292 } else { 293 Ok(()) 294 } 295 }, 296 ) 297 .await?; 298 } 299 TestingCmd::TxCheck { logs } => { 300 let cfg = NexusCfg::parse(cfg)?; 301 let db = pool(&cfg.db_cfg).await?; 302 let (client, bank) = cfg.expect_full_keys()?; 303 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?; 304 let dialect = cfg.ebics()?.dialect.standard(); 305 let res = tx_check( 306 &ebics, 307 &db, 308 &client, 309 &bank, 310 &dialect.downloads(&OrderDoc::acknowledgement)[0], 311 &dialect.direct_debit(), 312 ) 313 .await?; 314 println!("{res:?}") 315 } 316 TestingCmd::Wss { logs } => { 317 let cfg = NexusCfg::parse(cfg)?; 318 let db = pool(&cfg.db_cfg).await?; 319 let (client, bank) = cfg.expect_full_keys()?; 320 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?; 321 let (sender, mut receiver) = tokio::sync::mpsc::channel(10); 322 tokio::join!( 323 listen_for_notification(&ebics, &db, &client, &bank, sender), 324 async { 325 while let Some(orders) = receiver.recv().await { 326 debug!(target: "testing", "{orders:?}") 327 } 328 } 329 ); 330 } 331 } 332 Ok(()) 333 } 334 }