libeufin

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

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 }