list.rs (8773B)
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::{fmt::from_fn, io::Write as _, str::FromStr}; 21 22 use sqlx::PgPool; 23 use taler_common::types::{ 24 amount::{Currency, Decimal}, 25 payto::FullIbanPayto, 26 }; 27 28 use crate::db::list::{self, InMetadata, InitMetadata, InitMetadataAck, OutMetadata}; 29 30 fn fmt_payto(f: &mut std::fmt::Formatter, payto: &str) -> std::fmt::Result { 31 match FullIbanPayto::from_str(payto) { 32 Ok(p) => { 33 write!(f, "{}", p.iban)?; 34 if let Some(bic) = &p.bic { 35 write!(f, " {bic}")?; 36 } 37 // TODO handle missing name better 38 write!(f, " {}", p.name) 39 } 40 Err(_) => write!(f, "{}", payto.trim_start_matches("payto//")), 41 } 42 } 43 44 /// List nexus transactions 45 #[derive(clap::Subcommand, Debug)] 46 pub enum ListCmd { 47 /// List incoming transactions 48 Incoming { 49 /// Only list transactions that are incomplete 50 #[arg(long)] 51 incomplete: bool, 52 }, 53 /// List outgoing transactions 54 Outgoing, 55 /// List initiated transactions 56 Initiated { 57 /// Only list transactions awaiting manual acknowledgment 58 #[arg(long, alias("awaiting-ack"))] 59 ack: bool, 60 }, 61 } 62 63 impl ListCmd { 64 pub async fn run(self, db: &PgPool, currency: &Currency) -> anyhow::Result<()> { 65 match self { 66 ListCmd::Incoming { incomplete } => { 67 let txs = list::incoming(db, incomplete, currency).await?; 68 let out = &mut std::io::stdout().lock(); 69 for InMetadata { 70 id, 71 date, 72 amount, 73 credit_fee, 74 subject, 75 debtor, 76 talerable, 77 bounced, 78 } in txs 79 { 80 write!(out, "{date} {id} {amount}")?; 81 if let Some(credit_fee) = credit_fee 82 && credit_fee != Decimal::ZERO 83 { 84 write!(out, "-{credit_fee}")?; 85 } 86 writeln!(out)?; 87 if let Some(debtor) = debtor { 88 writeln!(out, " debtor: {}", from_fn(|f| fmt_payto(f, &debtor)))?; 89 } 90 if let Some(subject) = subject { 91 writeln!(out, " subject: {subject}")?; 92 } 93 if let Some(talerable) = talerable { 94 writeln!(out, " talerable: {talerable}")?; 95 } 96 if let Some(bounced) = bounced { 97 writeln!(out, " bounced: {bounced}")?; 98 } 99 writeln!(out)?; 100 } 101 } 102 ListCmd::Outgoing => { 103 let txs = list::outgoing(db, currency).await?; 104 let out = &mut std::io::stdout().lock(); 105 for OutMetadata { 106 id, 107 date, 108 amount, 109 subject, 110 creditor, 111 wtid, 112 exchange_base_url, 113 } in txs 114 { 115 writeln!(out, "{date} {id} {amount}")?; 116 if let Some(creditor) = creditor { 117 writeln!(out, " creditor: {}", from_fn(|f| fmt_payto(f, &creditor)))?; 118 } 119 if let Some(subject) = subject { 120 writeln!(out, " subject: {subject}")?; 121 } 122 if let Some(wtid) = wtid { 123 writeln!( 124 out, 125 " talerable: {wtid} {}", 126 exchange_base_url.unwrap_or_default() 127 )?; 128 } 129 writeln!(out)?; 130 } 131 } 132 ListCmd::Initiated { ack } => { 133 if ack { 134 let txs = list::initiated_ack(db, currency).await?; 135 let out = &mut std::io::stdout().lock(); 136 for InitMetadataAck { 137 date, 138 amount, 139 subject, 140 creditor, 141 db_id, 142 id, 143 } in txs 144 { 145 writeln!(out, "{date} {id} {amount}")?; 146 writeln!(out, " creditor: {}", from_fn(|f| fmt_payto(f, &creditor)))?; 147 writeln!(out, " subject: {subject}")?; 148 writeln!(out, " ack: {db_id}")?; 149 writeln!(out)?; 150 } 151 } else { 152 let txs = list::initiated(db, currency).await?; 153 let out = &mut std::io::stdout().lock(); 154 for InitMetadata { 155 date, 156 amount, 157 subject, 158 creditor, 159 id, 160 batch, 161 batch_order, 162 status, 163 msg, 164 submission_time, 165 submission_counter, 166 } in txs 167 { 168 writeln!(out, "{date} {id} {amount}")?; 169 writeln!(out, " creditor: {}", from_fn(|f| fmt_payto(f, &creditor)))?; 170 writeln!(out, " subject: {subject}")?; 171 if let Some(batch) = batch { 172 if let Some(order) = batch_order { 173 writeln!(out, " batch: {batch} {order}")?; 174 } else { 175 writeln!(out, " batch: {batch}")?; 176 } 177 } 178 if let Some(submission_time) = submission_time { 179 writeln!(out, " submission: {submission_time} {submission_counter}")?; 180 } 181 write!(out, " status: {status}")?; 182 if let Some(msg) = msg { 183 write!(out, "{msg}")?; 184 } 185 writeln!(out, "\n")?; 186 } 187 } 188 } 189 } 190 Ok(()) 191 } 192 } 193 194 #[cfg(test)] 195 mod test { 196 use sqlx::PgPool; 197 use taler_macros::db_test; 198 199 use crate::{ 200 list::ListCmd, 201 test::{ 202 CURR, incomplete_out, malformed_completeted_in, malformed_in, malformed_incomplete_in, 203 malformed_out, prepared_completeted_in, prepared_incomplete_in, 204 talerable_completeted_in, talerable_in, talerable_incomplete_in, talerable_kyc_in, 205 talerable_out, 206 }, 207 }; 208 209 #[db_test] 210 async fn check(db: PgPool) { 211 let check = async || { 212 for cmd in [ 213 ListCmd::Outgoing, 214 ListCmd::Incoming { incomplete: true }, 215 ListCmd::Incoming { incomplete: false }, 216 ListCmd::Initiated { ack: true }, 217 ListCmd::Initiated { ack: false }, 218 ] { 219 cmd.run(&db, &CURR).await.unwrap(); 220 } 221 }; 222 223 // Check empty 224 check().await; 225 226 // Check with transactions 227 malformed_in(&db).await; 228 malformed_out(&db).await; 229 check().await; 230 231 // Check with taler transactions 232 talerable_out(&db).await; 233 talerable_in(&db).await; 234 talerable_kyc_in(&db).await; 235 malformed_completeted_in(&db).await; 236 talerable_completeted_in(&db).await; 237 prepared_completeted_in(&db).await; 238 check().await; 239 240 // Check with incomplete 241 incomplete_out(&db).await; 242 prepared_incomplete_in(&db).await; 243 malformed_incomplete_in(&db).await; 244 talerable_incomplete_in(&db).await; 245 check().await; 246 } 247 }