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 }