libeufin

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

manual.rs (6350B)


      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::{
     21     fmt::Write as _,
     22     io::{BufWriter, Read, Write, stdin, stdout},
     23 };
     24 
     25 use anyhow::bail;
     26 use compact_str::CompactString;
     27 use jiff::{Timestamp, tz::TimeZone};
     28 use libeufin_ebics::ebics::rand_ebics_id;
     29 use sqlx::PgPool;
     30 use taler_macros::EnumMeta;
     31 use tracing::{error, info, warn};
     32 use zip::write::FileOptions;
     33 
     34 use crate::{
     35     batch_pain001,
     36     config::NexusCfg,
     37     db::initiated::{
     38         batch_initiated, batch_status_update, initiated_ack, initiated_submittable,
     39         tx_status_update,
     40     },
     41     fetch::register_camt,
     42     model::SubmissionState,
     43 };
     44 
     45 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumMeta)]
     46 #[enum_meta(Str)]
     47 #[allow(non_camel_case_types)]
     48 pub enum Kind {
     49     batch,
     50     tx,
     51 }
     52 
     53 /// Manual management commands
     54 #[derive(clap::Subcommand, Debug)]
     55 pub enum ManualCmd {
     56     /// Export pending batches as pain001 messages
     57     Export { out: CompactString },
     58     /// Import EBICS camt files
     59     Import { sources: Vec<CompactString> },
     60     Status {
     61         kind: Kind,
     62         id: CompactString,
     63         status: SubmissionState,
     64         msg: Option<CompactString>,
     65     },
     66     /// Manually acknowledge the outgoing transaction for submission
     67     Ack { ids: Vec<u64> },
     68 }
     69 
     70 impl ManualCmd {
     71     pub async fn run(self, db: &PgPool, cfg: &NexusCfg) -> anyhow::Result<()> {
     72         match self {
     73             ManualCmd::Export { out } => {
     74                 let out: &mut dyn Write = if out == "-" {
     75                     &mut stdout().lock()
     76                 } else {
     77                     &mut BufWriter::new(std::fs::File::create_new(out)?)
     78                 };
     79 
     80                 // Create and get pending batches
     81                 batch_initiated(
     82                     db,
     83                     &Timestamp::now(),
     84                     &rand_ebics_id(),
     85                     cfg.submit()?.require_ack,
     86                 )
     87                 .await?;
     88                 let mut zip = zip::ZipWriter::new_stream(&mut *out);
     89                 let batches = initiated_submittable(db, &cfg.currency).await?;
     90                 let mut metadata = String::new();
     91                 write!(&mut metadata, "Exported {} pain.001 files:", batches.len()).unwrap();
     92                 for batch in batches {
     93                     let datetime = batch.creation_date.to_zoned(TimeZone::UTC).datetime();
     94                     zip.start_file(
     95                         format!("{datetime}-{}.xml", batch.msg_id),
     96                         FileOptions::DEFAULT,
     97                     )?;
     98                     let xml = batch_pain001(&batch, cfg.ebics()?, Timestamp::now(), false)?;
     99                     zip.write_all(xml.as_bytes())?;
    100                     write!(&mut metadata, "\nbatch {}:", batch.msg_id)?;
    101                     for tx in batch.payments {
    102                         write!(
    103                             &mut metadata,
    104                             "\n- tx {} {} {} '{}'",
    105                             tx.e2e_id, tx.amount, tx.creditor.iban, tx.creditor.name
    106                         )?;
    107                     }
    108                     writeln!(&mut metadata)?;
    109                 }
    110                 zip.start_file("README.txt", FileOptions::DEFAULT)?;
    111                 zip.write_all(metadata.as_bytes())?;
    112                 zip.finish()?;
    113                 info!("{metadata}");
    114             }
    115             ManualCmd::Import { sources } => {
    116                 let mut nb_total = 0;
    117                 let mut nb_error = 0;
    118                 for source in sources {
    119                     let xml = if source == "-" {
    120                         let mut buf = Vec::with_capacity(8 * 1024);
    121                         stdin().lock().read_to_end(&mut buf)?;
    122                         buf
    123                     } else {
    124                         std::fs::read(&source)?
    125                     };
    126                     nb_total += 1;
    127                     match register_camt(db, cfg, &xml).await {
    128                         Ok(nb) => {
    129                             info!("Imported {nb} transactions from {source}")
    130                         }
    131                         Err(e) => {
    132                             error!("{source} {e}");
    133                             nb_error += 0
    134                         }
    135                     }
    136                 }
    137                 info!("Imported {nb_total} files");
    138                 if nb_error > 0 {
    139                     bail!("Failed to import {nb_error} files");
    140                 }
    141             }
    142             ManualCmd::Status {
    143                 kind,
    144                 id,
    145                 status,
    146                 msg,
    147             } => match kind {
    148                 Kind::batch => {
    149                     if batch_status_update(db, &id, status, msg.as_deref().unwrap_or_default())
    150                         .await?
    151                     {
    152                         info!("Updated batch '{id}' to {status}");
    153                     } else {
    154                         bail!("Unknown batch {id}")
    155                     }
    156                 }
    157                 Kind::tx => {
    158                     if tx_status_update(db, &id, "", status, msg.as_deref().unwrap_or_default())
    159                         .await?
    160                     {
    161                         info!("Updated tx '{id}' to {status}");
    162                     } else {
    163                         bail!("Unknown tx {id}")
    164                     }
    165                 }
    166             },
    167             ManualCmd::Ack { ids } => {
    168                 for id in ids {
    169                     if initiated_ack(db, id).await? {
    170                         info!("Mark {id} as acknowledge for submission");
    171                     } else {
    172                         warn!("Unknown transaction {id}");
    173                     }
    174                 }
    175             }
    176         }
    177         Ok(())
    178     }
    179 }