libeufin

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

commit 50720217452d49dd88ae5992bc4a08f0732cd69f
parent 81b9419c3a46a9e5f8a1d7ce3982ac0e1bb478d1
Author: Antoine A <>
Date:   Fri, 24 Apr 2026 10:37:59 +0200

nexus: add camt parser

Diffstat:
MCargo.lock | 117++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
MCargo.toml | 4++--
Msrc/api.rs | 21++++++++++++---------
Msrc/bin/iso20022-codegen.rs | 28+++++++++-------------------
Msrc/config.rs | 14+++++++++-----
Msrc/crypto.rs | 10++++++----
Msrc/db.rs | 22+++++++++++-----------
Msrc/db/exchange.rs | 2+-
Msrc/db/initiated.rs | 18+++++++++---------
Msrc/db/payment.rs | 139+++++++++++++++++++++++++++++++------------------------------------------------
Msrc/ebics_code.rs | 139++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/iso20022/bank_tx_code.rs | 383++-----------------------------------------------------------------------------
Asrc/iso20022/camt.rs | 1230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/iso20022/external_code.rs | 1826++++++++++++++++++++-----------------------------------------------------------
Msrc/iso20022/mod.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/key_management.rs | 59++++++++++++++++++++++++++++-------------------------------
Msrc/lib.rs | 24++++++++++--------------
Msrc/model.rs | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/worker.rs | 18++++++++----------
Msrc/xml.rs | 270++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtestbench/sample/postfinance/camt054_P_CH2909000000250094239_1111111119_0_2022030911011199_v2019.xml | 2+-
21 files changed, 2486 insertions(+), 2182 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -308,9 +308,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -732,6 +732,12 @@ dependencies = [ ] [[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1074,9 +1080,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hashlink" @@ -1375,12 +1381,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1519,9 +1525,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -1580,6 +1586,7 @@ dependencies = [ "getrandom 0.4.2", "jiff", "pem", + "pretty_assertions", "rand 0.10.0", "rcgen", "reedline", @@ -1589,11 +1596,10 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum 0.28.0", - "strum_macros 0.28.0", "taler-api", "taler-build", "taler-common", + "taler-enum-meta", "taler-test-utils", "thiserror 2.0.18", "tokio", @@ -1612,14 +1618,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] @@ -1979,6 +1985,16 @@ dependencies = [ ] [[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2185,9 +2201,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -2205,8 +2221,8 @@ dependencies = [ "nu-ansi-term", "serde", "strip-ansi-escapes", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "thiserror 2.0.18", "unicase", "unicode-segmentation", @@ -2425,9 +2441,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ "aws-lc-rs", "ring", @@ -2955,12 +2971,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" - -[[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2974,18 +2984,6 @@ dependencies = [ ] [[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3100,6 +3098,15 @@ dependencies = [ ] [[package]] +name = "taler-enum-meta" +version = "1.5.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "taler-test-utils" version = "1.5.0" dependencies = [ @@ -3241,9 +3248,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -3600,9 +3607,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -3613,9 +3620,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -3623,9 +3630,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3633,9 +3640,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -3646,9 +3653,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -3689,9 +3696,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4255,6 +4262,12 @@ dependencies = [ ] [[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -26,14 +26,14 @@ taler-common = { path = "../taler-rust/common/taler-common" } taler-api = { path = "../taler-rust/common/taler-api" } taler-build = { path = "../taler-rust/common/taler-build" } taler-test-utils = { path = "../taler-rust/common/taler-test-utils" } +taler-enum-meta = { path = "../taler-rust/common/taler-enum-meta" } #taler-common = { git = "git://git.taler.net/taler-rust.git/" } #taler-api = { git = "git://git.taler.net/taler-rust.git/" } #taler-build = { git = "git://git.taler.net/taler-rust.git/" } #taler-test-utils = { git = "git://git.taler.net/taler-rust.git/" } url = "*" clap = { version = "4.5", features = ["derive"] } -strum = "0.28" -strum_macros = "0.28" +pretty_assertions = "*" aws-lc-rs = { version = "*" } serde = { version = "*", features = ["derive"] } reedline = "*" diff --git a/src/api.rs b/src/api.rs @@ -56,7 +56,7 @@ use crate::{ payment::{IncomingRegistrationResult, register_in_talerable}, transfer::{RegistrationResult, transfer_register, transfer_unregister}, }, - model::{IncomingId, IncomingPayment}, + model::{InId, InTx}, rand_ebics_id, }; @@ -112,18 +112,21 @@ async fn add_incoming( let now = Timestamp::now(); match register_in_talerable( db, - &IncomingPayment { - id: IncomingId { + &InTx { + id: InId { uetr: None, tx_id: Some(rand_ebics_id()), acct_svcr_ref: None, }, amount, - credit_fee: None, - subject: Some(format!( - "Manual incoming {}", - fmt_in_subject(subject.ty(), subject.key()) - )), + credit_fee: Amount::zero(&amount.currency), + subject: Some( + format!( + "Manual incoming {}", + fmt_in_subject(subject.ty(), subject.key()) + ) + .into_boxed_str(), + ), execution_time: now, debtor: Some(debit_account), }, @@ -270,7 +273,7 @@ impl PreparedTransfer for NexusApi { RegistrationResult::Success => ApiResult::Ok(RegistrationResponse { subjects: vec![ TransferSubject::QrBill { - credit_amount: req.credit_amount.clone(), + credit_amount: req.credit_amount, qr_reference_number: reference_number, }, TransferSubject::Simple { diff --git a/src/bin/iso20022-codegen.rs b/src/bin/iso20022-codegen.rs @@ -85,7 +85,7 @@ pub async fn iso20022codegen_external_code_set() { // THIS FILE IS GENERATED, DO NOT EDIT -use strum_macros::{AsRefStr, Display, EnumString}; +use taler_enum_meta::EnumMeta; " .to_string(); @@ -106,23 +106,18 @@ use strum_macros::{AsRefStr, Display, EnumString}; writeln!( &mut out, " - #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] + #[enum_meta(DomainCode, Description, Str)] pub enum {enum_name} {{ " ) .unwrap(); for (code, name, description) in set.iter() { writeln!(&mut out, "/// {description}").unwrap(); - writeln!(&mut out, "#[strum(serialize = \"{code}\")]").unwrap(); + writeln!(&mut out, "#[code = \"{code}\"]").unwrap(); writeln!(&mut out, "{name},").unwrap(); } - writeln!(&mut out, "}}\nimpl {enum_name} {{\n").unwrap(); - writeln!(&mut out, "pub fn description(&self) -> &'static str {{").unwrap(); - writeln!(&mut out, "match self {{").unwrap(); - for (_, name, description) in set { - writeln!(&mut out, "Self::{name} => \"{description}\",").unwrap(); - } - writeln!(&mut out, "}}}}}}\n").unwrap(); + writeln!(&mut out, "}}").unwrap(); } std::fs::write("src/iso20022/external_code.rs", out).unwrap(); } @@ -178,7 +173,7 @@ pub async fn iso20022codegen_bank_transaction_code() { // THIS FILE IS GENERATED, DO NOT EDIT -use strum_macros::{AsRefStr, Display, EnumString}; +use taler_enum_meta::EnumMeta; " .to_string(); @@ -190,7 +185,8 @@ use strum_macros::{AsRefStr, Display, EnumString}; writeln!( &mut out, " - #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] + #[enum_meta(Description, Str)] pub enum {enum_name} {{ " ) @@ -199,13 +195,7 @@ use strum_macros::{AsRefStr, Display, EnumString}; writeln!(&mut out, "/// {description}").unwrap(); writeln!(&mut out, "{code},").unwrap(); } - writeln!(&mut out, "}}\nimpl {enum_name} {{\n").unwrap(); - writeln!(&mut out, "pub fn description(&self) -> &'static str {{").unwrap(); - writeln!(&mut out, "match self {{").unwrap(); - for (code, description) in set.iter() { - writeln!(&mut out, "Self::{code} => \"{description}\",").unwrap(); - } - writeln!(&mut out, "}}}}}}\n").unwrap(); + writeln!(&mut out, "}}").unwrap(); } std::fs::write("src/iso20022/bank_tx_code.rs", out).unwrap(); } diff --git a/src/config.rs b/src/config.rs @@ -27,6 +27,7 @@ use regex::Regex; use taler_api::config::DbCfg; use taler_common::{ config::{Config, ValueErr}, + map_config, types::{ amount::{Amount, Currency}, utils::date_to_utc_ts, @@ -71,8 +72,7 @@ impl EbicsHostCfg { } } -#[derive(Debug, Clone, Copy, strum_macros::EnumString)] -#[strum(serialize_all = "snake_case")] +#[derive(Debug, Clone, Copy)] pub enum AccountType { Exchange, Normal, @@ -150,7 +150,11 @@ impl NexusCfg { let s = cfg.section("nexus-ebics"); Ok(Self { currency: s.currency("currency").require()?, - account_type: s.parse("account type", "ACCOUNT_TYPE").require()?, + account_type: map_config!(s, "account type", "ACCOUNT_TYPE", + "exchange" => { Ok(AccountType::Exchange) }, + "normal" => { Ok(AccountType::Normal) } + ) + .require()?, cfg, keys: OnceCell::new(), host: OnceCell::new(), @@ -196,8 +200,8 @@ impl NexusCfg { ignore_bounces_before: fetch.ignore_bounces_before, restriction_payto_regex: fetch.restriction_payto_regex.clone(), bounce_deduce_fee: fetch.bounce_deduce_fee, - bounce_fee: fetch.bounce_fee.clone(), - currency: self.currency.clone(), + bounce_fee: fetch.bounce_fee, + currency: self.currency, }) } } diff --git a/src/crypto.rs b/src/crypto.rs @@ -22,6 +22,7 @@ use aws_lc_rs::{ iv::FixedLength, rsa::{Pkcs1PrivateDecryptingKey, PrivateDecryptingKey, PublicEncryptingKey}, }; +use base64::{Engine as _, prelude::BASE64_STANDARD}; use jiff::{Timestamp, Zoned, tz::TimeZone}; use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyUsagePurpose}; use x509_parser::prelude::{FromDer as _, X509Certificate}; @@ -65,11 +66,12 @@ pub fn x509_certificate_from_rsa_private( } /// Extract an RSA public key from a X.509 certificate -pub fn rsa_private_from_x509_certificate_from(der: &[u8]) -> PublicEncryptingKey { - let (_, cert) = X509Certificate::from_der(der).unwrap(); +pub fn rsa_private_from_b64_x509_certificate(encoded: &str) -> anyhow::Result<PublicEncryptingKey> { + let der = BASE64_STANDARD.decode(encoded)?; + let (_, cert) = X509Certificate::from_der(&der)?; let issuer_public_key = cert.public_key(); - cert.verify_signature(Some(issuer_public_key)).unwrap(); - PublicEncryptingKey::from_der(issuer_public_key.raw).unwrap() + cert.verify_signature(Some(issuer_public_key))?; + Ok(PublicEncryptingKey::from_der(issuer_public_key.raw)?) } pub fn decrypt_ebics_e002(transaction_key: &DecryptingKey, mut encrypted_data: Vec<u8>) -> Vec<u8> { diff --git a/src/db.rs b/src/db.rs @@ -111,7 +111,7 @@ pub mod test { use crate::{ CONFIG_SOURCE, db::{ebics_first, ebics_register, ebics_remove}, - model::{IncomingId, IncomingPayment, InitiatedPayment, OutgoingId, OutgoingPayment}, + model::{InId, InTx, InitiatedPayment, OutId, OutTx}, rand_ebics_id, }; @@ -122,15 +122,15 @@ pub mod test { } /** Generates an outgoing payment, given its subject */ - pub fn gen_out_pay(subject: impl Into<String>) -> OutgoingPayment { - OutgoingPayment { - id: OutgoingId { + pub fn gen_out_pay(subject: impl Into<Box<str>>) -> OutTx { + OutTx { + id: OutId { msg_id: None, - end_to_end_id: Some(rand_ebics_id()), - acct_svcr_ref: None, + e2e_id: Some(rand_ebics_id()), + sref: None, }, amount: Amount::new(&CURRENCY, 44, 0), - debit_fee: None, + debit_fee: Amount::zero(&CURRENCY), creditor: Some( IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") .unwrap() @@ -159,11 +159,11 @@ pub mod test { } /** Generates an incoming payment, given its subject */ - pub fn gen_in_pay(subject: impl Into<String>) -> IncomingPayment { - IncomingPayment { - id: IncomingId::new(None, Some(rand_ebics_id()), None), + pub fn gen_in_pay(subject: impl Into<Box<str>>) -> InTx { + InTx { + id: InId::new(None, Some(rand_ebics_id()), None), amount: Amount::new(&CURRENCY, 44, 0), - credit_fee: None, + credit_fee: Amount::zero(&CURRENCY), debtor: Some( IbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith") .unwrap() diff --git a/src/db/exchange.rs b/src/db/exchange.rs @@ -213,7 +213,7 @@ pub async fn transfer( .bind(&req.request_uid) .bind(&req.wtid) .bind(&subject) - .bind(&req.amount) + .bind(req.amount) .bind(req.exchange_base_url.as_str()) .bind(&req.metadata) .bind(req.credit_account.as_ref().as_str()) diff --git a/src/db/initiated.rs b/src/db/initiated.rs @@ -20,11 +20,11 @@ use const_format::formatcp; use jiff::Timestamp; use sqlx::{PgPool, Row as _, postgres::PgRow}; use taler_api::db::{BindHelper as _, TypeHelper as _}; -use taler_common::types::amount::Currency; +use taler_common::types::amount::{Amount, Currency}; use crate::{ db::{PENDING, UNSETTLED}, - model::{InitiatedPayment, OutgoingId, OutgoingPayment, PaymentBatch, SubmissionState}, + model::{InitiatedPayment, OutId, OutTx, PaymentBatch, SubmissionState}, }; /// Outgoing payments initiation result @@ -51,7 +51,7 @@ pub async fn initiate( RETURNING initiated_outgoing_transaction_id ", ) - .bind(&payment.amount) + .bind(payment.amount) .bind(&payment.subject) .bind(payment.creditor.as_ref().as_str()) .bind_timestamp(&payment.initiation_time) @@ -169,7 +169,7 @@ pub async fn unsettled_tx_in_batch( currency: &Currency, msg_id: &str, execution_time: &Timestamp, -) -> sqlx::Result<Vec<OutgoingPayment>> { +) -> sqlx::Result<Vec<OutTx>> { sqlx::query(formatcp!( " SELECT @@ -185,14 +185,14 @@ pub async fn unsettled_tx_in_batch( )) .bind(msg_id) .try_map(|r: PgRow| { - Ok(OutgoingPayment { - id: OutgoingId { + Ok(OutTx { + id: OutId { msg_id: Some(msg_id.into()), - end_to_end_id: r.try_get("end_to_end_id")?, - acct_svcr_ref: None, + e2e_id: r.try_get("end_to_end_id")?, + sref: None, }, amount: r.try_get_amount("amount", currency)?, - debit_fee: None, + debit_fee: Amount::zero(currency), subject: r.try_get("subject")?, execution_time: *execution_time, creditor: r.try_get_opt_payto("credit_payto")?, diff --git a/src/db/payment.rs b/src/db/payment.rs @@ -21,9 +21,9 @@ use taler_api::{ db::{BindHelper as _, TypeHelper as _}, subject::{IncomingSubject, OutgoingSubject}, }; -use taler_common::types::amount::{Amount, Currency}; +use taler_common::types::amount::Amount; -use crate::model::{IncomingPayment, OutgoingPayment}; +use crate::model::{InTx, OutTx}; #[derive(Debug, PartialEq, Eq)] pub struct OutgoingRegistrationResult { @@ -35,7 +35,7 @@ pub struct OutgoingRegistrationResult { /** Register an outgoing payment reconciling it with its initiated payment counterpart if present */ pub async fn register_out_tx( pool: &PgPool, - payment: &OutgoingPayment, + payment: &OutTx, subject: Option<&OutgoingSubject>, ) -> sqlx::Result<OutgoingRegistrationResult> { sqlx::query( @@ -44,19 +44,14 @@ pub async fn register_out_tx( FROM register_outgoing($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ", ) - .bind(&payment.amount) - .bind( - payment - .debit_fee - .as_ref() - .unwrap_or(&Amount::zero(&payment.amount.currency)), - ) + .bind(payment.amount) + .bind(payment.debit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.creditor.as_ref().map(|it| it.as_ref().as_str())) - .bind(&payment.id.end_to_end_id) + .bind(&payment.id.e2e_id) .bind(&payment.id.msg_id) - .bind(&payment.id.acct_svcr_ref) + .bind(&payment.id.sref) .bind(subject.as_ref().map(|s| &s.wtid)) .bind(subject.as_ref().map(|s| s.exchange_base_url.as_str())) .bind(subject.as_ref().map(|s| &s.metadata)) @@ -74,8 +69,7 @@ pub async fn register_out_tx( /// Register an outgoing batch pub async fn register_out_batch( pool: &PgPool, - currency: &Currency, - payment: &OutgoingPayment, + payment: &OutTx, subject: Option<&OutgoingSubject>, ) -> sqlx::Result<OutgoingRegistrationResult> { sqlx::query( @@ -84,19 +78,14 @@ pub async fn register_out_batch( FROM register_outgoing($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ", ) - .bind(&payment.amount) - .bind( - payment - .debit_fee - .as_ref() - .unwrap_or(&Amount::zero(currency)), - ) + .bind(payment.amount) + .bind(payment.debit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.creditor.as_ref().map(|it| it.as_ref().as_str())) - .bind(&payment.id.end_to_end_id) + .bind(&payment.id.e2e_id) .bind(&payment.id.msg_id) - .bind(&payment.id.acct_svcr_ref) + .bind(&payment.id.sref) .bind(subject.as_ref().map(|s| &s.wtid)) .bind(subject.as_ref().map(|s| s.exchange_base_url.as_str())) .bind(subject.as_ref().map(|s| &s.metadata)) @@ -130,20 +119,15 @@ pub enum IncomingRegistrationResult { } /** Register an incoming payment */ -pub async fn register_in(pool: &PgPool, payment: &IncomingPayment) -> sqlx::Result<InResult> { +pub async fn register_in(pool: &PgPool, payment: &InTx) -> sqlx::Result<InResult> { sqlx::query( " SELECT out_found, out_completed, out_tx_id, out_bounce_id FROM register_incoming($1,$2,$3,$4,$5,$6,$7,$8,NULL,NULL,NULL) ", ) - .bind(&payment.amount) - .bind( - payment - .credit_fee - .as_ref() - .unwrap_or(&Amount::zero(&payment.amount.currency)), - ) + .bind(payment.amount) + .bind(payment.credit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.debtor.as_ref().map(|it| it.as_ref().as_str())) @@ -166,7 +150,7 @@ pub async fn register_in(pool: &PgPool, payment: &IncomingPayment) -> sqlx::Resu /** Register an talerable incoming payment */ pub async fn register_in_talerable( pool: &PgPool, - payment: &IncomingPayment, + payment: &InTx, subject: &IncomingSubject, ) -> sqlx::Result<IncomingRegistrationResult> { sqlx::query( @@ -183,13 +167,8 @@ pub async fn register_in_talerable( FROM register_incoming($1,$2,$3,$4,$5,$6,$7,$8,$9::taler_incoming_type,$10,NULL) ", ) - .bind(&payment.amount) - .bind( - payment - .credit_fee - .as_ref() - .unwrap_or(&Amount::zero(&payment.amount.currency)), - ) + .bind(payment.amount) + .bind(payment.credit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.debtor.as_ref().map(|it| it.as_ref().as_str())) @@ -222,7 +201,7 @@ pub async fn register_in_talerable( /** Register an talerable incoming payment */ pub async fn register_in_qr_bill( pool: &PgPool, - payment: &IncomingPayment, + payment: &InTx, reference: &str, ) -> sqlx::Result<IncomingRegistrationResult> { sqlx::query( @@ -239,13 +218,8 @@ pub async fn register_in_qr_bill( FROM register_incoming($1,$2,$3,$4,$5,$6,$7,$8,NULL,NULL,$9) ", ) - .bind(&payment.amount) - .bind( - payment - .credit_fee - .as_ref() - .unwrap_or(&Amount::zero(&payment.amount.currency)), - ) + .bind(payment.amount) + .bind(payment.credit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.debtor.as_ref().map(|it| it.as_ref().as_str())) @@ -284,7 +258,7 @@ pub enum IncomingBounceRegistrationResult { /** Register an incoming payment and bounce it */ pub async fn register_in_malformed( pool: &PgPool, - payment: &IncomingPayment, + payment: &InTx, bounce_amount: &Amount, bounce_end_to_end_id: &str, timestamp: &Timestamp, @@ -296,13 +270,8 @@ pub async fn register_in_malformed( FROM register_and_bounce_incoming($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ", ) - .bind(&payment.amount) - .bind( - payment - .credit_fee - .as_ref() - .unwrap_or(&Amount::zero(&payment.amount.currency)), - ) + .bind(payment.amount) + .bind(payment.credit_fee) .bind(&payment.subject) .bind_timestamp(&payment.execution_time) .bind(payment.debtor.as_ref().map(|it| it.as_ref().as_str())) @@ -358,7 +327,7 @@ mod test { }, transfer::{RegistrationResult, transfer_register}, }, - model::{IncomingId, IncomingPayment, OutgoingBatch, OutgoingId, OutgoingPayment}, + model::{InId, InTx, OutBatch, OutId, OutTx}, rand_ebics_id, worker::{register_incoming, register_outgoing, register_outgoing_batch}, }; @@ -375,7 +344,7 @@ mod test { assert!(matches!( initiate( &db, - &gen_init_pay(payment.id.end_to_end_id.clone().unwrap(), subject), + &gen_init_pay(payment.id.e2e_id.clone().unwrap(), subject), ) .await, Ok(PaymentInitiationResult::Success(_)) @@ -397,11 +366,11 @@ mod test { new: false } ); - let payment = OutgoingPayment { - id: OutgoingId { + let payment = OutTx { + id: OutId { msg_id: None, - end_to_end_id: None, - acct_svcr_ref: payment.id.end_to_end_id, + e2e_id: None, + sref: payment.id.e2e_id, }, ..payment }; @@ -503,7 +472,7 @@ mod test { register_outgoing_batch( &db, &CURRENCY, - &OutgoingBatch { + &OutBatch { msg_id: "BATCH".into(), execution_time: Timestamp::now(), }, @@ -530,7 +499,7 @@ mod test { register_outgoing_batch( &db, &CURRENCY, - &OutgoingBatch { + &OutBatch { msg_id: "BATCH_MANUAL".into(), execution_time: Timestamp::now(), }, @@ -549,7 +518,7 @@ mod test { register_outgoing_batch( &db, &CURRENCY, - &OutgoingBatch { + &OutBatch { msg_id: "BATCH_MANUAL".into(), execution_time: Timestamp::now(), }, @@ -666,7 +635,7 @@ mod test { check_in_state(&db, &[Bounced, Bounced, Simple]).await; let original = gen_in_pay("test 2".to_owned()); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, debtor: None, ..original.clone() @@ -717,7 +686,7 @@ mod test { let new = EddsaPublicKey::rand(); let original = gen_in_pay(format!("test 2 with {new} reserve pub")); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, debtor: None, ..original.clone() @@ -788,7 +757,7 @@ mod test { check_in_state(&db, &[Reserve(first.clone()), Simple]).await; let original = gen_in_pay(format!("test 2 for {subject}")); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, debtor: None, ..original.clone() @@ -912,7 +881,7 @@ mod test { check_in_state(&db, &[Reserve(first.clone()), Simple]).await; let original = gen_in_pay(reference_number.clone()); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, debtor: None, ..original.clone() @@ -1000,7 +969,7 @@ mod test { let (_, db) = setup().await; let cfg = NexusIngestConfig::simple(AccountType::Exchange, &CURRENCY); - async fn check_content(db: &PgPool, p: &IncomingPayment) { + async fn check_content(db: &PgPool, p: &InTx) { sqlx::query( " SELECT @@ -1028,9 +997,9 @@ mod test { // Non talerable for (i, id) in [ - IncomingId::new(Some(Uuid::new_v4()), None, None), - IncomingId::new(None, Some(rand_ebics_id()), None), - IncomingId::new(None, None, Some(rand_ebics_id())), + InId::new(Some(Uuid::new_v4()), None, None), + InId::new(None, Some(rand_ebics_id()), None), + InId::new(None, None, Some(rand_ebics_id())), ] .iter() .enumerate() @@ -1038,7 +1007,7 @@ mod test { let payment = gen_in_pay("subject".to_owned()); // Register minimal - let partial = IncomingPayment { + let partial = InTx { id: id.clone(), subject: None, debtor: None, @@ -1049,12 +1018,12 @@ mod test { check_in_count(&db, i + 1, i, 0).await; // Recover ID - let full_id = IncomingId::new( + let full_id = InId::new( Some(id.uetr.unwrap_or_else(Uuid::new_v4)), Some(id.tx_id.clone().unwrap_or_else(rand_ebics_id)), Some(id.acct_svcr_ref.clone().unwrap_or_else(rand_ebics_id)), ); - let full = IncomingPayment { + let full = InTx { id: full_id.clone(), ..partial.clone() }; @@ -1063,7 +1032,7 @@ mod test { check_in_count(&db, i + 1, i, 0).await; // Recover subject & debtor - let full = IncomingPayment { + let full = InTx { id: full_id, ..payment.clone() }; @@ -1074,9 +1043,9 @@ mod test { // Talerable for (i, id) in [ - IncomingId::new(Some(Uuid::new_v4()), None, None), - IncomingId::new(None, Some(rand_ebics_id()), None), - IncomingId::new(None, None, Some(rand_ebics_id())), + InId::new(Some(Uuid::new_v4()), None, None), + InId::new(None, Some(rand_ebics_id()), None), + InId::new(None, None, Some(rand_ebics_id())), ] .iter() .enumerate() @@ -1085,7 +1054,7 @@ mod test { let payment = gen_in_pay(format!("test with {key} reserve pub")); // Register minimal - let partial = IncomingPayment { + let partial = InTx { id: id.clone(), subject: None, debtor: None, @@ -1096,12 +1065,12 @@ mod test { check_in_count(&db, i + 4, 3, i).await; // Recover ID - let full_id = IncomingId::new( + let full_id = InId::new( Some(id.uetr.unwrap_or_else(Uuid::new_v4)), Some(id.tx_id.clone().unwrap_or_else(rand_ebics_id)), Some(id.acct_svcr_ref.clone().unwrap_or_else(rand_ebics_id)), ); - let full = IncomingPayment { + let full = InTx { id: full_id.clone(), ..partial.clone() }; @@ -1110,7 +1079,7 @@ mod test { check_in_count(&db, i + 4, 3, i).await; // Recover subject & debtor - let full = IncomingPayment { + let full = InTx { id: full_id, ..payment.clone() }; @@ -1142,7 +1111,7 @@ mod test { .unwrap(), IncomingBounceRegistrationResult::Talerable ); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, ..payment.clone() }; @@ -1154,7 +1123,7 @@ mod test { // Check we do not register as talerable bounced transaction let new_key = EddsaPublicKey::rand(); let payment = gen_in_pay(format!("bounced {new_key}")); - let incomplete = IncomingPayment { + let incomplete = InTx { subject: None, ..payment.clone() }; diff --git a/src/ebics_code.rs b/src/ebics_code.rs @@ -17,6 +17,8 @@ * <http://www.gnu.org/licenses/> */ +use taler_enum_meta::EnumMeta; + /// EBICS Error Class (First two digits of the return code) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EbicsKind { @@ -32,159 +34,158 @@ pub enum EbicsKind { NonRecoverableError, } -use strum_macros::{AsRefStr, Display, EnumString}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(DomainCode, Str)] #[allow(non_camel_case_types)] pub enum EbicsReturnCode { // --- 00: Information --- - #[strum(serialize = "000000")] + #[code = "000000"] EBICS_OK, // --- 01: Notes --- - #[strum(serialize = "011000")] + #[code = "011000"] EBICS_DOWNLOAD_POSTPROCESS_DONE, - #[strum(serialize = "011001")] + #[code = "011001"] EBICS_DOWNLOAD_POSTPROCESS_SKIPPED, - #[strum(serialize = "011101")] + #[code = "011101"] EBICS_TX_SEGMENT_NUMBER_UNDERRUN, - #[strum(serialize = "011301")] + #[code = "011301"] EBICS_NO_ONLINE_CHECKS, // --- 03: Warnings --- - #[strum(serialize = "031001")] + #[code = "031001"] EBICS_ORDER_PARAMS_IGNORED, // --- 06: Technical Errors (Recoverable) --- - #[strum(serialize = "061001")] + #[code = "061001"] EBICS_AUTHENTICATION_FAILED, - #[strum(serialize = "061002")] + #[code = "061002"] EBICS_INVALID_REQUEST, - #[strum(serialize = "061099")] + #[code = "061099"] EBICS_INTERNAL_ERROR, - #[strum(serialize = "061101")] + #[code = "061101"] EBICS_TX_RECOVERY_SYNC, // --- 09: Business Errors (Non-Recoverable) --- - #[strum(serialize = "090003")] + #[code = "090003"] EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED, - #[strum(serialize = "090004")] + #[code = "090004"] EBICS_INVALID_ORDER_DATA_FORMAT, - #[strum(serialize = "090005")] + #[code = "090005"] EBICS_NO_DOWNLOAD_DATA_AVAILABLE, - #[strum(serialize = "090006")] + #[code = "090006"] EBICS_UNSUPPORTED_REQUEST_FOR_ORDER_INSTANCE, // --- 09: Transaction Administration --- - #[strum(serialize = "091002")] + #[code = "091002"] EBICS_INVALID_USER_OR_USER_STATE, - #[strum(serialize = "091003")] + #[code = "091003"] EBICS_USER_UNKNOWN, - #[strum(serialize = "091004")] + #[code = "091004"] EBICS_INVALID_USER_STATE, - #[strum(serialize = "091005")] + #[code = "091005"] EBICS_INVALID_ORDER_TYPE, - #[strum(serialize = "091006")] + #[code = "091006"] EBICS_UNSUPPORTED_ORDER_TYPE, - #[strum(serialize = "091007")] + #[code = "091007"] EBICS_DISTRIBUTED_SIGNATURE_AUTHORISATION_FAILED, - #[strum(serialize = "091008")] + #[code = "091008"] EBICS_BANK_PUBKEY_UPDATE_REQUIRED, - #[strum(serialize = "091009")] + #[code = "091009"] EBICS_SEGMENT_SIZE_EXCEEDED, - #[strum(serialize = "091010")] + #[code = "091010"] EBICS_INVALID_XML, - #[strum(serialize = "091011")] + #[code = "091011"] EBICS_INVALID_HOST_ID, // --- 09: Transaction Processing --- - #[strum(serialize = "091101")] + #[code = "091101"] EBICS_TX_UNKNOWN_TXID, - #[strum(serialize = "091102")] + #[code = "091102"] EBICS_TX_ABORT, - #[strum(serialize = "091103")] + #[code = "091103"] EBICS_TX_MESSAGE_REPLAY, - #[strum(serialize = "091104")] + #[code = "091104"] EBICS_TX_SEGMENT_NUMBER_EXCEEDED, - #[strum(serialize = "091105")] + #[code = "091105"] EBICS_RECOVERY_NOT_SUPPORTED, - #[strum(serialize = "091111")] + #[code = "091111"] EBICS_INVALID_SIGNATURE_FILE_FORMAT, - #[strum(serialize = "091112")] + #[code = "091112"] EBICS_INVALID_ORDER_PARAMS, - #[strum(serialize = "091113")] + #[code = "091113"] EBICS_INVALID_REQUEST_CONTENT, - #[strum(serialize = "091114")] + #[code = "091114"] EBICS_ORDERID_UNKNOWN, - #[strum(serialize = "091115")] + #[code = "091115"] EBICS_ORDERID_ALREADY_FINAL, - #[strum(serialize = "091116")] + #[code = "091116"] EBICS_PROCESSING_ERROR, - #[strum(serialize = "091117")] + #[code = "091117"] EBICS_MAX_ORDER_DATA_SIZE_EXCEEDED, - #[strum(serialize = "091118")] + #[code = "091118"] EBICS_MAX_SEGMENTS_EXCEEDED, - #[strum(serialize = "091119")] + #[code = "091119"] EBICS_MAX_TRANSACTIONS_EXCEEDED, - #[strum(serialize = "091120")] + #[code = "091120"] EBICS_PARTNER_ID_MISMATCH, - #[strum(serialize = "091121")] + #[code = "091121"] EBICS_INCOMPATIBLE_ORDER_ATTRIBUTE, - #[strum(serialize = "091122")] + #[code = "091122"] EBICS_ORDER_ALREADY_EXISTS, // --- 09: Key Management (X.509 & Keys) --- - #[strum(serialize = "091201")] + #[code = "091201"] EBICS_KEYMGMT_UNSUPPORTED_VERSION_SIGNATURE, - #[strum(serialize = "091202")] + #[code = "091202"] EBICS_KEYMGMT_UNSUPPORTED_VERSION_AUTHENTICATION, - #[strum(serialize = "091203")] + #[code = "091203"] EBICS_KEYMGMT_UNSUPPORTED_VERSION_ENCRYPTION, - #[strum(serialize = "091204")] + #[code = "091204"] EBICS_KEYMGMT_KEYLENGTH_ERROR_SIGNATURE, - #[strum(serialize = "091205")] + #[code = "091205"] EBICS_KEYMGMT_KEYLENGTH_ERROR_AUTHENTICATION, - #[strum(serialize = "091206")] + #[code = "091206"] EBICS_KEYMGMT_KEYLENGTH_ERROR_ENCRYPTION, - #[strum(serialize = "091207")] + #[code = "091207"] EBICS_KEYMGMT_NO_X509_SUPPORT, - #[strum(serialize = "091208")] + #[code = "091208"] EBICS_X509_CERTIFICATE_EXPIRED, - #[strum(serialize = "091209")] + #[code = "091209"] EBICS_X509_CERTIFICATE_NOT_VALID_YET, - #[strum(serialize = "091210")] + #[code = "091210"] EBICS_X509_WRONG_KEY_USAGE, - #[strum(serialize = "091211")] + #[code = "091211"] EBICS_X509_WRONG_ALGORITHM, - #[strum(serialize = "091212")] + #[code = "091212"] EBICS_X509_INVALID_THUMBPRINT, - #[strum(serialize = "091213")] + #[code = "091213"] EBICS_X509_CTL_INVALID, - #[strum(serialize = "091214")] + #[code = "091214"] EBICS_X509_UNKNOWN_CERTIFICATE_AUTHORITY, - #[strum(serialize = "091215")] + #[code = "091215"] EBICS_X509_INVALID_POLICY, - #[strum(serialize = "091216")] + #[code = "091216"] EBICS_X509_INVALID_BASIC_CONSTRAINTS, - #[strum(serialize = "091217")] + #[code = "091217"] EBICS_ONLY_X509_SUPPORT, - #[strum(serialize = "091218")] + #[code = "091218"] EBICS_KEYMGMT_DUPLICATE_KEY, - #[strum(serialize = "091219")] + #[code = "091219"] EBICS_CERTIFICATES_VALIDATION_ERROR, // --- 09: Pre-verification / Signature Logic --- - #[strum(serialize = "091301")] + #[code = "091301"] EBICS_SIGNATURE_VERIFICATION_FAILED, - #[strum(serialize = "091302")] + #[code = "091302"] EBICS_ACCOUNT_AUTHORISATION_FAILED, - #[strum(serialize = "091303")] + #[code = "091303"] EBICS_AMOUNT_CHECK_FAILED, - #[strum(serialize = "091304")] + #[code = "091304"] EBICS_SIGNER_UNKNOWN, - #[strum(serialize = "091305")] + #[code = "091305"] EBICS_INVALID_SIGNER_STATE, - #[strum(serialize = "091306")] + #[code = "091306"] EBICS_DUPLICATE_SIGNATURE, } diff --git a/src/iso20022/bank_tx_code.rs b/src/iso20022/bank_tx_code.rs @@ -19,9 +19,10 @@ // THIS FILE IS GENERATED, DO NOT EDIT -use strum_macros::{AsRefStr, Display, EnumString}; +use taler_enum_meta::EnumMeta; -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Description, Str)] pub enum BankTxDomainCode { /// Account Management ACMT, @@ -46,25 +47,9 @@ pub enum BankTxDomainCode { /// Extended Domain XTND, } -impl BankTxDomainCode { - pub fn description(&self) -> &'static str { - match self { - Self::ACMT => "Account Management", - Self::CAMT => "Cash Management", - Self::CMDT => "Commodities", - Self::DERV => "Derivatives", - Self::FORX => "Foreign Exchange", - Self::LDAS => "Loans, Deposits & Syndications", - Self::PMET => "Precious Metal", - Self::PMNT => "Payments", - Self::SECU => "Securities", - Self::TRAD => "Trade Services", - Self::XTND => "Extended Domain", - } - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Description, Str)] pub enum BankTxFamilyCode { /// Account Balancing ACCB, @@ -189,75 +174,9 @@ pub enum BankTxFamilyCode { /// Syndications SYDN, } -impl BankTxFamilyCode { - pub fn description(&self) -> &'static str { - match self { - Self::ACCB => "Account Balancing", - Self::ACOP => "Additional Miscellaneous Credit Operations", - Self::ADOP => "Additional Miscellaneous Debit Operations", - Self::BLOC => "Blocked Transactions", - Self::CAPL => "Cash Pooling", - Self::CASH => "Miscellaneous Securities Operations", - Self::CCRD => "Customer Card Transactions", - Self::CLNC => "Clean Collection", - Self::CNTR => "Counter Transactions", - Self::COLC => "Custody Collection", - Self::COLL => "Collateral Management", - Self::CORP => "Corporate Action", - Self::CSLN => "Consumer Loans", - Self::CUST => "Custody", - Self::DCCT => "Documentary Credit", - Self::DLVR => "Delivery", - Self::DOCC => "Documentary Collection", - Self::DRFT => "Drafts", - Self::FTDP => "Fixed Term Deposits", - Self::FTLN => "Fixed Term Loans", - Self::FTUR => "Futures", - Self::FWRD => "Forwards", - Self::GUAR => "Guarantees", - Self::ICCN => "Issued Cash Concentration Transactions", - Self::ICDT => "Issued Credit Transfers", - Self::ICHQ => "Issued Cheques", - Self::IDDT => "Issued Direct Debits", - Self::IRCT => "Issued Real-Time Credit Transfers", - Self::LACK => "Lack", - Self::LBOX => "Lockbox Transactions", - Self::LFUT => "Listed Derivatives - Futures", - Self::LOCT => "Stand-By Letter Of Credit", - Self::LOPT => "Listed Derivatives - Options", - Self::MCOP => "Miscellaneous Credit Operations", - Self::MCRD => "Merchant Card Transactions", - Self::MDOP => "Miscellaneous Debit Operations", - Self::MGLN => "Mortgage Loans", - Self::NDFX => "Non Deliverable", - Self::NSET => "Non Settled", - Self::NTAV => "Not Available", - Self::NTDP => "Notice Deposits", - Self::NTLN => "Notice Loans", - Self::OBND => "OTC Derivatives - Bonds", - Self::OCRD => "OTC Derivatives - Credit", - Self::OEQT => "OTC Derivatives - Equity", - Self::OIRT => "OTC Derivatives - Interest Rates", - Self::OPCL => "Opening & Closing", - Self::OPTN => "Options", - Self::OSED => "OTC Derivatives - Structured Exotic Derivatives", - Self::OSWP => "OTC Derivatives – Swaps", - Self::OTHB => "CSD Blocked transactions", - Self::OTHR => "Other", - Self::RCCN => "Received Cash Concentration Transactions", - Self::RCDT => "Received Credit Transfers", - Self::RCHQ => "Received Cheques", - Self::RDDT => "Received Direct Debits", - Self::RRCT => "Received Real-Time Credit Transfers", - Self::SETT => "Trade, Clearing and Settlement", - Self::SPOT => "Spots", - Self::SWAP => "Swaps", - Self::SYDN => "Syndications", - } - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Description, Str)] pub enum BankTxSubFamilyCode { /// Account Closing ACCC, @@ -824,291 +743,3 @@ pub enum BankTxSubFamilyCode { /// Zero Balancing ZABA, } -impl BankTxSubFamilyCode { - pub fn description(&self) -> &'static str { - match self { - Self::ACCC => "Account Closing", - Self::ACCO => "Account Opening", - Self::ACCT => "Account Transfer", - Self::ACDT => "ACH Credit", - Self::ACON => "ACH Concentration", - Self::ACOR => "ACH Corporate Trade", - Self::ADBT => "ACH Debit", - Self::ADJT => "Adjustments (Generic)", - Self::APAC => "ACH Pre-Authorised", - Self::ARET => "ACH Return", - Self::AREV => "ACH Reversal", - Self::ARPD => "ARP Debit", - Self::ASET => "ACH Settlement", - Self::ATXN => "ACH Transaction", - Self::AUTT => "Automatic Transfer", - Self::BACT => "Branch Account Transfer", - Self::BBDD => "SEPA B2B Direct Debit", - Self::BCDP => "Branch Deposit", - Self::BCHQ => "Bank Cheque ", - Self::BCKV => "Back Value", - Self::BCWD => "Branch Withdrawl", - Self::BFWD => "Bond Forward", - Self::BIDS => "Repurchase offer/Issuer Bid/Reverse Rights", - Self::BKFE => "Bank Fees", - Self::BONU => "Bonus Issue/Capitalisation Issue", - Self::BOOK => "Internal Book Transfer", - Self::BPUT => "Put Redemption", - Self::BROK => "Brokerage Fee", - Self::BSBC => "Sell Buy Back", - Self::BSBO => "Buy Sell Back", - Self::CAJT => "Credit Adjustments (Generic)", - Self::CAPG => "Capital Gains Distribution", - Self::CASH => "Cash Letter", - Self::CCCH => "Certified Customer Cheque", - Self::CCHQ => "Cheque", - Self::CCIR => "Cross Currency IRS", - Self::CCPC => "CCP Cleared Initial Margin", - Self::CCPM => "CCP Cleared Variation Margin", - Self::CCSM => "CCP Cleared Segregated Initial Margin", - Self::CDIS => "Controlled Disbursement", - Self::CDPT => "Cash Deposit", - Self::CHAR => "Charge/Fees", - Self::CHKD => "Check Deposit", - Self::CHRG => "Charges (Generic)", - Self::CLAI => "Compensation/Claims", - Self::CLCQ => "Circular Cheque", - Self::CMBO => "Corporate Mark Broker Owned", - Self::CMCO => "Corporate Mark Client Owned", - Self::COAT => "Corporate Own Account Transfer", - Self::COME => "Commission Excluding Taxes (Generic)", - Self::COMI => "Commission Including Taxes (Generic)", - Self::COMM => "Commission (Generic)", - Self::COMT => "Non Taxable Commissions (Generic)", - Self::CONV => "Conversion", - Self::COVE => "Cover Transaction ", - Self::CPEN => "Cash Penalties", - Self::CPRB => "Corporate Rebate", - Self::CQRV => "Cheque Reversal", - Self::CRCQ => "Crossed Cheque", - Self::CRDS => "Credit DefaultSwap", - Self::CROS => "Cross Trade", - Self::CRPR => "Cross Product", - Self::CRSP => "Credit Support", - Self::CRTL => "Credit Line", - Self::CSHA => "Cash Letter Adjustment", - Self::CSLI => "Cash In Lieu", - Self::CWDL => "Cash Withdrawal", - Self::DAJT => "Debit Adjustments (Generic)", - Self::DDFT => "Discounted Draft", - Self::DDWN => "Drawdown", - Self::DECR => "Decrease in Value", - Self::DMCG => "Draft Maturity Change", - Self::DMCT => "Domestic Credit Transfer", - Self::DPST => "Deposit", - Self::DRAW => "Drawing", - Self::DRIP => "Dividend Reinvestment", - Self::DSBR => "Controlled Disbursement", - Self::DTCH => "Dutch Auction", - Self::DVCA => "Cash Dividend", - Self::DVOP => "Dividend Option", - Self::ENCT => "Nordic Payment Council Credit Transfer", - Self::EQBO => "Equity Mark Broker Owned", - Self::EQCO => "Equity Mark Client Owned", - Self::EQPT => "Equity Option", - Self::EQUS => "Equity Swap", - Self::ERTA => "Exchange Rate Adjustment", - Self::ERWA => "Lending Income", - Self::ERWI => "Borrowing Fee", - Self::ESCT => "SEPA Credit Transfer", - Self::ESDD => "SEPA Core Direct Debit", - Self::EXOF => "Exchange", - Self::EXPT => "Exotic Option", - Self::EXRI => "Call On Intermediate Securities", - Self::EXTD => "Exchange Traded Derivatives", - Self::EXWA => "Warrant Exercise/Warrant Conversion", - Self::FCDP => "Foreign Currencies Deposit", - Self::FCTA => "Factor Update", - Self::FCWD => "Foreign Currencies Withdrawal", - Self::FEES => "Fees (Generic)", - Self::FICT => "Financial Institution Credit Transfer", - Self::FIDD => "Financial Institution Direct Debit Payment", - Self::FIOA => "Financial Institution Own Account Transfer", - Self::FIXI => "Fixed Income", - Self::FLTA => "Float Adjustment", - Self::FRZF => "Freeze Of Funds", - Self::FUCO => "Futures Commission", - Self::FUTU => "Future Variation Margin", - Self::FWBC => "Forwards Broker Owned Collateral", - Self::FWCC => "Forwards Client Owned Collateral", - Self::FWSB => "MFA Segregated Broker Cash Collateral", - Self::FWSC => "MFA Segregated Client Cash Collateral", - Self::GEN1 => "Withdrawal/Distribution", - Self::GEN2 => "Deposit/Contribution", - Self::IADD => "Invoice Accepted with Differed Due Date", - Self::ICCT => "Intra Company Transfer", - Self::INFD => "Fixed Deposit Interest Amount", - Self::INSP => "Inspeci/Share Exchange", - Self::INTR => "Interests (Generic)", - Self::ISSU => "Depositary Receipt Issue", - Self::LBCA => "Credit Adjustment", - Self::LBDB => "Debit", - Self::LBDP => "Deposit", - Self::LIQU => "Liquidation Dividend / Liquidation Payment", - Self::MARG => "Margin Payments", - Self::MBSB => "Mortgage Back Segregated Broker Cash Collateral", - Self::MBSC => "Mortgage Back Segregated Client Cash Collateral", - Self::MCAL => "Full Call / Early Redemption", - Self::MGCC => "Margin Client Owned Cash Collateral", - Self::MGSC => "Initial Futures Margin Segregated Client Cash Collateral", - Self::MIXD => "Mixed Deposit", - Self::MNFE => "Management Fees", - Self::MRGR => "Merger", - Self::MSCD => "Miscellaneous Deposit", - Self::NETT => "Netting", - Self::NPCC => "Non Presented Circular Cheques", - Self::NSYN => "Non Syndicated", - Self::NTAV => "Not Available", - Self::NWID => "New issue distribution", - Self::OCCC => "Client owned OCC pledged collateral", - Self::ODFT => "Overdraft", - Self::ODLT => "Odd Lot Sale/Purchase", - Self::OODD => "One-Off Direct Debit", - Self::OPBC => "Option Broker Owned Collateral", - Self::OPCC => "Option Client Owned Collateral", - Self::OPCQ => "Open Cheque", - Self::OPSB => "OTC Option Segregated Broker Cash Collateral", - Self::OPSC => "OTC Option Segregated Client Cash Collateral", - Self::OPTN => "FX Option", - Self::ORCQ => "Order Cheque", - Self::OTCC => "OTC CCP", - Self::OTCD => "OTC Derivatives", - Self::OTCG => "OTC", - Self::OTCN => "OTC Non-CCP", - Self::OTHR => "Other", - Self::OVCH => "Overdraft Charge", - Self::OWNE => "External Account Transfer", - Self::OWNI => "Internal Account Transfer", - Self::PADD => "Pre-Authorised Direct Debit", - Self::PAIR => "Pair-Off", - Self::PCAL => "Partial Redemption with reduction of nominal value", - Self::PLAC => "Placement", - Self::PMDD => "Direct Debit", - Self::PORT => "Portfolio Move", - Self::POSC => "Credit Card Payment", - Self::POSD => "Point-of-Sale (POS) Payment - Debit Card", - Self::POSP => "Point-of-Sale (POS) Payment", - Self::PPAY => "Principal Payment", - Self::PRCT => "Priority Credit Transfer", - Self::PRDD => "Reversal Due To Payment Reversal ", - Self::PRED => "Partial Redemption Without Reduction of Nominal Value", - Self::PRII => "Interest Payment with Principles", - Self::PRIN => "Interest Payment with Principles", - Self::PRIO => "Priority Issue", - Self::PRUD => "Principal Pay-Down/Pay-Up", - Self::PSTE => "Posting Error", - Self::RCDD => "Reversal Due To Payment Cancellation Request", - Self::RCOV => "Reversal due to a Cover Transaction Return", - Self::REAA => "Redemption Asset Allocation", - Self::REDM => "Redemption", - Self::REPU => "Repo", - Self::RESI => "Futures Residual Amount", - Self::RHTS => "Rights Issue/Subscription Rights/Rights Offer", - Self::RIMB => "Reimbursement (Generic)", - Self::RNEW => "Renewal", - Self::RPBC => "Bi-lateral repo broker owned collateral", - Self::RPCC => "Repo client owned collateral", - Self::RPCR => "Reversal Due To Payment Cancellation Request ", - Self::RPMT => "Repayment", - Self::RPSB => "Bi-lateral Repo Segregated Broker Cash Collateral", - Self::RPSC => "Bi-lateral Repo Segregated Client Cash Collateral", - Self::RRTN => "Reversal Due To Payment Return", - Self::RVPO => "Reverse Repo", - Self::RWPL => "Redemption Withdrawing Plan", - Self::SABG => "Settlement Against Bank Guarantee", - Self::SALA => "Payroll/Salary Payment", - Self::SBSC => "Securities Buy Sell Sell Buy Back", - Self::SCIE => "Single Currency IRS Exotic", - Self::SCIR => "Single Currency IRS", - Self::SCRP => "Securities Cross Products", - Self::SDVA => "Same Day Value Credit Transfer", - Self::SECB => "Securities Borrowing", - Self::SECL => "Securities Lending", - Self::SHBC => "Broker owned collateral Short Sale", - Self::SHCC => "Client owned collateral Short Sale", - Self::SHPR => "Equity Premium Reserve", - Self::SHSL => "Short Sell", - Self::SLBC => "Lending Broker Owned Cash Collateral", - Self::SLCC => "Lending Client Owned Cash Collateral", - Self::SLEB => "Securities Lending And Borrowing", - Self::SLOA => "SecuredLoan", - Self::SMCD => "Smart-Card Payment", - Self::SMRT => "Smart-Card Payment", - Self::SOSE => "Settlement Of Sight Export Document", - Self::SOSI => "Settlement Of Sight Import Document", - Self::SSPL => "Subscription Savings Plan", - Self::STAC => "Settlement After Collection", - Self::STAM => "Stamp Duty", - Self::STDO => "Standing Order", - Self::STLM => "Settlement", - Self::STLR => "Settlement Under Reserve", - Self::STOD => "Bill of Exchange Settlement on Demand", - Self::SUAA => "Subscription Asset Allocation", - Self::SUBS => "Subscription", - Self::SWAP => "Swap Payment", - Self::SWBC => "Swap Broker Owned Collateral", - Self::SWCC => "Swap Client Owned Cash Collateral", - Self::SWEP => "Sweep", - Self::SWFP => "Final Payment", - Self::SWIC => "Switch", - Self::SWPP => "Partial Payment", - Self::SWPT => "Swaption", - Self::SWRS => "Reset Payment", - Self::SWSB => "ISDA/CSA Segregated Broker Cash Collateral", - Self::SWSC => "ISDA/CSA Segregated Client Cash Collateral", - Self::SWUF => "Upfront Payment", - Self::SYND => "Syndicated", - Self::TAXE => "Taxes (Generic)", - Self::TBAC => "TBA Closing", - Self::TBAS => "To Be Announced", - Self::TBBC => "TBA Broker owned cash collateral", - Self::TBCC => "TBA Client owned cash collateral", - Self::TCDP => "Travellers Cheques Deposit", - Self::TCWD => "Travellers Cheques Withdrawal", - Self::TEND => "Tender", - Self::TOPG => "Topping", - Self::TOUT => "Transfer Out", - Self::TRAD => "Trade", - Self::TRCP => "Treasury Cross Product", - Self::TREC => "Tax Reclaim", - Self::TRFE => "Transaction Fees", - Self::TRIN => "Transfer In", - Self::TRPO => "Triparty Repo", - Self::TRVO => "Triparty Reverse Repo", - Self::TTLS => "Treasury Tax And Loan Service", - Self::TURN => "Turnaround", - Self::UDFT => "Dishonoured/Unpaid Draft", - Self::UNCO => "Underwriting Commission", - Self::UPCQ => "Unpaid Cheque", - Self::UPCT => "Unpaid Card Transaction", - Self::UPDD => "Reversal Due To Return/Unpaid Direct Debit", - Self::URCQ => "Cheque Under Reserve", - Self::URDD => "Direct Debit Under Reserve", - Self::VALD => "Value Date", - Self::VCOM => "Credit Transfer With Agreed Commercial Information", - Self::WITH => "Withholding Tax", - Self::XBCP => "Cross-Border Credit Card Payment", - Self::XBCQ => "Foreign Cheque", - Self::XBCT => "Cross-Border Credit Transfer", - Self::XBCW => "Cross-Border Cash Withdrawal", - Self::XBDD => "Cross-Border Direct Debit", - Self::XBRD => "Cross-Border", - Self::XBSA => "Cross-Border Payroll/Salary Payment", - Self::XBST => "Cross-Border Standing Order", - Self::XCHC => "Exchange Traded CCP", - Self::XCHG => "Exchange Traded", - Self::XCHN => "Exchange Traded Non-CCP", - Self::XICT => "Cross-Border Intra Company Transfer", - Self::XPCQ => "Unpaid Foreign Cheque", - Self::XRCQ => "Foreign Cheque Under Reserve", - Self::XRTN => "Cross Border Reversal Due to Payment Return", - Self::YTDA => "YTD Adjustment", - Self::ZABA => "Zero Balancing", - } - } -} diff --git a/src/iso20022/camt.rs b/src/iso20022/camt.rs @@ -0,0 +1,1230 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +use std::{fmt::Write as _, str::FromStr}; + +use compact_str::CompactString; +use jiff::{Timestamp, civil, tz::TimeZone}; +use taler_common::types::{ + amount::{Amount, Currency}, + iban::IBAN, + payto::{BankID, IbanPayto, PaytoImpl, PaytoURI}, +}; +use taler_enum_meta::EnumMeta; +use tracing::{trace, warn}; +use uuid::Uuid; + +use crate::{ + iso20022::{ + ChargeBearer, + bank_tx_code::{BankTxDomainCode, BankTxFamilyCode, BankTxSubFamilyCode}, + external_code::ExternalReturnReasonCode, + }, + model::{BatchId, InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, + xml::{self, Xml, XmlAccess as _}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Str)] +enum Kind { + CRDT, + DBIT, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AccountId { + Iban(IBAN), + Other(CompactString), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountTransactions { + pub id: AccountId, + pub currency: Option<Currency>, + pub txs: Vec<Tx>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum OutIds { + Tx(OutId), + Batch(BatchId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct BankTxCode { + domain: BankTxDomainCode, + family: BankTxFamilyCode, + subfamily: BankTxSubFamilyCode, +} + +impl BankTxCode { + fn is_reversal(&self) -> bool { + matches!( + self.subfamily, + BankTxSubFamilyCode::RPCR | BankTxSubFamilyCode::RRTN | BankTxSubFamilyCode::PSTE + ) + } +} + +impl std::fmt::Display for BankTxCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + domain, + family, + subfamily, + } = self; + write!( + f, + "{domain} {family} {subfamily} - '{}' '{}' '{}'", + domain.description(), + family.description(), + subfamily.description() + ) + } +} + +/** Parse the instruction execution date */ +fn execution_date(n: Xml) -> xml::Result<Timestamp> { + // Value date if present else booking date + let date = n + .opt("ValDt") + .transpose() + .unwrap_or_else(|| n.one("BookgDt"))?; + let date = if let Some(date) = date.opt("Dt")? { + date.parse::<civil::Date>()?.into() + } else { + date.one("DtTm").parse::<civil::DateTime>()? + }; + Ok(date.to_zoned(TimeZone::UTC).unwrap().timestamp()) +} + +/** Parse a payto */ +fn payto(n: Xml, prefix: &str) -> xml::Result<Option<PaytoURI>> { + let Some(parties) = n.opt("RltdPties")? else { + return Ok(None); + }; + + let Some(iban) = parties + .opt(&format!("{prefix}Acct")) + .one("Id") + .opt("IBAN") + .parse()? + else { + return Ok(None); + }; + // TODO parse BIC + let bank_id = BankID { iban, bic: None }; + Ok(Some(if let Some(p) = parties.opt(prefix)? { + let name = p + .opt("Nm") + .transpose() + .unwrap_or_else(|| p.one("Pty").one("Nm"))? + .text(); + IbanPayto::new(bank_id).as_full_payto(name) + } else { + IbanPayto::new(bank_id).as_payto() + })) +} + +/** Parse batch message ID and transaction end-to-end ID as generated by libeufin-nexus */ +fn outgoing_id(n: Xml, sref: Option<&str>) -> xml::Result<OutIds> { + Ok(if let Some(refs) = n.opt("Refs")? { + let e2e_id: Option<CompactString> = refs.opt("EndToEndId").parse()?; + let msg_id: Option<CompactString> = refs.opt("MsgId").parse()?; + let sref: Option<CompactString> = + sref.filter(|it| *it != "NOTPROVIDED").map(|it| it.into()); + match (e2e_id, msg_id) { + // This is a batch representation + (None, Some(msg_id)) => OutIds::Batch(BatchId { msg_id, sref }), + // If not set use MsgId as end-to-end ID for retrocompatibility + (Some(e2e_id), msg_id) if &e2e_id == "NOTPROVIDED" => OutIds::Tx(OutId { + e2e_id: msg_id.clone(), + msg_id, + sref, + }), + (e2e_id, msg_id) => OutIds::Tx(OutId { + msg_id, + e2e_id, + sref, + }), + } + } else { + OutIds::Tx(OutId { + msg_id: None, + e2e_id: None, + sref: sref.map(|it| it.into()), + }) + }) +} + +/** Parse transaction ids as provided by bank */ +fn incoming_id(n: Xml, sref: Option<&str>) -> xml::Result<InId> { + if let Some(refs) = n.opt("Refs")? { + let uetr: Option<Uuid> = refs.opt("UETR").parse()?; + let tx_id: Option<CompactString> = refs.opt("TxId").parse()?; + Ok(InId { + uetr, + tx_id, + acct_svcr_ref: sref.map(|it| it.into()), + }) + } else { + Ok(InId { + uetr: None, + tx_id: None, + acct_svcr_ref: sref.map(|it| it.into()), + }) + } +} + +/** Parse transaction wire transfer subject */ +fn wire_transfer_subject(n: Xml) -> xml::Result<Option<Box<str>>> { + Ok(n.opt("RmtInf")?.map(|n| { + n.many("Ustrd") + .map(|n| n.text()) + .collect::<String>() + .into_boxed_str() + })) +} + +/** Parse and format transaction return reasons */ +fn return_reason(n: Xml) -> xml::Result<Box<str>> { + let mut buf = String::new(); + if let Some(n) = n.opt("RtrInf")? { + let code: ExternalReturnReasonCode = n.one("Rsn").one("Cd").parse()?; + + write!(&mut buf, "{code} '{}'", code.description()).unwrap(); + let mut infos = n.many("AddtlInf"); + if let Some(first) = infos.next() { + buf.push_str(" - '"); + buf.push_str(first.text()); + for info in infos { + buf.push_str(info.text()); + } + buf.push('\''); + } + } else if let Some(n) = wire_transfer_subject(n)? { + return Ok(n); + } + Ok(buf.into_boxed_str()) +} +/** Parse amount */ +fn amount(n: Xml) -> xml::Result<Amount> { + let amt = n.one("Amt")?; + let currency = amt.attr("Ccy")?; + let amount = amt.text(); + let concat = format!("{currency}:0{amount}"); + Amount::from_str(&concat).map_err(|e| amt.parse_err(e)) +} + +#[derive(Debug, Clone, Copy)] +struct ComplexAmount { + /// Transaction amount + amount: Amount, + /// The applied fee + fee: Amount, +} + +impl ComplexAmount { + /// Check that entry and tx amount are compatible and return the result + fn resolve(&self, tx: &ComplexAmount) -> xml::Result<ComplexAmount> { + // Most time transaction will match + if self.amount == tx.amount && self.fee == tx.fee { + return Ok(*self); + } + + // Or one of the level is missing the fee + if (tx.amount.decimal() > tx.fee.decimal() + && tx.amount.try_sub(&tx.fee).unwrap() == self.amount) + || self.amount.try_sub(&self.fee).unwrap() == tx.amount + { + return if tx.fee.is_zero() { Ok(*self) } else { Ok(*tx) }; + } + + // Or the conversion information are only present at the entry layer + if tx.amount.currency != self.amount.currency { + return Ok(*self); + } + + panic!("Amount mismatch, got {self:?} in the entry and {tx:?} in the tx") + } +} + +struct ChargeRecord { + amount: Amount, + kind: Kind, + included: bool, + bearer: ChargeBearer, +} + +fn charges(n: Xml) -> xml::Result<Vec<ChargeRecord>> { + if let Some(n) = n.opt("Chrgs")? { + n.many("Rcrd") + .map(|n| { + Ok(ChargeRecord { + amount: amount(n)?, + kind: n.opt("CdtDbtInd").parse()?.unwrap_or(Kind::CRDT), + included: n.opt("ChrgInclInd").parse()? == Some(true), // TODO not clear in spec + bearer: n.opt("Br").parse()?.unwrap_or(ChargeBearer::SHAR), + }) + }) + .collect() + } else { + Ok(Vec::new()) + } +} + +fn complex_amount(amt: Xml, charges: &[ChargeRecord]) -> xml::Result<ComplexAmount> { + // Amount before charges + let currency = amt.attr("Ccy")?; + // In case of fee overflow it's possible to have a negative amount here + // We ignore this as it will be handled elsewhere correctly + let amount = amt.text().trim_start_matches('-'); + let concat = format!("{currency}:0{amount}"); + + let mut amount = Amount::from_str(&concat).map_err(|e| amt.parse_err(e))?; + let mut fee = Amount::zero(&amount.currency); + + for chr in charges { + if chr.included && !chr.amount.is_zero() { + fee = fee.try_add(&chr.amount).expect("Should never overflow"); + if chr.kind == Kind::DBIT { + if chr.bearer == ChargeBearer::DEBT { + if chr.amount.decimal() > amount.decimal() { + // This can happen when an incoming transaction fail because of debit fee + amount = chr.amount.try_sub(&amount).expect("Should never overflow"); + } else { + amount = amount.try_sub(&chr.amount).expect("Should never overflow"); + } + } else if chr.bearer == ChargeBearer::CRED { + amount = amount.try_add(&chr.amount).expect("Should never overflow"); + } else { + return Err(amt.parse_err(format_args!( + "Included charge {} with bearer {}", + chr.kind, chr.bearer + ))); + } + } + } + } + + Ok(ComplexAmount { amount, fee }) +} + +/** Parse bank transaction code */ +fn bank_tx_code(n: Xml) -> xml::Result<BankTxCode> { + let domnd = n.one("Domn")?; + let fmly = domnd.one("Fmly")?; + Ok(BankTxCode { + domain: domnd.one("Cd").parse()?, + family: fmly.one("Cd").parse()?, + subfamily: fmly.one("SubFmlyCd").parse()?, + }) +} + +/** Parse camt files */ +pub fn parse_txs(xml: &str) -> xml::Result<Vec<AccountTransactions>> { + /* + In ISO 20022 specifications, most fields are optional and the same information + can be written several times in different places. For libeufin, we're only + interested in a subset of the available values that can be found in both camt.052, + camt.053 and camt.054. This function should not fail on legitimate files and should + simply warn when available information are insufficient. + + EBICS and ISO20022 do not provide a perfect transaction identifier. The best is the + UETR (unique end-to-end transaction reference), which is a universally unique + identifier (UUID). However, it is not supplied by all banks. TxId (TransactionIdentification) + is a unique identification as assigned by the first instructing agent. As its format + is ambiguous, its uniqueness is not guaranteed by the standard, and it is only + supposed to be unique for a “pre-agreed period”, whatever that means. These two + identifiers are optional in the standard, but have the advantage of being unique + and can be used to track a transaction between banks so we use them when available. + + It is also possible to use AccountServicerReference, which is a unique reference + assigned by the account servicing institution. They can be present at several levels + (batch level, transaction level, etc.) and are often optional. They also have the + disadvantage of being known only by the account servicing institution. They should + therefore only be used as a last resort. + */ + trace!("Parse transactions camt file"); + + fn parse_inner(root: Xml) -> xml::Result<AccountTransactions> { + let (id, currency) = { + let account = root.one("Acct")?; + let id = account.one("Id")?; + let account_id = if let Some(iban) = id.opt("IBAN")? { + AccountId::Iban(iban.parse()?) + } else { + AccountId::Other(id.one("Othr").one("Id").parse()?) + }; + let currency: Option<Currency> = account.opt("Ccy").parse()?; + (account_id, currency) + }; + let txs = root.many("Ntry").try_fold(Vec::new(), |mut txs, entry| { + // Skip if not booked + if !{ + let status = entry.one("Sts")?; + let status = status + .opt("Cd")? + .map(|n| n.text()) + .unwrap_or_else(|| status.text()); + status == "BOOK" + } { + return Ok(txs); + } + + let reversal = entry.opt("RvslInd").parse()? == Some(true); + let entry_code = bank_tx_code(entry.one("BkTxCd")?)?; + let entry_kind = entry.opt("CdtDbtInd").parse::<Kind>()?; + let entry_ref = entry.opt("AcctSvcrRef").parse::<CompactString>()?; + let date = execution_date(entry)?; + let entry_charges = charges(entry)?; + let entry_amount = complex_amount(entry.one("Amt")?, &entry_charges)?; + + let Some(details) = entry.opt("NtryDtls")? else { + return Ok(txs); + }; + // When an entry only contain a single transactions information will sometimes only be stored at the entry level + let unique = details.many("TxDtls").count() == 1; + for tx in details.many("TxDtls") { + // Check information are present and coherent + let kind = tx.opt("CdtDbtInd").parse()?.or(entry_kind).unwrap(); + + // Sometimes the transaction level have a more precise bank transaction code + let code = tx + .opt("BkTxCd")? + .map(bank_tx_code) + .transpose()? + .unwrap_or(entry_code); + + let tx_charges = charges(tx)?; + // Amount + let amount = if unique { + // When unique the charges can be only at the entry level + if let Some(amt) = tx.opt("Amt")? { + let tx_amount = complex_amount( + amt, + if tx_charges.is_empty() { + &entry_charges + } else { + &tx_charges + }, + )?; + // Check coherence + entry_amount.resolve(&tx_amount)? + } else { + entry_amount + } + } else { + // When many inner transaction the entry level is an aggregate of them + // We only use the transaction level information + complex_amount(tx.one("Amt")?, &tx_charges)? + }; + + // We can only use the entry ref as the transaction ref if there is a single transaction in the batch + let sref: Option<CompactString> = tx + .opt("Refs") + .opt("AcctSvcrRef") + .parse::<CompactString>()? + .or_else(|| unique.then(|| entry_ref.clone()).flatten()); + + match (kind, code.is_reversal() || reversal) { + (Kind::CRDT, true) => { + let out_id = outgoing_id(tx, sref.as_deref())?; + if let OutIds::Tx(OutId { + msg_id, + e2e_id: Some(e2e_id), + .. + }) = out_id + { + txs.push(Tx::Reversal(OutReversal { + e2e_id, + msg_id, + reason: return_reason(tx)?, + execution_time: date, + })) + } else { + warn!("missing unique ID for Credit reversal {out_id:?}"); + } + } + (Kind::DBIT, true) | (Kind::CRDT, false) => { + let id = incoming_id(tx, sref.as_deref())?; + if id.uetr.is_none() && id.tx_id.is_none() && id.acct_svcr_ref.is_none() { + warn!("missing unique ID for Credit") + } else { + txs.push(Tx::In(InTx { + id, + amount: amount.amount, + credit_fee: amount.fee, + subject: wire_transfer_subject(tx)?, + execution_time: date, + debtor: payto(tx, "Dbtr")?, + })); + } + } + (Kind::DBIT, false) => { + let id = outgoing_id(tx, sref.as_deref())?; + match id { + OutIds::Tx(id) => { + if id.e2e_id.is_none() && id.msg_id.is_none() && id.sref.is_none() { + warn!("missing unique ID for Debit") + } else { + txs.push(Tx::Out(OutTx { + id, + amount: amount.amount, + debit_fee: amount.fee, + subject: wire_transfer_subject(tx)?, + execution_time: date, + creditor: payto(tx, "Cdtr")?, + })); + } + } + OutIds::Batch(BatchId { msg_id, .. }) => { + txs.push(Tx::Batch(OutBatch { + msg_id, + execution_time: date, + })); + } + } + } + } + } + Ok(txs) + })?; + Ok(AccountTransactions { id, currency, txs }) + } + + Xml::parse_str(xml, "Document", |root| { + if let Some(camt053) = root.opt("BkToCstmrStmt")? { + camt053.many("Stmt").map(parse_inner).collect() + } else if let Some(camt052) = root.opt("BkToCstmrAcctRpt")? { + camt052.many("Rpt").map(parse_inner).collect() + } else if let Some(camt054) = root.opt("BkToCstmrDbtCdtNtfctn")? { + camt054.many("Ntfctn").map(parse_inner).collect() + } else { + Err(root.parse_err("Malformed camt file")) + } + }) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use jiff::civil::Date; + use taler_common::types::{ + amount::{Amount, Currency}, + iban::IBAN, + payto::iban_payto, + utils::date_to_utc_ts, + }; + + use crate::{ + iso20022::camt::{AccountId, parse_txs}, + model::{InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, + }; + + pub fn check_tx(path: &str, iban: &str, currency: Option<&str>, txs: &[Tx]) { + let content = std::fs::read_to_string(path).unwrap(); + let res = parse_txs(&content).unwrap(); + assert_eq!(res.len(), 1); + + let first = &res[0]; + assert_eq!(first.id, AccountId::Iban(IBAN::from_str(iban).unwrap())); + assert_eq!( + first.currency, + currency.map(|it| Currency::from_str(it).unwrap()) + ); + pretty_assertions::assert_eq!(first.txs, txs); + } + + pub fn tx_out( + id: (Option<&str>, Option<&str>, Option<&str>), + amount: &str, + debit_fee: &str, + subject: Option<&str>, + execution_time: &str, + creditor: Option<(&str, &str)>, + ) -> Tx { + Tx::Out(OutTx { + id: OutId::new( + id.0.map(Into::into), + id.1.map(Into::into), + id.2.map(Into::into), + ), + amount: Amount::from_str(amount).unwrap(), + debit_fee: Amount::from_str(debit_fee).unwrap(), + subject: subject.map(Into::into), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + creditor: creditor.map(|(iban, name)| iban_payto(iban, name)), + }) + } + + pub fn tx_in( + id: (Option<&str>, Option<&str>, Option<&str>), + amount: &str, + credit_fee: &str, + subject: Option<&str>, + execution_time: &str, + debtor: Option<(&str, &str)>, + ) -> Tx { + Tx::In(InTx { + id: InId::new( + id.0.map(|it| it.parse().unwrap()), + id.1.map(Into::into), + id.2.map(Into::into), + ), + amount: Amount::from_str(amount).unwrap(), + credit_fee: Amount::from_str(credit_fee).unwrap(), + subject: subject.map(Into::into), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + debtor: debtor.map(|(iban, name)| iban_payto(iban, name)), + }) + } + + pub fn tx_reversal( + e2e_id: &str, + msg_id: Option<&str>, + reason: &str, + execution_time: &str, + ) -> Tx { + Tx::Reversal(OutReversal { + e2e_id: e2e_id.parse().unwrap(), + msg_id: msg_id.map(Into::into), + reason: reason.into(), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + }) + } + + pub fn tx_batch(msg_id: &str, execution_time: &str) -> Tx { + Tx::Batch(OutBatch { + msg_id: msg_id.into(), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + }) + } + + #[test] + fn postfinance_camt054() { + check_tx( + "libeufin-nexus/sample/platform/postfinance_camt054.xml", + "CH9289144596463965762", + Some("CHF"), + &[ + tx_out( + ( + Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), + Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), + None, + ), + "CHF:3.00", + "CHF:0", + None, + "2024-01-15", + None, + ), + tx_in( + ( + Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"), + None, + Some("231121CH0AZWCR9T"), + ), + "CHF:10", + "CHF:0", + Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG"), + "2023-12-19", + Some(("CH7389144832588726658", "Mr Test")), + ), + tx_in( + ( + Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"), + None, + Some("231121CH0AZWCVR1"), + ), + "CHF:2.53", + "CHF:0", + Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB"), + "2023-12-19", + Some(("CH7389144832588726658", "Mr Test")), + ), + tx_reversal( + "50820f78-9024-44ff-978d-63a18c", + Some("50820f78-9024-44ff-978d-63a18c"), + "", + "2024-01-15", + ), + tx_batch("ZS1PGNTSV0ZNDFAJBBWWB8015G", "2024-01-15"), + ], + ); + } + + #[test] + fn postfinance_camt053() { + check_tx( + "libeufin-nexus/sample/platform/postfinance_camt053.xml", + "CH9289144596463965762", + Some("CHF"), + &[ + tx_reversal( + "889d1a80-1267-49bd-8fcc-85701a", + Some("889d1a80-1267-49bd-8fcc-85701a"), + "InconsistenWithEndCustomer 'Identification of end customer is not consistent with associated account number, organisation ID or private ID' - 'more info here ...'", + "2023-11-22", + ), + tx_reversal( + "4cc61cc7-6230-49c2-b5e2-b40bbb", + Some("4cc61cc7-6230-49c2-b5e2-b40bbb"), + "MissingCreditorNameOrAddress 'Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing' - 'more info here ...'", + "2023-11-22", + ), + tx_batch("EB4D22D428214261B2B3012D2A8CEC36", "2024-08-26"), + ], + ); + } + + #[test] + fn raiffeisen_camt053() { + check_tx( + "libeufin-nexus/sample/platform/raiffeisen_camt053.xml", + "CH7389144832588726658", + None, + &[ + tx_in( + (None, None, Some("A200020494367552")), + "CHF:20000", + "CHF:0", + Some("1. TZ 2025"), + "2025-12-23", + Some(("CH7389144832588726658", "KANTON BERN")), + ), + tx_out( + (None, None, Some("19868398389")), + "CHF:15", + "CHF:0", + None, + "2025-12-31", + None, + ), + tx_out( + (None, None, Some("19890406743")), + "CHF:2", + "CHF:0", + None, + "2025-12-31", + None, + ), + tx_out( + (None, None, Some("19885172770")), + "CHF:3", + "CHF:0", + None, + "2025-12-31", + None, + ), + ], + ); + } + + #[test] + fn valiant_camt052() { + check_tx( + "libeufin-nexus/sample/platform/valiant_camt052.xml", + "CH7389144832588726658", + Some("CHF"), + &[ + tx_out( + ( + Some("MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L"), + Some("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE"), + Some("ZV20251030/511372/1"), + ), + "CHF:0.1", + "CHF:0", + Some("single 2025-10-30T09:46:04.55293090 9Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), + Some("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"), + Some("ZV20251030/511373/1"), + ), + "CHF:0.1", + "CHF:0", + Some("multi 0 2025-10-30T09:46:10.3877961 30Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), + Some("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"), + Some("ZV20251030/511373/2"), + ), + "CHF:0.11", + "CHF:0", + Some("multi 1 2025-10-30T09:46:10.3877961 30Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), + Some("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"), + Some("ZV20251030/511373/3"), + ), + "CHF:0.12", + "CHF:0", + Some("multi 2 2025-10-30T09:46:10.3877961 30Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"), + Some("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"), + Some("ZV20251030/511373/4"), + ), + "CHF:0.13", + "CHF:0", + Some("multi 3 2025-10-30T09:46:10.3877961 30Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_in( + (None, Some("51030655601.0001"), Some("ZV20251030/514778/1")), + "CHF:0.85", + "CHF:0", + Some("fun stuff"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_in( + (None, Some("51030655601.0002"), Some("ZV20251030/514779/1")), + "CHF:0.95", + "CHF:0", + Some("Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("X166701F6RV59LP71RVWVIW9SV2AFZYLG4"), + Some("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX"), + Some("ZV20251030/524078/1"), + ), + "CHF:0.21", + "CHF:0", + Some("bad name 2025-10-30T12:03:24.997478 811Z"), + "2025-10-30", + Some(("CH6208704048981247126", "John Smith")), + ), + tx_out( + ( + Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), + Some("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"), + Some("ZV20251030/524079/1"), + ), + "CHF:0.1", + "CHF:0", + Some("single 2025-10-30T12:04:00.37042083 6Z"), + "2025-10-30", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), + Some("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"), + Some("ZV20251030/524079/2"), + ), + "CHF:0.21", + "CHF:0", + Some("bad name 2025-10-30T12:03:53.042190 686Z"), + "2025-10-30", + Some(("CH6208704048981247126", "John Smith")), + ), + tx_reversal( + "XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y", + None, + "Error msg in german", + "2025-10-30", + ), + tx_reversal( + "R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX", + None, + "Error msg in german", + "2025-10-30", + ), + tx_out( + ( + Some("OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P"), + Some("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7"), + Some("ZV20251030/524077/1"), + ), + "CHF:0.23", + "CHF:5", + Some("foreign iban 2025-10-30T12:03:44.0972 63765Z"), + "2025-10-30", + Some(("DE48330605920000686018", "Christian Grothoff")), + ), + tx_out( + ( + Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"), + Some("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"), + Some("ZV20251030/524080/1"), + ), + "CHF:0.23", + "CHF:5", + Some("foreign iban 2025-10-30T12:03:58.0046 73747Z"), + "2025-10-30", + Some(("DE48330605920000686018", "Christian Grothoff")), + ), + tx_in( + ( + Some("7b76d488-05d5-44ab-9d77-31d4165ec158"), + Some("00204EQY370"), + Some("ZV20251118/685062/1"), + ), + "CHF:4.55", + "CHF:0", + Some("TEST"), + "2025-11-18", + None, + ), + ], + ) + } + + #[test] + fn gls_camt052() { + check_tx( + "libeufin-nexus/sample/platform/gls_camt052.xml", + "DE84500105177118117964", + Some("EUR"), + &[ + tx_out( + ( + Some("COMPAT_SUCCESS"), + Some("COMPAT_SUCCESS"), + Some("2024041801514102000"), + ), + "EUR:2", + "EUR:0", + Some("TestABC123"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + tx_reversal( + "8XK8Z7RAX224FGWK832FD40GYC", + None, + "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", + "2024-09-05", + ), + tx_in( + ( + None, + Some("BYLADEM1WOR-G2910276709458A2"), + Some("2024041210041357000"), + ), + "EUR:3", + "EUR:0", + Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"), + "2024-04-12", + Some(("DE84500105177118117964", "John Smith")), + ), + tx_reversal( + "COMPAT_FAILURE", + None, + "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", + "2024-04-12", + ), + tx_out( + ( + Some("BATCH_SINGLE_SUCCESS"), + Some("FD622SMXKT5QWSAHDY0H8NYG3G"), + Some("2024090216552232000"), + ), + "EUR:1.1", + "EUR:0", + Some("single 2024-09-02T14:29:52.875253314Z"), + "2024-09-02", + Some(("DE89500105173198527518", "Grothoff Hans")), + ), + tx_out( + ( + Some("YF5QBARGQ0MNY0VK59S477VDG4"), + Some("YF5QBARGQ0MNY0VK59S477VDG4"), + Some("2024041810552821000"), + ), + "EUR:1.1", + "EUR:0", + Some("Simple tx"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + tx_batch("BATCH_MANY_SUCCESS", "2024-09-20"), + tx_out( + ( + Some("BATCH_SINGLE_RETURN"), + Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"), + Some("2024092100252498000"), + ), + "EUR:0.42", + "EUR:0", + Some("This should fail because bad iban"), + "2024-09-23", + Some(("DE18500105173385245163", "John Smith")), + ), + tx_reversal( + "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + None, + "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", + "2024-09-24", + ), + ], + ) + } + + #[test] + fn gls_camt053() { + check_tx( + "libeufin-nexus/sample/platform/gls_camt053.xml", + "DE84500105177118117964", + Some("EUR"), + &[ + tx_out( + ( + Some("COMPAT_SUCCESS"), + Some("COMPAT_SUCCESS"), + Some("2024041801514102000"), + ), + "EUR:2", + "EUR:0", + Some("TestABC123"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + tx_reversal( + "KGTDBASWTJ6JM89WXD3Q5KFQC4", + None, + "Retoure aus SEPA Überweisung multi line", + "2024-09-04", + ), + tx_batch("BATCH_MANY_PART", "2024-09-04"), + tx_in( + ( + None, + Some("BYLADEM1WOR-G2910276709458A2"), + Some("2024041210041357000"), + ), + "EUR:3", + "EUR:0", + Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"), + "2024-04-12", + Some(("DE84500105177118117964", "John Smith")), + ), + tx_reversal( + "COMPAT_FAILURE", + None, + "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", + "2024-04-12", + ), + tx_out( + ( + Some("BATCH_SINGLE_SUCCESS"), + Some("FD622SMXKT5QWSAHDY0H8NYG3G"), + Some("2024090216552232000"), + ), + "EUR:1.1", + "EUR:0", + Some("single 2024-09-02T14:29:52.875253314Z"), + "2024-09-02", + Some(("DE89500105173198527518", "Grothoff Hans")), + ), + tx_out( + ( + Some("YF5QBARGQ0MNY0VK59S477VDG4"), + Some("YF5QBARGQ0MNY0VK59S477VDG4"), + Some("2024041810552821000"), + ), + "EUR:1.1", + "EUR:0", + Some("Simple tx"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + ], + ) + } + + #[test] + fn gls_camt054() { + check_tx( + "libeufin-nexus/sample/platform/gls_camt054.xml", + "DE84500105177118117964", + Some("EUR"), + &[tx_in( + (None, Some("IS11PGENODEFF2DA8899900378806"), None), + "EUR:2.5", + "EUR:0", + Some("Test ICT"), + "2024-05-05", + Some(("DE84500105177118117964", "Mr Test")), + )], + ); + } + + #[test] + fn maerki_baumann_camt053() { + check_tx( + "libeufin-nexus/sample/platform/maerki_baumann_camt053.xml", + "CH7389144832588726658", + Some("CHF"), + &[ + tx_in( + ( + Some("adbe4a5a-6cea-4263-b259-8ab964561a32"), + Some("41103099704.0002"), + Some("ZV20241104/765446/1"), + ), + "CHF:1", + "CHF:0.2", + Some("SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G"), + "2024-11-04", + Some(("CH7389144832588726658", "Mr Test")), + ), + tx_in( + ( + Some("7371795e-62fa-42dd-93b7-da89cc120faa"), + Some("41103099704.0003"), + Some("ZV20241104/765447/1"), + ), + "CHF:1", + "CHF:0.2", + Some("Random subject"), + "2024-11-04", + Some(("CH7389144832588726658", "Mr Test")), + ), + tx_in( + (None, Some("50523424675.0001"), Some("ZV20250523/851716/1")), + "CHF:0.5", + "CHF:0.2", + None, + "2025-05-23", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("BATCH_SINGLE_REPORTING"), + Some("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"), + Some("ZV20241121/773541/1"), + ), + "CHF:0.1", + "CHF:0", + Some("multi 0 2024-11-21T15:21:59.8859234 63Z"), + "2024-11-27", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("BATCH_SINGLE_REPORTING"), + Some("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"), + Some("ZV20241121/773541/4"), + ), + "CHF:0.13", + "CHF:0", + Some("multi 3 2024-11-21T15:21:59.8859234 63Z"), + "2024-11-27", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("BATCH_SINGLE_REPORTING"), + Some("A09R35EW0359SZ51464E7TC37A0P2CBK04"), + Some("ZV20241121/773541/3"), + ), + "CHF:0.12", + "CHF:0", + Some("multi 2 2024-11-21T15:21:59.8859234 63Z"), + "2024-11-27", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + ( + Some("BATCH_SINGLE_REPORTING"), + Some("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"), + Some("ZV20241121/773541/2"), + ), + "CHF:0.11", + "CHF:0", + Some("multi 1 2024-11-21T15:21:59.8859234 63Z"), + "2024-11-27", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_in( + ( + Some("f203fbb4-6e13-4c78-9b2a-d852fea6374a"), + Some("41202060702.0001"), + Some("ZV20241202/778108/1"), + ), + "CHF:0.05", + "CHF:0.2", + Some("mini"), + "2024-12-02", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_in( + ( + Some("81b0d8c6-a677-4577-b75e-a639dcc03681"), + Some("41120636093.0001"), + Some("ZV20241121/773118/1"), + ), + "CHF:0.1", + "CHF:0.2", + Some("small transfer test"), + "2024-11-21", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + (None, None, Some("GB20241220/205792/1")), + "CHF:3000", + "CHF:0", + None, + "2024-12-20", + None, + ), + tx_in( + (None, None, Some("ZV20250114/796191/1")), + "CHF:3003", + "CHF:0", + Some("Fix bad payment by MB."), + "2025-01-27", + None, + ), + tx_in( + (None, Some("F000787951230001"), Some("ZV20250526/852733/1")), + "CHF:1.38", + "CHF:0.2", + Some("Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G"), + "2025-05-26", + Some(("DE20500105172419259181", "Mr German")), + ), + ], + ) + } +} diff --git a/src/iso20022/external_code.rs b/src/iso20022/external_code.rs @@ -19,2276 +19,1356 @@ // THIS FILE IS GENERATED, DO NOT EDIT -use strum_macros::{AsRefStr, Display, EnumString}; +use taler_enum_meta::EnumMeta; -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(DomainCode, Description, Str)] pub enum ExternalStatusReasonCode { /// Clearing process aborted due to timeout - #[strum(serialize = "AB01")] + #[code = "AB01"] AbortedClearingTimeout, /// Clearing process aborted due to a fatal error - #[strum(serialize = "AB02")] + #[code = "AB02"] AbortedClearingFatalError, /// Settlement aborted due to timeout - #[strum(serialize = "AB03")] + #[code = "AB03"] AbortedSettlementTimeout, /// Settlement process aborted due to a fatal error - #[strum(serialize = "AB04")] + #[code = "AB04"] AbortedSettlementFatalError, /// Transaction stopped due to timeout at the Creditor Agent - #[strum(serialize = "AB05")] + #[code = "AB05"] TimeoutCreditorAgent, /// Transaction stopped due to timeout at the Instructed Agent - #[strum(serialize = "AB06")] + #[code = "AB06"] TimeoutInstructedAgent, /// Agent of message is not online - #[strum(serialize = "AB07")] + #[code = "AB07"] OfflineAgent, /// Creditor Agent is not online - #[strum(serialize = "AB08")] + #[code = "AB08"] OfflineCreditorAgent, /// Transaction stopped due to error at the Creditor Agent - #[strum(serialize = "AB09")] + #[code = "AB09"] ErrorCreditorAgent, /// Transaction stopped due to error at the Instructed Agent - #[strum(serialize = "AB10")] + #[code = "AB10"] ErrorInstructedAgent, /// Transaction stopped due to timeout at the Debtor Agent - #[strum(serialize = "AB11")] + #[code = "AB11"] TimeoutDebtorAgent, /// Duplicate Concurrent Batch Sequence number– for Settlement Instructions - #[strum(serialize = "AB12")] + #[code = "AB12"] InvalidConcurrentBatch, /// Wrong Message Routing Type for Return-of-Funds - #[strum(serialize = "AB13")] + #[code = "AB13"] InvalidRoutingCodeUtilised, /// Instruction may not be placed on the Continuous Processing Line settlement processor - #[strum(serialize = "AB15")] + #[code = "AB15"] InvalidAccountNumberForSettlementType, /// Agreement number not valid (beneficiary) - #[strum(serialize = "AB21")] + #[code = "AB21"] InvalidSettlementAgreementNumberSpecified, /// Settlement Instruction does not exist - #[strum(serialize = "AB26")] + #[code = "AB26"] InvalidBatchSettlementInstruction, /// Account number is invalid or missing - #[strum(serialize = "AC01")] + #[code = "AC01"] IncorrectAccountNumber, /// Debtor account number invalid or missing - #[strum(serialize = "AC02")] + #[code = "AC02"] InvalidDebtorAccountNumber, /// Creditor account number invalid or missing - #[strum(serialize = "AC03")] + #[code = "AC03"] InvalidCreditorAccountNumber, /// Account number specified has been closed on the bank of account's books - #[strum(serialize = "AC04")] + #[code = "AC04"] ClosedAccountNumber, /// Debtor account number closed - #[strum(serialize = "AC05")] + #[code = "AC05"] ClosedDebtorAccountNumber, /// Account specified is blocked, prohibiting posting of transactions against it - #[strum(serialize = "AC06")] + #[code = "AC06"] BlockedAccount, /// Creditor account number closed - #[strum(serialize = "AC07")] + #[code = "AC07"] ClosedCreditorAccountNumber, /// Branch code is invalid or missing - #[strum(serialize = "AC08")] + #[code = "AC08"] InvalidBranchCode, /// Account currency is invalid or missing - #[strum(serialize = "AC09")] + #[code = "AC09"] InvalidAccountCurrency, /// Debtor account currency is invalid or missing - #[strum(serialize = "AC10")] + #[code = "AC10"] InvalidDebtorAccountCurrency, /// Creditor account currency is invalid or missing - #[strum(serialize = "AC11")] + #[code = "AC11"] InvalidCreditorAccountCurrency, /// Account type missing or invalid - #[strum(serialize = "AC12")] + #[code = "AC12"] InvalidAccountType, /// Debtor account type missing or invalid - #[strum(serialize = "AC13")] + #[code = "AC13"] InvalidDebtorAccountType, /// Creditor account type missing or invalid - #[strum(serialize = "AC14")] + #[code = "AC14"] InvalidCreditorAccountType, /// The account details for the counterparty have changed - #[strum(serialize = "AC15")] + #[code = "AC15"] AccountDetailsChanged, /// Credit or debit card number is invalid - #[strum(serialize = "AC16")] + #[code = "AC16"] CardNumberInvalid, /// Request-to-pay Expiry Date and Time has already passed - #[strum(serialize = "AEXR")] + #[code = "AEXR"] AlreadyExpiredRTP, /// Transaction forbidden on this type of account (formerly NoAgreement) - #[strum(serialize = "AG01")] + #[code = "AG01"] TransactionForbidden, /// Bank Operation code specified in the message is not valid for receiver - #[strum(serialize = "AG02")] + #[code = "AG02"] InvalidBankOperationCode, /// Transaction type not supported/authorized on this account - #[strum(serialize = "AG03")] + #[code = "AG03"] TransactionNotSupported, /// Agent country code is missing or invalid - #[strum(serialize = "AG04")] + #[code = "AG04"] InvalidAgentCountry, /// Debtor agent country code is missing or invalid - #[strum(serialize = "AG05")] + #[code = "AG05"] InvalidDebtorAgentCountry, /// Creditor agent country code is missing or invalid - #[strum(serialize = "AG06")] + #[code = "AG06"] InvalidCreditorAgentCountry, /// Debtor account cannot be debited for a generic reason - #[strum(serialize = "AG07")] + #[code = "AG07"] UnsuccesfulDirectDebit, /// Transaction failed due to invalid or missing user or access right - #[strum(serialize = "AG08")] + #[code = "AG08"] InvalidAccessRights, /// Original payment never received - #[strum(serialize = "AG09")] + #[code = "AG09"] PaymentNotReceived, /// Agent of message is suspended from the Real Time Payment system - #[strum(serialize = "AG10")] + #[code = "AG10"] AgentSuspended, /// Creditor Agent of message is suspended from the Real Time Payment system - #[strum(serialize = "AG11")] + #[code = "AG11"] CreditorAgentSuspended, /// Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed - #[strum(serialize = "AG12")] + #[code = "AG12"] NotAllowedBookTransfer, /// Returned payments derived from previously returned transactions are not allowed - #[strum(serialize = "AG13")] + #[code = "AG13"] ForbiddenReturnPayment, /// Agent in the payment workflow is incorrect - #[strum(serialize = "AGNT")] + #[code = "AGNT"] IncorrectAgent, /// Request-to-pay has already been accepted by the Debtor - #[strum(serialize = "ALAC")] + #[code = "ALAC"] AlreadyAcceptedRTP, /// Specified message amount is equal to zero - #[strum(serialize = "AM01")] + #[code = "AM01"] ZeroAmount, /// Specific transaction/message amount is greater than allowed maximum - #[strum(serialize = "AM02")] + #[code = "AM02"] NotAllowedAmount, /// Specified message amount is an non processable currency outside of existing agreement - #[strum(serialize = "AM03")] + #[code = "AM03"] NotAllowedCurrency, /// Amount of funds available to cover specified message amount is insufficient - #[strum(serialize = "AM04")] + #[code = "AM04"] InsufficientFunds, /// Duplication - #[strum(serialize = "AM05")] + #[code = "AM05"] Duplication, /// Specified transaction amount is less than agreed minimum - #[strum(serialize = "AM06")] + #[code = "AM06"] TooLowAmount, /// Amount specified in message has been blocked by regulatory authorities - #[strum(serialize = "AM07")] + #[code = "AM07"] BlockedAmount, /// Amount received is not the amount agreed or expected - #[strum(serialize = "AM09")] + #[code = "AM09"] WrongAmount, /// Sum of instructed amounts does not equal the control sum - #[strum(serialize = "AM10")] + #[code = "AM10"] InvalidControlSum, /// Transaction currency is invalid or missing - #[strum(serialize = "AM11")] + #[code = "AM11"] InvalidTransactionCurrency, /// Amount is invalid or missing - #[strum(serialize = "AM12")] + #[code = "AM12"] InvalidAmount, /// Transaction amount exceeds limits set by clearing system - #[strum(serialize = "AM13")] + #[code = "AM13"] AmountExceedsClearingSystemLimit, /// Transaction amount exceeds limits agreed between bank and client - #[strum(serialize = "AM14")] + #[code = "AM14"] AmountExceedsAgreedLimit, /// Transaction amount below minimum set by clearing system - #[strum(serialize = "AM15")] + #[code = "AM15"] AmountBelowClearingSystemMinimum, /// Control Sum at the Group level is invalid - #[strum(serialize = "AM16")] + #[code = "AM16"] InvalidGroupControlSum, /// Control Sum at the Payment Information level is invalid - #[strum(serialize = "AM17")] + #[code = "AM17"] InvalidPaymentInfoControlSum, /// Number of transactions is invalid or missing - #[strum(serialize = "AM18")] + #[code = "AM18"] InvalidNumberOfTransactions, /// Number of transactions at the Group level is invalid or missing - #[strum(serialize = "AM19")] + #[code = "AM19"] InvalidGroupNumberOfTransactions, /// Number of transactions at the Payment Information level is invalid - #[strum(serialize = "AM20")] + #[code = "AM20"] InvalidPaymentInfoNumberOfTransactions, /// Transaction amount exceeds limits agreed between bank and client - #[strum(serialize = "AM21")] + #[code = "AM21"] LimitExceeded, /// Unable to apply zero amount to designated account - #[strum(serialize = "AM22")] + #[code = "AM22"] ZeroAmountNotApplied, /// Transaction amount exceeds settlement limit - #[strum(serialize = "AM23")] + #[code = "AM23"] AmountExceedsSettlementLimit, /// Size of the attachment exceeds the allowed maximum - #[strum(serialize = "AMSE")] + #[code = "AMSE"] AttachmentMaximumSize, /// Request To Pay has already been paid by the Debtor - #[strum(serialize = "APAR")] + #[code = "APAR"] AlreadyPaidRTP, /// Request-to-pay has already been refused by the Debtor - #[strum(serialize = "ARFR")] + #[code = "ARFR"] AlreadyRefusedRTP, /// Request-to-pay has already been rejected - #[strum(serialize = "ARJR")] + #[code = "ARJR"] AlreadyRejectedRTP, /// Attachments to the request-to-pay are not supported - #[strum(serialize = "ATNS")] + #[code = "ATNS"] AttachementsNotSupported, /// Settlement Cycle Day and Calendar day should be the same - #[strum(serialize = "BDAY")] + #[code = "BDAY"] NotBusinessDay, /// Identification of end customer is not consistent with associated account number - #[strum(serialize = "BE01")] + #[code = "BE01"] InconsistenWithEndCustomer, /// Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress) - #[strum(serialize = "BE04")] + #[code = "BE04"] MissingCreditorAddress, /// Party who initiated the message is not recognised by the end customer - #[strum(serialize = "BE05")] + #[code = "BE05"] UnrecognisedInitiatingParty, /// End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books - #[strum(serialize = "BE06")] + #[code = "BE06"] UnknownEndCustomer, /// Specification of debtor's address, which is required for payment, is missing/not correct - #[strum(serialize = "BE07")] + #[code = "BE07"] MissingDebtorAddress, /// Debtor name is missing - #[strum(serialize = "BE08")] + #[code = "BE08"] MissingDebtorName, /// Country code is missing or Invalid - #[strum(serialize = "BE09")] + #[code = "BE09"] InvalidCountry, /// Debtor country code is missing or invalid - #[strum(serialize = "BE10")] + #[code = "BE10"] InvalidDebtorCountry, /// Creditor country code is missing or invalid - #[strum(serialize = "BE11")] + #[code = "BE11"] InvalidCreditorCountry, /// Country code of residence is missing or Invalid - #[strum(serialize = "BE12")] + #[code = "BE12"] InvalidCountryOfResidence, /// Country code of debtor's residence is missing or Invalid - #[strum(serialize = "BE13")] + #[code = "BE13"] InvalidDebtorCountryOfResidence, /// Country code of creditor's residence is missing or Invalid - #[strum(serialize = "BE14")] + #[code = "BE14"] InvalidCreditorCountryOfResidence, /// Identification code missing or invalid - #[strum(serialize = "BE15")] + #[code = "BE15"] InvalidIdentificationCode, /// Debtor or Ultimate Debtor identification code missing or invalid - #[strum(serialize = "BE16")] + #[code = "BE16"] InvalidDebtorIdentificationCode, /// Creditor or Ultimate Creditor identification code missing or invalid - #[strum(serialize = "BE17")] + #[code = "BE17"] InvalidCreditorIdentificationCode, /// Contact details missing or invalid - #[strum(serialize = "BE18")] + #[code = "BE18"] InvalidContactDetails, /// Charge bearer code for transaction type is invalid - #[strum(serialize = "BE19")] + #[code = "BE19"] InvalidChargeBearerCode, /// Name length exceeds local rules for payment type - #[strum(serialize = "BE20")] + #[code = "BE20"] InvalidNameLength, /// Name missing or invalid - #[strum(serialize = "BE21")] + #[code = "BE21"] MissingName, /// Creditor name is missing - #[strum(serialize = "BE22")] + #[code = "BE22"] MissingCreditorName, /// Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid - #[strum(serialize = "BE23")] + #[code = "BE23"] AccountProxyInvalid, /// Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI - #[strum(serialize = "CERI")] + #[code = "CERI"] CheckERI, /// Value in Requested Execution Date or Requested Collection Date is too far in the future - #[strum(serialize = "CH03")] + #[code = "CH03"] RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture, /// Value in Requested Execution Date or Requested Collection Date is too far in the past - #[strum(serialize = "CH04")] + #[code = "CH04"] RequestedExecutionDateOrRequestedCollectionDateTooFarInPast, /// Element is not to be used at B- and C-Level - #[strum(serialize = "CH07")] + #[code = "CH07"] ElementIsNotToBeUsedAtBandCLevel, /// Mandate changes are not allowed - #[strum(serialize = "CH09")] + #[code = "CH09"] MandateChangesNotAllowed, /// Information on mandate changes are missing - #[strum(serialize = "CH10")] + #[code = "CH10"] InformationOnMandateChangesMissing, /// Value in Creditor Identifier is incorrect - #[strum(serialize = "CH11")] + #[code = "CH11"] CreditorIdentifierIncorrect, /// Creditor Identifier is ambiguous at Transaction Level - #[strum(serialize = "CH12")] + #[code = "CH12"] CreditorIdentifierNotUnambiguouslyAtTransactionLevel, /// Original Debtor Account is not to be used - #[strum(serialize = "CH13")] + #[code = "CH13"] OriginalDebtorAccountIsNotToBeUsed, /// Original Debtor Agent is not to be used - #[strum(serialize = "CH14")] + #[code = "CH14"] OriginalDebtorAgentIsNotToBeUsed, /// Content Remittance Information/Structured includes more than 140 characters - #[strum(serialize = "CH15")] + #[code = "CH15"] ElementContentIncludesMoreThan140Characters, /// Content is incorrect - #[strum(serialize = "CH16")] + #[code = "CH16"] ElementContentFormallyIncorrect, /// Element is not allowed - #[strum(serialize = "CH17")] + #[code = "CH17"] ElementNotAdmitted, /// Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day - #[strum(serialize = "CH19")] + #[code = "CH19"] ValuesWillBeSetToNextTARGETday, /// Number of decimal points not compatible with the currency - #[strum(serialize = "CH20")] + #[code = "CH20"] DecimalPointsNotCompatibleWithCurrency, /// Mandatory element is missing - #[strum(serialize = "CH21")] + #[code = "CH21"] RequiredCompulsoryElementMissing, /// SDD CORE and B2B not permitted within one message - #[strum(serialize = "CH22")] + #[code = "CH22"] COREandB2BwithinOnemessage, /// Related to a Charge message to convey that the code in Charge Breakdown / Type / Code is not accepted by the receiving party - #[strum(serialize = "CHCO")] + #[code = "CHCO"] UnacceptedChargeCodeType, /// Cheque has been presented in cheque clearing and settled on the creditor’s account - #[strum(serialize = "CHQC")] + #[code = "CHQC"] ChequeSettledOnCreditorAccount, /// Related to a Charge message to convey that the charge bearer code used in the corresponding Payment message was not debt - #[strum(serialize = "CHRG")] + #[code = "CHRG"] UnderlyingChargeBearerWasNotDebt, /// Authorisation is cancelled - #[strum(serialize = "CN01")] + #[code = "CN01"] AuthorisationCancelled, /// Credit notes are not supported - #[strum(serialize = "CNNS")] + #[code = "CNNS"] CreditNotesNotSupported, /// Creditor bank is not registered under this BIC in the CSM - #[strum(serialize = "CNOR")] + #[code = "CNOR"] CreditorBankIsNotRegistered, /// Currency of the payment is incorrect - #[strum(serialize = "CURR")] + #[code = "CURR"] IncorrectCurrency, /// Cancellation requested by the Debtor - #[strum(serialize = "CUST")] + #[code = "CUST"] RequestedByCustomer, /// Rejection of a payment due to covering FI settlement not being received - #[strum(serialize = "DC02")] + #[code = "DC02"] SettlementNotReceived, /// Debtor bank is not registered under this BIC in the CSM - #[strum(serialize = "DNOR")] + #[code = "DNOR"] DebtorBankIsNotRegistered, /// The electronic signature(s) is/are correct - #[strum(serialize = "DS01")] + #[code = "DS01"] ElectronicSignaturesCorrect, /// An authorized user has cancelled the order - #[strum(serialize = "DS02")] + #[code = "DS02"] OrderCancelled, /// The user’s attempt to cancel the order was not successful - #[strum(serialize = "DS03")] + #[code = "DS03"] OrderNotCancelled, /// The order was rejected by the bank side (for reasons concerning content) - #[strum(serialize = "DS04")] + #[code = "DS04"] OrderRejected, /// The order was correct and could be forwarded for postprocessing - #[strum(serialize = "DS05")] + #[code = "DS05"] OrderForwardedForPostprocessing, /// The order was transferred to VEU - #[strum(serialize = "DS06")] + #[code = "DS06"] TransferOrder, /// All actions concerning the order could be done by the EBICS bank server - #[strum(serialize = "DS07")] + #[code = "DS07"] ProcessingOK, /// The decompression of the file was not successful - #[strum(serialize = "DS08")] + #[code = "DS08"] DecompressionError, /// The decryption of the file was not successful - #[strum(serialize = "DS09")] + #[code = "DS09"] DecryptionError, /// Data signature is required - #[strum(serialize = "DS0A")] + #[code = "DS0A"] DataSignRequested, /// Data signature for the format is not available or invalid - #[strum(serialize = "DS0B")] + #[code = "DS0B"] UnknownDataSignFormat, /// The signer certificate is revoked - #[strum(serialize = "DS0C")] + #[code = "DS0C"] SignerCertificateRevoked, /// The signer certificate is not valid (revoked or not active) - #[strum(serialize = "DS0D")] + #[code = "DS0D"] SignerCertificateNotValid, /// The signer certificate is not present - #[strum(serialize = "DS0E")] + #[code = "DS0E"] IncorrectSignerCertificate, /// The authority of the signer certification sending the certificate is unknown - #[strum(serialize = "DS0F")] + #[code = "DS0F"] SignerCertificationAuthoritySignerNotValid, /// Signer is not allowed to sign this operation type - #[strum(serialize = "DS0G")] + #[code = "DS0G"] NotAllowedPayment, /// Signer is not allowed to sign for this account - #[strum(serialize = "DS0H")] + #[code = "DS0H"] NotAllowedAccount, /// The number of transaction is over the number allowed for this signer - #[strum(serialize = "DS0K")] + #[code = "DS0K"] NotAllowedNumberOfTransaction, /// The certificate is revoked for the first signer - #[strum(serialize = "DS10")] + #[code = "DS10"] Signer1CertificateRevoked, /// The certificate is not valid (revoked or not active) for the first signer - #[strum(serialize = "DS11")] + #[code = "DS11"] Signer1CertificateNotValid, /// The certificate is not present for the first signer - #[strum(serialize = "DS12")] + #[code = "DS12"] IncorrectSigner1Certificate, /// The authority of signer certification sending the certificate is unknown for the first signer - #[strum(serialize = "DS13")] + #[code = "DS13"] SignerCertificationAuthoritySigner1NotValid, /// The user is unknown on the server - #[strum(serialize = "DS14")] + #[code = "DS14"] UserDoesNotExist, /// The same signature has already been sent to the bank - #[strum(serialize = "DS15")] + #[code = "DS15"] IdenticalSignatureFound, /// The public key version is not correct - #[strum(serialize = "DS16")] + #[code = "DS16"] PublicKeyVersionIncorrect, /// Order data and signatures don’t match - #[strum(serialize = "DS17")] + #[code = "DS17"] DifferentOrderDataInSignatures, /// File cannot be tested, the complete order has to be repeated - #[strum(serialize = "DS18")] + #[code = "DS18"] RepeatOrder, /// The user’s rights (concerning his signature) are insufficient to execute the order - #[strum(serialize = "DS19")] + #[code = "DS19"] ElectronicSignatureRightsInsufficient, /// The certificate is revoked for the second signer - #[strum(serialize = "DS20")] + #[code = "DS20"] Signer2CertificateRevoked, /// The certificate is not valid (revoked or not active) for the second signer - #[strum(serialize = "DS21")] + #[code = "DS21"] Signer2CertificateNotValid, /// The certificate is not present for the second signer - #[strum(serialize = "DS22")] + #[code = "DS22"] IncorrectSigner2Certificate, /// The authority of signer certification sending the certificate is unknown for the second signer - #[strum(serialize = "DS23")] + #[code = "DS23"] SignerCertificationAuthoritySigner2NotValid, /// Waiting time expired due to incomplete order - #[strum(serialize = "DS24")] + #[code = "DS24"] WaitingTimeExpired, /// The order file was deleted by the bank server - #[strum(serialize = "DS25")] + #[code = "DS25"] OrderFileDeleted, /// The same user has signed multiple times - #[strum(serialize = "DS26")] + #[code = "DS26"] UserSignedMultipleTimes, /// The user is not yet activated (technically) - #[strum(serialize = "DS27")] + #[code = "DS27"] UserNotYetActivated, /// Message routed to the wrong environment - #[strum(serialize = "DS28")] + #[code = "DS28"] ReturnForTechnicalReason, /// Invalid date (eg, wrong or missing settlement date) - #[strum(serialize = "DT01")] + #[code = "DT01"] InvalidDate, /// Invalid creation date and time in Group Header (eg, historic date) - #[strum(serialize = "DT02")] + #[code = "DT02"] InvalidCreationDate, /// Invalid non bank processing date (eg, weekend or local public holiday) - #[strum(serialize = "DT03")] + #[code = "DT03"] InvalidNonProcessingDate, /// Future date not supported - #[strum(serialize = "DT04")] + #[code = "DT04"] FutureDateNotSupported, /// Associated message, payment information block or transaction was received after agreed processing cut-off date, i - #[strum(serialize = "DT05")] + #[code = "DT05"] InvalidCutOffDate, /// Execution Date has been modified in order for transaction to be processed - #[strum(serialize = "DT06")] + #[code = "DT06"] ExecutionDateChanged, /// Message Identification is not unique - #[strum(serialize = "DU01")] + #[code = "DU01"] DuplicateMessageID, /// Payment Information Block is not unique - #[strum(serialize = "DU02")] + #[code = "DU02"] DuplicatePaymentInformationID, /// Transaction is not unique - #[strum(serialize = "DU03")] + #[code = "DU03"] DuplicateTransaction, /// End To End ID is not unique - #[strum(serialize = "DU04")] + #[code = "DU04"] DuplicateEndToEndID, /// Instruction ID is not unique - #[strum(serialize = "DU05")] + #[code = "DU05"] DuplicateInstructionID, /// Payment or charge is a duplicate of another payment or charge - #[strum(serialize = "DUPL")] + #[code = "DUPL"] DuplicatePaymentOrCharge, /// Correspondent bank not possible - #[strum(serialize = "ED01")] + #[code = "ED01"] CorrespondentBankNotPossible, /// Balance of payments complementary info is requested - #[strum(serialize = "ED03")] + #[code = "ED03"] BalanceInfoRequest, /// Settlement of the transaction has failed - #[strum(serialize = "ED05")] + #[code = "ED05"] SettlementFailed, /// Interbank settlement system not available - #[strum(serialize = "ED06")] + #[code = "ED06"] SettlementSystemNotAvailable, /// Requested execution date of the payment is not accepted - #[strum(serialize = "EDNA")] + #[code = "EDNA"] ExecutionDateNotAccepted, /// Expiry date time of the request-to-pay is too far in the future - #[strum(serialize = "EDTL")] + #[code = "EDTL"] ExpiryDateTooLong, /// Expiry date time of the request-to-pay is already reached - #[strum(serialize = "EDTR")] + #[code = "EDTR"] ExpiryDateTimeReached, /// Expiration of the payment authorisation due to no use for too long - #[strum(serialize = "EOL1")] + #[code = "EOL1"] EndOfLife, /// Extended Remittance Information (ERI) option is not supported - #[strum(serialize = "ERIN")] + #[code = "ERIN"] ERIOptionNotSupported, /// File Format incomplete or invalid - #[strum(serialize = "FF01")] + #[code = "FF01"] InvalidFileFormat, /// Syntax error reason is provided as narrative information in the additional reason information - #[strum(serialize = "FF02")] + #[code = "FF02"] SyntaxError, /// Payment Type Information is missing or invalid - #[strum(serialize = "FF03")] + #[code = "FF03"] InvalidPaymentTypeInformation, /// Service Level code is missing or invalid - #[strum(serialize = "FF04")] + #[code = "FF04"] InvalidServiceLevelCode, /// Local Instrument code is missing or invalid - #[strum(serialize = "FF05")] + #[code = "FF05"] InvalidLocalInstrumentCode, /// Category Purpose code is missing or invalid - #[strum(serialize = "FF06")] + #[code = "FF06"] InvalidCategoryPurposeCode, /// Purpose is missing or invalid - #[strum(serialize = "FF07")] + #[code = "FF07"] InvalidPurpose, /// End to End Id missing or invalid - #[strum(serialize = "FF08")] + #[code = "FF08"] InvalidEndToEndId, /// Cheque number missing or invalid - #[strum(serialize = "FF09")] + #[code = "FF09"] InvalidChequeNumber, /// File or transaction cannot be processed due to technical issues at the bank side - #[strum(serialize = "FF10")] + #[code = "FF10"] BankSystemProcessingError, /// Clearing request rejected due it being subject to an abort operation - #[strum(serialize = "FF11")] + #[code = "FF11"] ClearingRequestAborted, /// Original payment is not eligible to be returned given its current status - #[strum(serialize = "FF12")] + #[code = "FF12"] OriginalTransactionNotEligibleForRequestedReturn, /// No record of request for cancellation found - #[strum(serialize = "FF13")] + #[code = "FF13"] RequestForCancellationNotFound, /// Return following a cancellation request - #[strum(serialize = "FOCR")] + #[code = "FOCR"] FollowingCancellationRequest, /// Returned as a result of fraud - #[strum(serialize = "FR01")] + #[code = "FR01"] Fraud, /// Cancellation requested following a transaction that was originated fraudulently - #[strum(serialize = "FRAD")] + #[code = "FRAD"] FraudulentOrigin, /// In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure - #[strum(serialize = "G000")] + #[code = "G000"] PaymentTransferredAndTracked, /// In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure - #[strum(serialize = "G001")] + #[code = "G001"] PaymentTransferredAndNotTracked, /// In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day - #[strum(serialize = "G002")] + #[code = "G002"] CreditDebitNotConfirmed, /// In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents - #[strum(serialize = "G003")] + #[code = "G003"] CreditPendingDocuments, /// In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover - #[strum(serialize = "G004")] + #[code = "G004"] CreditPendingFunds, /// Payment has been delivered to creditor agent with service level - #[strum(serialize = "G005")] + #[code = "G005"] DeliveredWithServiceLevel, /// Payment has been delivered to creditor agent without service level - #[strum(serialize = "G006")] + #[code = "G006"] DeliveredWIthoutServiceLevel, /// Signature file was sent to the bank but the corresponding original file has not been sent yet - #[strum(serialize = "ID01")] + #[code = "ID01"] CorrespondingOriginalFileStillNotSent, /// Expiry date time of the request-to-pay is incorrect - #[strum(serialize = "IEDT")] + #[code = "IEDT"] IncorrectExpiryDateTime, /// Payer’s activation reference is invalid - #[strum(serialize = "INAR")] + #[code = "INAR"] InvalidActivationReference, /// Details not valid for this field - #[strum(serialize = "INDT")] + #[code = "INDT"] InvalidDetails, /// Payments in instalments are not supported - #[strum(serialize = "IPNS")] + #[code = "IPNS"] InstalmentPaymentsNotSupported, /// No initial request-to-pay has been received - #[strum(serialize = "IRNR")] + #[code = "IRNR"] InitialRTPNeverReceived, /// Cannot schedule instruction for Night Window - #[strum(serialize = "ISWS")] + #[code = "ISWS"] InvalidSettlementWindow, /// No Mandate - #[strum(serialize = "MD01")] + #[code = "MD01"] NoMandate, /// Mandate related information data required by the scheme is missing - #[strum(serialize = "MD02")] + #[code = "MD02"] MissingMandatoryInformationInMandate, /// Creditor or creditor's agent should not have collected the direct debit - #[strum(serialize = "MD05")] + #[code = "MD05"] CollectionNotDue, /// Return of funds requested by end customer - #[strum(serialize = "MD06")] + #[code = "MD06"] RefundRequestByEndCustomer, /// End customer is deceased - #[strum(serialize = "MD07")] + #[code = "MD07"] EndCustomerDeceased, /// Information missing for the field or cannot be empty - #[strum(serialize = "MINF")] + #[code = "MINF"] MissingInformation, /// Reason has not been specified by end customer - #[strum(serialize = "MS02")] + #[code = "MS02"] NotSpecifiedReasonCustomerGenerated, /// Reason has not been specified by agent - #[strum(serialize = "MS03")] + #[code = "MS03"] NotSpecifiedReasonAgentGenerated, /// Reason is provided as narrative information in the additional reason information - #[strum(serialize = "NARR")] + #[code = "NARR"] Narrative, /// Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI - #[strum(serialize = "NERI")] + #[code = "NERI"] NoERI, /// No existing agreement for receiving request-to-pay messages - #[strum(serialize = "NOAR")] + #[code = "NOAR"] NonAgreedRTP, /// No response from Beneficiary - #[strum(serialize = "NOAS")] + #[code = "NOAS"] NoAnswerFromCustomer, /// Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing - #[strum(serialize = "NOCM")] + #[code = "NOCM"] NotCompliantGeneric, /// Continuous Processing Line on Hold Instruction - #[strum(serialize = "NOFR")] + #[code = "NOFR"] OutstandingFundingForSettlement, /// Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided - #[strum(serialize = "NOPG")] + #[code = "NOPG"] NoPaymentGuarantee, /// Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable - #[strum(serialize = "NRCH")] + #[code = "NRCH"] PayerOrPayerRTPSPNotReachable, /// Requested optional service (for example instalment payments) is not supported - #[strum(serialize = "OSNS")] + #[code = "OSNS"] OptionalServiceNotSupported, /// Type of payment requested in the request-to-pay is not supported by the payer - #[strum(serialize = "PINS")] + #[code = "PINS"] TypeOfPaymentInstrumentNotSupported, /// Error code used for RTP-initiated CTR when the pacs - #[strum(serialize = "PNRT")] + #[code = "PNRT"] PaymentNotAlignedWithRTPRequest, /// Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode) - #[strum(serialize = "RC01")] + #[code = "RC01"] BankIdentifierIncorrect, /// Bank identifier is invalid or missing - #[strum(serialize = "RC02")] + #[code = "RC02"] InvalidBankIdentifier, /// Debtor bank identifier is invalid or missing - #[strum(serialize = "RC03")] + #[code = "RC03"] InvalidDebtorBankIdentifier, /// Creditor bank identifier is invalid or missing - #[strum(serialize = "RC04")] + #[code = "RC04"] InvalidCreditorBankIdentifier, /// BIC identifier is invalid or missing - #[strum(serialize = "RC05")] + #[code = "RC05"] InvalidBICIdentifier, /// Debtor BIC identifier is invalid or missing - #[strum(serialize = "RC06")] + #[code = "RC06"] InvalidDebtorBICIdentifier, /// Creditor BIC identifier is invalid or missing - #[strum(serialize = "RC07")] + #[code = "RC07"] InvalidCreditorBICIdentifier, /// ClearingSystemMemberidentifier is invalid or missing - #[strum(serialize = "RC08")] + #[code = "RC08"] InvalidClearingSystemMemberIdentifier, /// Debtor ClearingSystemMember identifier is invalid or missing - #[strum(serialize = "RC09")] + #[code = "RC09"] InvalidDebtorClearingSystemMemberIdentifier, /// Creditor ClearingSystemMember identifier is invalid or missing - #[strum(serialize = "RC10")] + #[code = "RC10"] InvalidCreditorClearingSystemMemberIdentifier, /// Intermediary Agent is invalid or missing - #[strum(serialize = "RC11")] + #[code = "RC11"] InvalidIntermediaryAgent, /// Creditor Scheme Id is invalid or missing - #[strum(serialize = "RC12")] + #[code = "RC12"] MissingCreditorSchemeId, /// Originator not active any more - #[strum(serialize = "RC13")] + #[code = "RC13"] ParticipantNotAnActiveMemberofRTGS, /// Settlement agreement required - #[strum(serialize = "RC15")] + #[code = "RC15"] ParticipantNotActiveMemberSettlementType, /// Participant blocked from SADC-RTGS - #[strum(serialize = "RC16")] + #[code = "RC16"] ParticipantNotActiveMemberofSADCRTGS, /// Conflict with R-Message - #[strum(serialize = "RCON")] + #[code = "RCON"] RMessageConflict, /// Further information regarding the intended recipient - #[strum(serialize = "RECI")] + #[code = "RECI"] ReceiverCustomerInformation, /// Request-to-pay has been received and can be processed further - #[strum(serialize = "REPR")] + #[code = "REPR"] RTPReceivedCanBeProcessed, /// Transaction reference is not unique within the message - #[strum(serialize = "RF01")] + #[code = "RF01"] NotUniqueTransactionReference, /// Payer did not recognize the request from Payee Participant, - #[strum(serialize = "RQNR")] + #[code = "RQNR"] RequestNotRecognized, /// Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing - #[strum(serialize = "RR01")] + #[code = "RR01"] MissingDebtorAccountOrIdentification, /// Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing - #[strum(serialize = "RR02")] + #[code = "RR02"] MissingDebtorNameOrAddress, /// Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing - #[strum(serialize = "RR03")] + #[code = "RR03"] MissingCreditorNameOrAddress, /// Regulatory Reason - #[strum(serialize = "RR04")] + #[code = "RR04"] RegulatoryReason, /// Regulatory or Central Bank Reporting information missing, incomplete or invalid - #[strum(serialize = "RR05")] + #[code = "RR05"] RegulatoryInformationInvalid, /// Tax information missing, incomplete or invalid - #[strum(serialize = "RR06")] + #[code = "RR06"] TaxInformationInvalid, /// Remittance information structure does not comply with rules for payment type - #[strum(serialize = "RR07")] + #[code = "RR07"] RemittanceInformationInvalid, /// Remittance information truncated to comply with rules for payment type - #[strum(serialize = "RR08")] + #[code = "RR08"] RemittanceInformationTruncated, /// Structured creditor reference invalid or missing - #[strum(serialize = "RR09")] + #[code = "RR09"] InvalidStructuredCreditorReference, /// Character set supplied not valid for the country and payment type - #[strum(serialize = "RR10")] + #[code = "RR10"] InvalidCharacterSet, /// Invalid or missing identification of a bank proprietary service - #[strum(serialize = "RR11")] + #[code = "RR11"] InvalidDebtorAgentServiceID, /// Invalid or missing identification required within a particular country or payment type - #[strum(serialize = "RR12")] + #[code = "RR12"] InvalidPartyID, /// Debtor does not support request-to-pay transactions - #[strum(serialize = "RTNS")] + #[code = "RTNS"] RTPNotSupportedForDebtor, /// Return following investigation request and no remediation possible - #[strum(serialize = "RUTA")] + #[code = "RUTA"] ReturnUponUnableToApply, /// Request for Cancellation is acknowledged following validation - #[strum(serialize = "S000")] + #[code = "S000"] ValidRequestForCancellationAcknowledged, /// Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation - #[strum(serialize = "S001")] + #[code = "S001"] UETRFlaggedForCancellation, /// Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network - #[strum(serialize = "S002")] + #[code = "S002"] NetworkStopOfUETR, /// Request for Cancellation has been forwarded to the payment processing/last payment processing agent - #[strum(serialize = "S003")] + #[code = "S003"] RequestForCancellationForwarded, /// Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent - #[strum(serialize = "S004")] + #[code = "S004"] RequestForCancellationDeliveryAcknowledgement, /// Remove Concurrent Batch Processing Line on hold instruction - #[strum(serialize = "SBRN")] + #[code = "SBRN"] SettlementBatchRemovalNotification, /// Due to specific service offered by the Debtor Agent - #[strum(serialize = "SL01")] + #[code = "SL01"] SpecificServiceOfferedByDebtorAgent, /// Due to specific service offered by the Creditor Agent - #[strum(serialize = "SL02")] + #[code = "SL02"] SpecificServiceOfferedByCreditorAgent, /// Due to a specific service offered by the clearing system - #[strum(serialize = "SL03")] + #[code = "SL03"] ServiceofClearingSystem, /// Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet) - #[strum(serialize = "SL11")] + #[code = "SL11"] CreditorNotOnWhitelistOfDebtor, /// Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist” - #[strum(serialize = "SL12")] + #[code = "SL12"] CreditorOnBlacklistOfDebtor, /// Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent - #[strum(serialize = "SL13")] + #[code = "SL13"] MaximumNumberOfDirectDebitTransactionsExceeded, /// Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent - #[strum(serialize = "SL14")] + #[code = "SL14"] MaximumDirectDebitTransactionAmountExceeded, /// Maximum number of credit transactions allowed by the account servicer per service period exceeded - #[strum(serialize = "SL15")] + #[code = "SL15"] MaximumNumberOfCreditTransactionsExceeded, /// Maximum total credit amount allowed by the account servicer per service period exceeded - #[strum(serialize = "SL16")] + #[code = "SL16"] MaximumCreditTransactionsAmountExceeded, /// Whitelisting service offered by payment system operator or financial institution - #[strum(serialize = "SL17")] + #[code = "SL17"] DebtorNotOnWhitelistOfCreditorSide, /// Blacklisting service offered by payment system operator or financial institution - #[strum(serialize = "SL18")] + #[code = "SL18"] DebtorOnBlacklistOfCreditorSide, /// Services are not yet rendered by the Payee Participant (Creditor) - #[strum(serialize = "SNRD")] + #[code = "SNRD"] ServiceNotRendered, /// Identifier of the request-to-pay service provider is incorrect - #[strum(serialize = "SPII")] + #[code = "SPII"] RTPServiceProviderIdentifierIncorrect, /// The transmission of the file was not successful – it had to be aborted (for technical reasons) - #[strum(serialize = "TA01")] + #[code = "TA01"] TransmissonAborted, /// There is no data available (for download) - #[strum(serialize = "TD01")] + #[code = "TD01"] NoDataAvailable, /// The file cannot be read (e - #[strum(serialize = "TD02")] + #[code = "TD02"] FileNonReadable, /// The file format is incomplete or invalid - #[strum(serialize = "TD03")] + #[code = "TD03"] IncorrectFileStructure, /// Token is invalid - #[strum(serialize = "TK01")] + #[code = "TK01"] TokenInvalid, /// Token used for the sender does not exist - #[strum(serialize = "TK02")] + #[code = "TK02"] SenderTokenNotFound, /// Token used for the receiver does not exist - #[strum(serialize = "TK03")] + #[code = "TK03"] ReceiverTokenNotFound, /// Token required for request is missing - #[strum(serialize = "TK09")] + #[code = "TK09"] TokenMissing, /// Token found with counterparty mismatch - #[strum(serialize = "TKCM")] + #[code = "TKCM"] TokenCounterpartyMismatch, /// Single Use Token already used - #[strum(serialize = "TKSG")] + #[code = "TKSG"] TokenSingleUse, /// Token found with suspended status - #[strum(serialize = "TKSP")] + #[code = "TKSP"] TokenSuspended, /// Token found with value limit rule violation - #[strum(serialize = "TKVE")] + #[code = "TKVE"] TokenValueLimitExceeded, /// Token expired - #[strum(serialize = "TKXP")] + #[code = "TKXP"] TokenExpired, /// Associated message, payment information block, or transaction was received after agreed processing cut-off time - #[strum(serialize = "TM01")] + #[code = "TM01"] InvalidCutOffTime, /// The (technical) transmission of the file was successful - #[strum(serialize = "TS01")] + #[code = "TS01"] TransmissionSuccessful, /// The order was transferred to pass by accompanying note signed by hand - #[strum(serialize = "TS04")] + #[code = "TS04"] TransferToSignByHand, /// Unknown Creditor - #[strum(serialize = "UCRD")] + #[code = "UCRD"] UnknownCreditor, /// Payment is not justified - #[strum(serialize = "UPAY")] + #[code = "UPAY"] UnduePayment, } -impl ExternalStatusReasonCode { - pub fn description(&self) -> &'static str { - match self { - Self::AbortedClearingTimeout => "Clearing process aborted due to timeout", - Self::AbortedClearingFatalError => "Clearing process aborted due to a fatal error", - Self::AbortedSettlementTimeout => "Settlement aborted due to timeout", - Self::AbortedSettlementFatalError => "Settlement process aborted due to a fatal error", - Self::TimeoutCreditorAgent => { - "Transaction stopped due to timeout at the Creditor Agent" - } - Self::TimeoutInstructedAgent => { - "Transaction stopped due to timeout at the Instructed Agent" - } - Self::OfflineAgent => "Agent of message is not online", - Self::OfflineCreditorAgent => "Creditor Agent is not online", - Self::ErrorCreditorAgent => "Transaction stopped due to error at the Creditor Agent", - Self::ErrorInstructedAgent => { - "Transaction stopped due to error at the Instructed Agent" - } - Self::TimeoutDebtorAgent => "Transaction stopped due to timeout at the Debtor Agent", - Self::InvalidConcurrentBatch => { - "Duplicate Concurrent Batch Sequence number– for Settlement Instructions" - } - Self::InvalidRoutingCodeUtilised => "Wrong Message Routing Type for Return-of-Funds", - Self::InvalidAccountNumberForSettlementType => { - "Instruction may not be placed on the Continuous Processing Line settlement processor" - } - Self::InvalidSettlementAgreementNumberSpecified => { - "Agreement number not valid (beneficiary)" - } - Self::InvalidBatchSettlementInstruction => "Settlement Instruction does not exist", - Self::IncorrectAccountNumber => "Account number is invalid or missing", - Self::InvalidDebtorAccountNumber => "Debtor account number invalid or missing", - Self::InvalidCreditorAccountNumber => "Creditor account number invalid or missing", - Self::ClosedAccountNumber => { - "Account number specified has been closed on the bank of account's books" - } - Self::ClosedDebtorAccountNumber => "Debtor account number closed", - Self::BlockedAccount => { - "Account specified is blocked, prohibiting posting of transactions against it" - } - Self::ClosedCreditorAccountNumber => "Creditor account number closed", - Self::InvalidBranchCode => "Branch code is invalid or missing", - Self::InvalidAccountCurrency => "Account currency is invalid or missing", - Self::InvalidDebtorAccountCurrency => "Debtor account currency is invalid or missing", - Self::InvalidCreditorAccountCurrency => { - "Creditor account currency is invalid or missing" - } - Self::InvalidAccountType => "Account type missing or invalid", - Self::InvalidDebtorAccountType => "Debtor account type missing or invalid", - Self::InvalidCreditorAccountType => "Creditor account type missing or invalid", - Self::AccountDetailsChanged => "The account details for the counterparty have changed", - Self::CardNumberInvalid => "Credit or debit card number is invalid", - Self::AlreadyExpiredRTP => "Request-to-pay Expiry Date and Time has already passed", - Self::TransactionForbidden => { - "Transaction forbidden on this type of account (formerly NoAgreement)" - } - Self::InvalidBankOperationCode => { - "Bank Operation code specified in the message is not valid for receiver" - } - Self::TransactionNotSupported => { - "Transaction type not supported/authorized on this account" - } - Self::InvalidAgentCountry => "Agent country code is missing or invalid", - Self::InvalidDebtorAgentCountry => "Debtor agent country code is missing or invalid", - Self::InvalidCreditorAgentCountry => { - "Creditor agent country code is missing or invalid" - } - Self::UnsuccesfulDirectDebit => "Debtor account cannot be debited for a generic reason", - Self::InvalidAccessRights => { - "Transaction failed due to invalid or missing user or access right" - } - Self::PaymentNotReceived => "Original payment never received", - Self::AgentSuspended => { - "Agent of message is suspended from the Real Time Payment system" - } - Self::CreditorAgentSuspended => { - "Creditor Agent of message is suspended from the Real Time Payment system" - } - Self::NotAllowedBookTransfer => { - "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed" - } - Self::ForbiddenReturnPayment => { - "Returned payments derived from previously returned transactions are not allowed" - } - Self::IncorrectAgent => "Agent in the payment workflow is incorrect", - Self::AlreadyAcceptedRTP => "Request-to-pay has already been accepted by the Debtor", - Self::ZeroAmount => "Specified message amount is equal to zero", - Self::NotAllowedAmount => { - "Specific transaction/message amount is greater than allowed maximum" - } - Self::NotAllowedCurrency => { - "Specified message amount is an non processable currency outside of existing agreement" - } - Self::InsufficientFunds => { - "Amount of funds available to cover specified message amount is insufficient" - } - Self::Duplication => "Duplication", - Self::TooLowAmount => "Specified transaction amount is less than agreed minimum", - Self::BlockedAmount => { - "Amount specified in message has been blocked by regulatory authorities" - } - Self::WrongAmount => "Amount received is not the amount agreed or expected", - Self::InvalidControlSum => "Sum of instructed amounts does not equal the control sum", - Self::InvalidTransactionCurrency => "Transaction currency is invalid or missing", - Self::InvalidAmount => "Amount is invalid or missing", - Self::AmountExceedsClearingSystemLimit => { - "Transaction amount exceeds limits set by clearing system" - } - Self::AmountExceedsAgreedLimit => { - "Transaction amount exceeds limits agreed between bank and client" - } - Self::AmountBelowClearingSystemMinimum => { - "Transaction amount below minimum set by clearing system" - } - Self::InvalidGroupControlSum => "Control Sum at the Group level is invalid", - Self::InvalidPaymentInfoControlSum => { - "Control Sum at the Payment Information level is invalid" - } - Self::InvalidNumberOfTransactions => "Number of transactions is invalid or missing", - Self::InvalidGroupNumberOfTransactions => { - "Number of transactions at the Group level is invalid or missing" - } - Self::InvalidPaymentInfoNumberOfTransactions => { - "Number of transactions at the Payment Information level is invalid" - } - Self::LimitExceeded => { - "Transaction amount exceeds limits agreed between bank and client" - } - Self::ZeroAmountNotApplied => "Unable to apply zero amount to designated account", - Self::AmountExceedsSettlementLimit => "Transaction amount exceeds settlement limit", - Self::AttachmentMaximumSize => "Size of the attachment exceeds the allowed maximum", - Self::AlreadyPaidRTP => "Request To Pay has already been paid by the Debtor", - Self::AlreadyRefusedRTP => "Request-to-pay has already been refused by the Debtor", - Self::AlreadyRejectedRTP => "Request-to-pay has already been rejected", - Self::AttachementsNotSupported => "Attachments to the request-to-pay are not supported", - Self::NotBusinessDay => "Settlement Cycle Day and Calendar day should be the same", - Self::InconsistenWithEndCustomer => { - "Identification of end customer is not consistent with associated account number" - } - Self::MissingCreditorAddress => { - "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)" - } - Self::UnrecognisedInitiatingParty => { - "Party who initiated the message is not recognised by the end customer" - } - Self::UnknownEndCustomer => { - "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books" - } - Self::MissingDebtorAddress => { - "Specification of debtor's address, which is required for payment, is missing/not correct" - } - Self::MissingDebtorName => "Debtor name is missing", - Self::InvalidCountry => "Country code is missing or Invalid", - Self::InvalidDebtorCountry => "Debtor country code is missing or invalid", - Self::InvalidCreditorCountry => "Creditor country code is missing or invalid", - Self::InvalidCountryOfResidence => "Country code of residence is missing or Invalid", - Self::InvalidDebtorCountryOfResidence => { - "Country code of debtor's residence is missing or Invalid" - } - Self::InvalidCreditorCountryOfResidence => { - "Country code of creditor's residence is missing or Invalid" - } - Self::InvalidIdentificationCode => "Identification code missing or invalid", - Self::InvalidDebtorIdentificationCode => { - "Debtor or Ultimate Debtor identification code missing or invalid" - } - Self::InvalidCreditorIdentificationCode => { - "Creditor or Ultimate Creditor identification code missing or invalid" - } - Self::InvalidContactDetails => "Contact details missing or invalid", - Self::InvalidChargeBearerCode => "Charge bearer code for transaction type is invalid", - Self::InvalidNameLength => "Name length exceeds local rules for payment type", - Self::MissingName => "Name missing or invalid", - Self::MissingCreditorName => "Creditor name is missing", - Self::AccountProxyInvalid => { - "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid" - } - Self::CheckERI => { - "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI" - } - Self::RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture => { - "Value in Requested Execution Date or Requested Collection Date is too far in the future" - } - Self::RequestedExecutionDateOrRequestedCollectionDateTooFarInPast => { - "Value in Requested Execution Date or Requested Collection Date is too far in the past" - } - Self::ElementIsNotToBeUsedAtBandCLevel => "Element is not to be used at B- and C-Level", - Self::MandateChangesNotAllowed => "Mandate changes are not allowed", - Self::InformationOnMandateChangesMissing => { - "Information on mandate changes are missing" - } - Self::CreditorIdentifierIncorrect => "Value in Creditor Identifier is incorrect", - Self::CreditorIdentifierNotUnambiguouslyAtTransactionLevel => { - "Creditor Identifier is ambiguous at Transaction Level" - } - Self::OriginalDebtorAccountIsNotToBeUsed => "Original Debtor Account is not to be used", - Self::OriginalDebtorAgentIsNotToBeUsed => "Original Debtor Agent is not to be used", - Self::ElementContentIncludesMoreThan140Characters => { - "Content Remittance Information/Structured includes more than 140 characters" - } - Self::ElementContentFormallyIncorrect => "Content is incorrect", - Self::ElementNotAdmitted => "Element is not allowed", - Self::ValuesWillBeSetToNextTARGETday => { - "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day" - } - Self::DecimalPointsNotCompatibleWithCurrency => { - "Number of decimal points not compatible with the currency" - } - Self::RequiredCompulsoryElementMissing => "Mandatory element is missing", - Self::COREandB2BwithinOnemessage => "SDD CORE and B2B not permitted within one message", - Self::UnacceptedChargeCodeType => { - "Related to a Charge message to convey that the code in Charge Breakdown / Type / Code is not accepted by the receiving party" - } - Self::ChequeSettledOnCreditorAccount => { - "Cheque has been presented in cheque clearing and settled on the creditor’s account" - } - Self::UnderlyingChargeBearerWasNotDebt => { - "Related to a Charge message to convey that the charge bearer code used in the corresponding Payment message was not debt" - } - Self::AuthorisationCancelled => "Authorisation is cancelled", - Self::CreditNotesNotSupported => "Credit notes are not supported", - Self::CreditorBankIsNotRegistered => { - "Creditor bank is not registered under this BIC in the CSM" - } - Self::IncorrectCurrency => "Currency of the payment is incorrect", - Self::RequestedByCustomer => "Cancellation requested by the Debtor", - Self::SettlementNotReceived => { - "Rejection of a payment due to covering FI settlement not being received" - } - Self::DebtorBankIsNotRegistered => { - "Debtor bank is not registered under this BIC in the CSM" - } - Self::ElectronicSignaturesCorrect => "The electronic signature(s) is/are correct", - Self::OrderCancelled => "An authorized user has cancelled the order", - Self::OrderNotCancelled => "The user’s attempt to cancel the order was not successful", - Self::OrderRejected => { - "The order was rejected by the bank side (for reasons concerning content)" - } - Self::OrderForwardedForPostprocessing => { - "The order was correct and could be forwarded for postprocessing" - } - Self::TransferOrder => "The order was transferred to VEU", - Self::ProcessingOK => { - "All actions concerning the order could be done by the EBICS bank server" - } - Self::DecompressionError => "The decompression of the file was not successful", - Self::DecryptionError => "The decryption of the file was not successful", - Self::DataSignRequested => "Data signature is required", - Self::UnknownDataSignFormat => { - "Data signature for the format is not available or invalid" - } - Self::SignerCertificateRevoked => "The signer certificate is revoked", - Self::SignerCertificateNotValid => { - "The signer certificate is not valid (revoked or not active)" - } - Self::IncorrectSignerCertificate => "The signer certificate is not present", - Self::SignerCertificationAuthoritySignerNotValid => { - "The authority of the signer certification sending the certificate is unknown" - } - Self::NotAllowedPayment => "Signer is not allowed to sign this operation type", - Self::NotAllowedAccount => "Signer is not allowed to sign for this account", - Self::NotAllowedNumberOfTransaction => { - "The number of transaction is over the number allowed for this signer" - } - Self::Signer1CertificateRevoked => "The certificate is revoked for the first signer", - Self::Signer1CertificateNotValid => { - "The certificate is not valid (revoked or not active) for the first signer" - } - Self::IncorrectSigner1Certificate => { - "The certificate is not present for the first signer" - } - Self::SignerCertificationAuthoritySigner1NotValid => { - "The authority of signer certification sending the certificate is unknown for the first signer" - } - Self::UserDoesNotExist => "The user is unknown on the server", - Self::IdenticalSignatureFound => "The same signature has already been sent to the bank", - Self::PublicKeyVersionIncorrect => "The public key version is not correct", - Self::DifferentOrderDataInSignatures => "Order data and signatures don’t match", - Self::RepeatOrder => "File cannot be tested, the complete order has to be repeated", - Self::ElectronicSignatureRightsInsufficient => { - "The user’s rights (concerning his signature) are insufficient to execute the order" - } - Self::Signer2CertificateRevoked => "The certificate is revoked for the second signer", - Self::Signer2CertificateNotValid => { - "The certificate is not valid (revoked or not active) for the second signer" - } - Self::IncorrectSigner2Certificate => { - "The certificate is not present for the second signer" - } - Self::SignerCertificationAuthoritySigner2NotValid => { - "The authority of signer certification sending the certificate is unknown for the second signer" - } - Self::WaitingTimeExpired => "Waiting time expired due to incomplete order", - Self::OrderFileDeleted => "The order file was deleted by the bank server", - Self::UserSignedMultipleTimes => "The same user has signed multiple times", - Self::UserNotYetActivated => "The user is not yet activated (technically)", - Self::ReturnForTechnicalReason => "Message routed to the wrong environment", - Self::InvalidDate => "Invalid date (eg, wrong or missing settlement date)", - Self::InvalidCreationDate => { - "Invalid creation date and time in Group Header (eg, historic date)" - } - Self::InvalidNonProcessingDate => { - "Invalid non bank processing date (eg, weekend or local public holiday)" - } - Self::FutureDateNotSupported => "Future date not supported", - Self::InvalidCutOffDate => { - "Associated message, payment information block or transaction was received after agreed processing cut-off date, i" - } - Self::ExecutionDateChanged => { - "Execution Date has been modified in order for transaction to be processed" - } - Self::DuplicateMessageID => "Message Identification is not unique", - Self::DuplicatePaymentInformationID => "Payment Information Block is not unique", - Self::DuplicateTransaction => "Transaction is not unique", - Self::DuplicateEndToEndID => "End To End ID is not unique", - Self::DuplicateInstructionID => "Instruction ID is not unique", - Self::DuplicatePaymentOrCharge => { - "Payment or charge is a duplicate of another payment or charge" - } - Self::CorrespondentBankNotPossible => "Correspondent bank not possible", - Self::BalanceInfoRequest => "Balance of payments complementary info is requested", - Self::SettlementFailed => "Settlement of the transaction has failed", - Self::SettlementSystemNotAvailable => "Interbank settlement system not available", - Self::ExecutionDateNotAccepted => { - "Requested execution date of the payment is not accepted" - } - Self::ExpiryDateTooLong => { - "Expiry date time of the request-to-pay is too far in the future" - } - Self::ExpiryDateTimeReached => { - "Expiry date time of the request-to-pay is already reached" - } - Self::EndOfLife => "Expiration of the payment authorisation due to no use for too long", - Self::ERIOptionNotSupported => { - "Extended Remittance Information (ERI) option is not supported" - } - Self::InvalidFileFormat => "File Format incomplete or invalid", - Self::SyntaxError => { - "Syntax error reason is provided as narrative information in the additional reason information" - } - Self::InvalidPaymentTypeInformation => "Payment Type Information is missing or invalid", - Self::InvalidServiceLevelCode => "Service Level code is missing or invalid", - Self::InvalidLocalInstrumentCode => "Local Instrument code is missing or invalid", - Self::InvalidCategoryPurposeCode => "Category Purpose code is missing or invalid", - Self::InvalidPurpose => "Purpose is missing or invalid", - Self::InvalidEndToEndId => "End to End Id missing or invalid", - Self::InvalidChequeNumber => "Cheque number missing or invalid", - Self::BankSystemProcessingError => { - "File or transaction cannot be processed due to technical issues at the bank side" - } - Self::ClearingRequestAborted => { - "Clearing request rejected due it being subject to an abort operation" - } - Self::OriginalTransactionNotEligibleForRequestedReturn => { - "Original payment is not eligible to be returned given its current status" - } - Self::RequestForCancellationNotFound => "No record of request for cancellation found", - Self::FollowingCancellationRequest => "Return following a cancellation request", - Self::Fraud => "Returned as a result of fraud", - Self::FraudulentOrigin => { - "Cancellation requested following a transaction that was originated fraudulently" - } - Self::PaymentTransferredAndTracked => { - "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure" - } - Self::PaymentTransferredAndNotTracked => { - "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure" - } - Self::CreditDebitNotConfirmed => { - "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day" - } - Self::CreditPendingDocuments => { - "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents" - } - Self::CreditPendingFunds => { - "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover" - } - Self::DeliveredWithServiceLevel => { - "Payment has been delivered to creditor agent with service level" - } - Self::DeliveredWIthoutServiceLevel => { - "Payment has been delivered to creditor agent without service level" - } - Self::CorrespondingOriginalFileStillNotSent => { - "Signature file was sent to the bank but the corresponding original file has not been sent yet" - } - Self::IncorrectExpiryDateTime => "Expiry date time of the request-to-pay is incorrect", - Self::InvalidActivationReference => "Payer’s activation reference is invalid", - Self::InvalidDetails => "Details not valid for this field", - Self::InstalmentPaymentsNotSupported => "Payments in instalments are not supported", - Self::InitialRTPNeverReceived => "No initial request-to-pay has been received", - Self::InvalidSettlementWindow => "Cannot schedule instruction for Night Window", - Self::NoMandate => "No Mandate", - Self::MissingMandatoryInformationInMandate => { - "Mandate related information data required by the scheme is missing" - } - Self::CollectionNotDue => { - "Creditor or creditor's agent should not have collected the direct debit" - } - Self::RefundRequestByEndCustomer => "Return of funds requested by end customer", - Self::EndCustomerDeceased => "End customer is deceased", - Self::MissingInformation => "Information missing for the field or cannot be empty", - Self::NotSpecifiedReasonCustomerGenerated => { - "Reason has not been specified by end customer" - } - Self::NotSpecifiedReasonAgentGenerated => "Reason has not been specified by agent", - Self::Narrative => { - "Reason is provided as narrative information in the additional reason information" - } - Self::NoERI => { - "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI" - } - Self::NonAgreedRTP => "No existing agreement for receiving request-to-pay messages", - Self::NoAnswerFromCustomer => "No response from Beneficiary", - Self::NotCompliantGeneric => { - "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing" - } - Self::OutstandingFundingForSettlement => { - "Continuous Processing Line on Hold Instruction" - } - Self::NoPaymentGuarantee => { - "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided" - } - Self::PayerOrPayerRTPSPNotReachable => { - "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable" - } - Self::OptionalServiceNotSupported => { - "Requested optional service (for example instalment payments) is not supported" - } - Self::TypeOfPaymentInstrumentNotSupported => { - "Type of payment requested in the request-to-pay is not supported by the payer" - } - Self::PaymentNotAlignedWithRTPRequest => { - "Error code used for RTP-initiated CTR when the pacs" - } - Self::BankIdentifierIncorrect => { - "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)" - } - Self::InvalidBankIdentifier => "Bank identifier is invalid or missing", - Self::InvalidDebtorBankIdentifier => "Debtor bank identifier is invalid or missing", - Self::InvalidCreditorBankIdentifier => "Creditor bank identifier is invalid or missing", - Self::InvalidBICIdentifier => "BIC identifier is invalid or missing", - Self::InvalidDebtorBICIdentifier => "Debtor BIC identifier is invalid or missing", - Self::InvalidCreditorBICIdentifier => "Creditor BIC identifier is invalid or missing", - Self::InvalidClearingSystemMemberIdentifier => { - "ClearingSystemMemberidentifier is invalid or missing" - } - Self::InvalidDebtorClearingSystemMemberIdentifier => { - "Debtor ClearingSystemMember identifier is invalid or missing" - } - Self::InvalidCreditorClearingSystemMemberIdentifier => { - "Creditor ClearingSystemMember identifier is invalid or missing" - } - Self::InvalidIntermediaryAgent => "Intermediary Agent is invalid or missing", - Self::MissingCreditorSchemeId => "Creditor Scheme Id is invalid or missing", - Self::ParticipantNotAnActiveMemberofRTGS => "Originator not active any more", - Self::ParticipantNotActiveMemberSettlementType => "Settlement agreement required", - Self::ParticipantNotActiveMemberofSADCRTGS => "Participant blocked from SADC-RTGS", - Self::RMessageConflict => "Conflict with R-Message", - Self::ReceiverCustomerInformation => { - "Further information regarding the intended recipient" - } - Self::RTPReceivedCanBeProcessed => { - "Request-to-pay has been received and can be processed further" - } - Self::NotUniqueTransactionReference => { - "Transaction reference is not unique within the message" - } - Self::RequestNotRecognized => { - "Payer did not recognize the request from Payee Participant," - } - Self::MissingDebtorAccountOrIdentification => { - "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing" - } - Self::MissingDebtorNameOrAddress => { - "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing" - } - Self::MissingCreditorNameOrAddress => { - "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing" - } - Self::RegulatoryReason => "Regulatory Reason", - Self::RegulatoryInformationInvalid => { - "Regulatory or Central Bank Reporting information missing, incomplete or invalid" - } - Self::TaxInformationInvalid => "Tax information missing, incomplete or invalid", - Self::RemittanceInformationInvalid => { - "Remittance information structure does not comply with rules for payment type" - } - Self::RemittanceInformationTruncated => { - "Remittance information truncated to comply with rules for payment type" - } - Self::InvalidStructuredCreditorReference => { - "Structured creditor reference invalid or missing" - } - Self::InvalidCharacterSet => { - "Character set supplied not valid for the country and payment type" - } - Self::InvalidDebtorAgentServiceID => { - "Invalid or missing identification of a bank proprietary service" - } - Self::InvalidPartyID => { - "Invalid or missing identification required within a particular country or payment type" - } - Self::RTPNotSupportedForDebtor => "Debtor does not support request-to-pay transactions", - Self::ReturnUponUnableToApply => { - "Return following investigation request and no remediation possible" - } - Self::ValidRequestForCancellationAcknowledged => { - "Request for Cancellation is acknowledged following validation" - } - Self::UETRFlaggedForCancellation => { - "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation" - } - Self::NetworkStopOfUETR => { - "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network" - } - Self::RequestForCancellationForwarded => { - "Request for Cancellation has been forwarded to the payment processing/last payment processing agent" - } - Self::RequestForCancellationDeliveryAcknowledgement => { - "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent" - } - Self::SettlementBatchRemovalNotification => { - "Remove Concurrent Batch Processing Line on hold instruction" - } - Self::SpecificServiceOfferedByDebtorAgent => { - "Due to specific service offered by the Debtor Agent" - } - Self::SpecificServiceOfferedByCreditorAgent => { - "Due to specific service offered by the Creditor Agent" - } - Self::ServiceofClearingSystem => { - "Due to a specific service offered by the clearing system" - } - Self::CreditorNotOnWhitelistOfDebtor => { - "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet)" - } - Self::CreditorOnBlacklistOfDebtor => { - "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”" - } - Self::MaximumNumberOfDirectDebitTransactionsExceeded => { - "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent" - } - Self::MaximumDirectDebitTransactionAmountExceeded => { - "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent" - } - Self::MaximumNumberOfCreditTransactionsExceeded => { - "Maximum number of credit transactions allowed by the account servicer per service period exceeded" - } - Self::MaximumCreditTransactionsAmountExceeded => { - "Maximum total credit amount allowed by the account servicer per service period exceeded" - } - Self::DebtorNotOnWhitelistOfCreditorSide => { - "Whitelisting service offered by payment system operator or financial institution" - } - Self::DebtorOnBlacklistOfCreditorSide => { - "Blacklisting service offered by payment system operator or financial institution" - } - Self::ServiceNotRendered => { - "Services are not yet rendered by the Payee Participant (Creditor)" - } - Self::RTPServiceProviderIdentifierIncorrect => { - "Identifier of the request-to-pay service provider is incorrect" - } - Self::TransmissonAborted => { - "The transmission of the file was not successful – it had to be aborted (for technical reasons)" - } - Self::NoDataAvailable => "There is no data available (for download)", - Self::FileNonReadable => "The file cannot be read (e", - Self::IncorrectFileStructure => "The file format is incomplete or invalid", - Self::TokenInvalid => "Token is invalid", - Self::SenderTokenNotFound => "Token used for the sender does not exist", - Self::ReceiverTokenNotFound => "Token used for the receiver does not exist", - Self::TokenMissing => "Token required for request is missing", - Self::TokenCounterpartyMismatch => "Token found with counterparty mismatch", - Self::TokenSingleUse => "Single Use Token already used", - Self::TokenSuspended => "Token found with suspended status", - Self::TokenValueLimitExceeded => "Token found with value limit rule violation", - Self::TokenExpired => "Token expired", - Self::InvalidCutOffTime => { - "Associated message, payment information block, or transaction was received after agreed processing cut-off time" - } - Self::TransmissionSuccessful => { - "The (technical) transmission of the file was successful" - } - Self::TransferToSignByHand => { - "The order was transferred to pass by accompanying note signed by hand" - } - Self::UnknownCreditor => "Unknown Creditor", - Self::UnduePayment => "Payment is not justified", - } - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(DomainCode, Description, Str)] pub enum ExternalPaymentGroupStatusCode { /// Settlement on the creditor's account has been completed - #[strum(serialize = "ACCC")] + #[code = "ACCC"] AcceptedSettlementCompletedCreditorAccount, /// Preceding check of technical validation was successful - #[strum(serialize = "ACCP")] + #[code = "ACCP"] AcceptedCustomerProfile, /// Settlement on the debtor's account has been completed - #[strum(serialize = "ACSC")] + #[code = "ACSC"] AcceptedSettlementCompletedDebitorAccount, /// All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution - #[strum(serialize = "ACSP")] + #[code = "ACSP"] AcceptedSettlementInProcess, /// Authentication and syntactical and semantical validation are successful - #[strum(serialize = "ACTC")] + #[code = "ACTC"] AcceptedTechnicalValidation, /// Instruction is accepted but a change will be made, such as date or remittance not sent - #[strum(serialize = "ACWC")] + #[code = "ACWC"] AcceptedWithChange, /// A number of transactions have been accepted, whereas another number of transactions have not yet achieved - #[strum(serialize = "PART")] + #[code = "PART"] PartiallyAccepted, /// Payment initiation or individual transaction included in the payment initiation is pending - #[strum(serialize = "PDNG")] + #[code = "PDNG"] Pending, /// Verification of Payee check have been applied to received transactions stating to be complete without mismatching data - #[strum(serialize = "RCVC")] + #[code = "RCVC"] ReceivedVerificationCompleted, /// Payment initiation has been received by the receiving agent - #[strum(serialize = "RCVD")] + #[code = "RCVD"] Received, /// Payment initiation or individual transaction included in the payment initiation has been rejected - #[strum(serialize = "RJCT")] + #[code = "RJCT"] Rejected, /// Verification of Payee checks have been applied to received transactions stating to be complete containing mismatching data - #[strum(serialize = "RVCM")] + #[code = "RVCM"] ReceivedVerificationCompletedWithMismatches, /// Verification of party check on transactions received is not yet completed - #[strum(serialize = "RVNC")] + #[code = "RVNC"] ReceivedVerificationNotCompleted, } -impl ExternalPaymentGroupStatusCode { - pub fn description(&self) -> &'static str { - match self { - Self::AcceptedSettlementCompletedCreditorAccount => { - "Settlement on the creditor's account has been completed" - } - Self::AcceptedCustomerProfile => { - "Preceding check of technical validation was successful" - } - Self::AcceptedSettlementCompletedDebitorAccount => { - "Settlement on the debtor's account has been completed" - } - Self::AcceptedSettlementInProcess => { - "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution" - } - Self::AcceptedTechnicalValidation => { - "Authentication and syntactical and semantical validation are successful" - } - Self::AcceptedWithChange => { - "Instruction is accepted but a change will be made, such as date or remittance not sent" - } - Self::PartiallyAccepted => { - "A number of transactions have been accepted, whereas another number of transactions have not yet achieved" - } - Self::Pending => { - "Payment initiation or individual transaction included in the payment initiation is pending" - } - Self::ReceivedVerificationCompleted => { - "Verification of Payee check have been applied to received transactions stating to be complete without mismatching data" - } - Self::Received => "Payment initiation has been received by the receiving agent", - Self::Rejected => { - "Payment initiation or individual transaction included in the payment initiation has been rejected" - } - Self::ReceivedVerificationCompletedWithMismatches => { - "Verification of Payee checks have been applied to received transactions stating to be complete containing mismatching data" - } - Self::ReceivedVerificationNotCompleted => { - "Verification of party check on transactions received is not yet completed" - } - } - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(DomainCode, Description, Str)] pub enum ExternalPaymentTransactionStatusCode { /// Settlement on the creditor's account has been completed - #[strum(serialize = "ACCC")] + #[code = "ACCC"] AcceptedSettlementCompletedCreditorAccount, /// Preceding check of technical validation was successful - #[strum(serialize = "ACCP")] + #[code = "ACCP"] AcceptedCustomerProfile, /// Preceding check of technical validation and customer profile was successful and an automatic funds check was positive - #[strum(serialize = "ACFC")] + #[code = "ACFC"] AcceptedFundsChecked, /// Preceding check of technical validation and customer profile was successful, and an automatic funds check was positive, but an explicit confirmation by the initiating party is outstanding - #[strum(serialize = "ACFW")] + #[code = "ACFW"] AcceptedFundsCheckedWaitingConfirmation, /// Payment instruction to issue a cheque has been accepted, and the cheque has been issued but not yet been deposited or cleared - #[strum(serialize = "ACIS")] + #[code = "ACIS"] AcceptedandChequeIssued, /// Status of transaction released from the Debtor Agent and accepted by the clearing - #[strum(serialize = "ACPD")] + #[code = "ACPD"] AcceptedClearingProcessed, /// Settlement completed - #[strum(serialize = "ACSC")] + #[code = "ACSC"] AcceptedSettlementCompletedDebitorAccount, /// All preceding checks such as technical validation and customer profile were successful and therefore the payment instruction has been accepted for execution - #[strum(serialize = "ACSP")] + #[code = "ACSP"] AcceptedSettlementInProcess, /// Authentication and syntactical and semantical validation are successful - #[strum(serialize = "ACTC")] + #[code = "ACTC"] AcceptedTechnicalValidation, /// Instruction is accepted but a change will be made, such as date or remittance not sent - #[strum(serialize = "ACWC")] + #[code = "ACWC"] AcceptedWithChange, /// Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account - #[strum(serialize = "ACWP")] + #[code = "ACWP"] AcceptedWithoutPosting, /// Payment transaction previously reported with status 'ACWP' is blocked, for example, funds will neither be posted to the Creditor's account, nor be returned to the Debtor - #[strum(serialize = "BLCK")] + #[code = "BLCK"] Blocked, /// Payment initiation has been successfully cancelled after having received a request for cancellation - #[strum(serialize = "CANC")] + #[code = "CANC"] Cancelled, /// Cash has been picked up by the Creditor - #[strum(serialize = "CPUC")] + #[code = "CPUC"] CashPickedUpByCreditor, /// Payment initiation needs multiple authentications, where some but not yet all have been performed - #[strum(serialize = "PATC")] + #[code = "PATC"] PartiallyAcceptedTechnicalCorrect, /// Payment instruction is pending - #[strum(serialize = "PDNG")] + #[code = "PDNG"] Pending, /// Request for Payment has been presented to the Debtor - #[strum(serialize = "PRES")] + #[code = "PRES"] Presented, /// Verification of Payee check has been applied to received transaction stating to be complete without mismatching data - #[strum(serialize = "RCVC")] + #[code = "RCVC"] ReceivedVerificationCompleted, /// Payment instruction has been received - #[strum(serialize = "RCVD")] + #[code = "RCVD"] Received, /// Payment instruction has been rejected - #[strum(serialize = "RJCT")] + #[code = "RJCT"] Rejected, /// Verification of Payee checks have been applied to received transaction stating to be completed containing mismatching data - #[strum(serialize = "RVCM")] + #[code = "RVCM"] ReceivedVerificationCompletedWithMismatches, /// Verification of Payee check has been applied to received transaction stating to be complete with data matching closely - #[strum(serialize = "RVMC")] + #[code = "RVMC"] ReceivedVerificationCompletedMatchClosely, /// Verification of Payee check has been applied to received transaction stating to be complete with not applicable data - #[strum(serialize = "RVNA")] + #[code = "RVNA"] ReceivedVerificationCompletedNotApplicable, /// Verification of party check on the transaction is not yet completed - #[strum(serialize = "RVNC")] + #[code = "RVNC"] ReceivedVerificationNotCompleted, /// Verification of Payee check has been applied to received transaction stating to be complete with mismatching data - #[strum(serialize = "RVNM")] + #[code = "RVNM"] ReceivedVerificationCompletedNoMatch, } -impl ExternalPaymentTransactionStatusCode { - pub fn description(&self) -> &'static str { - match self { - Self::AcceptedSettlementCompletedCreditorAccount => { - "Settlement on the creditor's account has been completed" - } - Self::AcceptedCustomerProfile => { - "Preceding check of technical validation was successful" - } - Self::AcceptedFundsChecked => { - "Preceding check of technical validation and customer profile was successful and an automatic funds check was positive" - } - Self::AcceptedFundsCheckedWaitingConfirmation => { - "Preceding check of technical validation and customer profile was successful, and an automatic funds check was positive, but an explicit confirmation by the initiating party is outstanding" - } - Self::AcceptedandChequeIssued => { - "Payment instruction to issue a cheque has been accepted, and the cheque has been issued but not yet been deposited or cleared" - } - Self::AcceptedClearingProcessed => { - "Status of transaction released from the Debtor Agent and accepted by the clearing" - } - Self::AcceptedSettlementCompletedDebitorAccount => "Settlement completed", - Self::AcceptedSettlementInProcess => { - "All preceding checks such as technical validation and customer profile were successful and therefore the payment instruction has been accepted for execution" - } - Self::AcceptedTechnicalValidation => { - "Authentication and syntactical and semantical validation are successful" - } - Self::AcceptedWithChange => { - "Instruction is accepted but a change will be made, such as date or remittance not sent" - } - Self::AcceptedWithoutPosting => { - "Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account" - } - Self::Blocked => { - "Payment transaction previously reported with status 'ACWP' is blocked, for example, funds will neither be posted to the Creditor's account, nor be returned to the Debtor" - } - Self::Cancelled => { - "Payment initiation has been successfully cancelled after having received a request for cancellation" - } - Self::CashPickedUpByCreditor => "Cash has been picked up by the Creditor", - Self::PartiallyAcceptedTechnicalCorrect => { - "Payment initiation needs multiple authentications, where some but not yet all have been performed" - } - Self::Pending => "Payment instruction is pending", - Self::Presented => "Request for Payment has been presented to the Debtor", - Self::ReceivedVerificationCompleted => { - "Verification of Payee check has been applied to received transaction stating to be complete without mismatching data" - } - Self::Received => "Payment instruction has been received", - Self::Rejected => "Payment instruction has been rejected", - Self::ReceivedVerificationCompletedWithMismatches => { - "Verification of Payee checks have been applied to received transaction stating to be completed containing mismatching data" - } - Self::ReceivedVerificationCompletedMatchClosely => { - "Verification of Payee check has been applied to received transaction stating to be complete with data matching closely" - } - Self::ReceivedVerificationCompletedNotApplicable => { - "Verification of Payee check has been applied to received transaction stating to be complete with not applicable data" - } - Self::ReceivedVerificationNotCompleted => { - "Verification of party check on the transaction is not yet completed" - } - Self::ReceivedVerificationCompletedNoMatch => { - "Verification of Payee check has been applied to received transaction stating to be complete with mismatching data" - } - } - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(DomainCode, Description, Str)] pub enum ExternalReturnReasonCode { /// Format of the account number specified is not correct - #[strum(serialize = "AC01")] + #[code = "AC01"] IncorrectAccountNumber, /// Debtor account number invalid or missing - #[strum(serialize = "AC02")] + #[code = "AC02"] InvalidDebtorAccountNumber, /// Wrong IBAN in SCT - #[strum(serialize = "AC03")] + #[code = "AC03"] InvalidCreditorAccountNumber, /// Account number specified has been closed on the bank of account's books - #[strum(serialize = "AC04")] + #[code = "AC04"] ClosedAccountNumber, /// Account specified is blocked, prohibiting posting of transactions against it - #[strum(serialize = "AC06")] + #[code = "AC06"] BlockedAccount, /// Creditor account number closed - #[strum(serialize = "AC07")] + #[code = "AC07"] ClosedCreditorAccountNumber, /// Debtor account type is missing or invalid - #[strum(serialize = "AC13")] + #[code = "AC13"] InvalidDebtorAccountType, /// An agent in the payment chain is invalid - #[strum(serialize = "AC14")] + #[code = "AC14"] InvalidAgent, /// Account details have changed - #[strum(serialize = "AC15")] + #[code = "AC15"] AccountDetailsChanged, /// Account is in sequestration - #[strum(serialize = "AC16")] + #[code = "AC16"] AccountInSequestration, /// Account is in liquidation - #[strum(serialize = "AC17")] + #[code = "AC17"] AccountInLiquidation, /// Transaction forbidden on this type of account (formerly NoAgreement) - #[strum(serialize = "AG01")] + #[code = "AG01"] TransactionForbidden, /// Bank Operation code specified in the message is not valid for receiver - #[strum(serialize = "AG02")] + #[code = "AG02"] InvalidBankOperationCode, /// Debtor account cannot be debited for a generic reason - #[strum(serialize = "AG07")] + #[code = "AG07"] UnsuccesfulDirectDebit, /// Agent in the payment workflow is incorrect - #[strum(serialize = "AGNT")] + #[code = "AGNT"] IncorrectAgent, /// Specified message amount is equal to zero - #[strum(serialize = "AM01")] + #[code = "AM01"] ZeroAmount, /// Specific transaction/message amount is greater than allowed maximum - #[strum(serialize = "AM02")] + #[code = "AM02"] NotAllowedAmount, /// Specified message amount is an non processable currency outside of existing agreement - #[strum(serialize = "AM03")] + #[code = "AM03"] NotAllowedCurrency, /// Amount of funds available to cover specified message amount is insufficient - #[strum(serialize = "AM04")] + #[code = "AM04"] InsufficientFunds, /// Duplication - #[strum(serialize = "AM05")] + #[code = "AM05"] Duplication, /// Specified transaction amount is less than agreed minimum - #[strum(serialize = "AM06")] + #[code = "AM06"] TooLowAmount, /// Amount specified in message has been blocked by regulatory authorities - #[strum(serialize = "AM07")] + #[code = "AM07"] BlockedAmount, /// Amount received is not the amount agreed or expected - #[strum(serialize = "AM09")] + #[code = "AM09"] WrongAmount, /// Sum of instructed amounts does not equal the control sum - #[strum(serialize = "AM10")] + #[code = "AM10"] InvalidControlSum, /// Already returned original SCT - #[strum(serialize = "ARDT")] + #[code = "ARDT"] AlreadyReturnedTransaction, /// Identification of end customer is not consistent with associated account number, organisation ID or private ID - #[strum(serialize = "BE01")] + #[code = "BE01"] InconsistenWithEndCustomer, /// Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress) - #[strum(serialize = "BE04")] + #[code = "BE04"] MissingCreditorAddress, /// Party who initiated the message is not recognised by the end customer - #[strum(serialize = "BE05")] + #[code = "BE05"] UnrecognisedInitiatingParty, /// End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books - #[strum(serialize = "BE06")] + #[code = "BE06"] UnknownEndCustomer, /// Specification of debtor's address, which is required for payment, is missing/not correct - #[strum(serialize = "BE07")] + #[code = "BE07"] MissingDebtorAddress, /// Returned as a result of a bank error - #[strum(serialize = "BE08")] + #[code = "BE08"] BankError, /// Debtor country code is missing or invalid - #[strum(serialize = "BE10")] + #[code = "BE10"] InvalidDebtorCountry, /// Creditor country code is missing or invalid - #[strum(serialize = "BE11")] + #[code = "BE11"] InvalidCreditorCountry, /// Debtor or Ultimate Debtor identification code missing or invalid - #[strum(serialize = "BE16")] + #[code = "BE16"] InvalidDebtorIdentificationCode, /// Creditor or Ultimate Creditor identification code missing or invalid - #[strum(serialize = "BE17")] + #[code = "BE17"] InvalidCreditorIdentificationCode, /// Authorisation is cancelled - #[strum(serialize = "CN01")] + #[code = "CN01"] AuthorisationCancelled, /// Creditor bank is not registered under this BIC in the CSM - #[strum(serialize = "CNOR")] + #[code = "CNOR"] CreditorBankIsNotRegistered, /// Cash not picked up by Creditor or cash could not be delivered to Creditor - #[strum(serialize = "CNPC")] + #[code = "CNPC"] CashNotPickedUp, /// Currency of the payment is incorrect - #[strum(serialize = "CURR")] + #[code = "CURR"] IncorrectCurrency, /// Cancellation requested by the Debtor - #[strum(serialize = "CUST")] + #[code = "CUST"] RequestedByCustomer, /// Return of Covering Settlement due to the underlying Credit Transfer details not being received - #[strum(serialize = "DC04")] + #[code = "DC04"] NoCustomerCreditTransferReceived, /// Debtor bank is not registered under this BIC in the CSM - #[strum(serialize = "DNOR")] + #[code = "DNOR"] DebtorBankIsNotRegistered, /// Return following technical problems resulting in erroneous transaction - #[strum(serialize = "DS28")] + #[code = "DS28"] ReturnForTechnicalReason, /// Invalid date (eg, wrong settlement date) - #[strum(serialize = "DT01")] + #[code = "DT01"] InvalidDate, /// Cheque has been issued but not deposited and is considered expired - #[strum(serialize = "DT02")] + #[code = "DT02"] ChequeExpired, /// Future date not supported - #[strum(serialize = "DT04")] + #[code = "DT04"] FutureDateNotSupported, /// Payment is a duplicate of another payment - #[strum(serialize = "DUPL")] + #[code = "DUPL"] DuplicatePayment, /// Correspondent bank not possible - #[strum(serialize = "ED01")] + #[code = "ED01"] CorrespondentBankNotPossible, /// Balance of payments complementary info is requested - #[strum(serialize = "ED03")] + #[code = "ED03"] BalanceInfoRequest, /// Settlement of the transaction has failed - #[strum(serialize = "ED05")] + #[code = "ED05"] SettlementFailed, /// The card payment is fraudulent and was not processed with EMV technology for an EMV card - #[strum(serialize = "EMVL")] + #[code = "EMVL"] EMVLiabilityShift, /// The Extended Remittance Information (ERI) option is not supported - #[strum(serialize = "ERIN")] + #[code = "ERIN"] ERIOptionNotSupported, /// Payment Type Information is missing or invalid - #[strum(serialize = "FF03")] + #[code = "FF03"] InvalidPaymentTypeInformation, /// Service Level code is missing or invalid - #[strum(serialize = "FF04")] + #[code = "FF04"] InvalidServiceLevelCode, /// Local Instrument code is missing or invalid - #[strum(serialize = "FF05")] + #[code = "FF05"] InvalidLocalInstrumentCode, /// Category Purpose code is missing or invalid - #[strum(serialize = "FF06")] + #[code = "FF06"] InvalidCategoryPurposeCode, /// Purpose is missing or invalid - #[strum(serialize = "FF07")] + #[code = "FF07"] InvalidPurpose, /// Return following a cancellation request - #[strum(serialize = "FOCR")] + #[code = "FOCR"] FollowingCancellationRequest, /// Returned as a result of fraud - #[strum(serialize = "FR01")] + #[code = "FR01"] Fraud, /// Final response/tracking is recalled as mandate is cancelled - #[strum(serialize = "FRTR")] + #[code = "FRTR"] FinalResponseMandateCancelled, /// In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover - #[strum(serialize = "G004")] + #[code = "G004"] CreditPendingFunds, /// No Mandate - #[strum(serialize = "MD01")] + #[code = "MD01"] NoMandate, /// Mandate related information data required by the scheme is missing - #[strum(serialize = "MD02")] + #[code = "MD02"] MissingMandatoryInformationInMandate, /// Creditor or creditor's agent should not have collected the direct debit - #[strum(serialize = "MD05")] + #[code = "MD05"] CollectionNotDue, /// Return of funds requested by end customer - #[strum(serialize = "MD06")] + #[code = "MD06"] RefundRequestByEndCustomer, /// End customer is deceased - #[strum(serialize = "MD07")] + #[code = "MD07"] EndCustomerDeceased, /// Reason has not been specified by end customer - #[strum(serialize = "MS02")] + #[code = "MS02"] NotSpecifiedReasonCustomerGenerated, /// Reason has not been specified by agent - #[strum(serialize = "MS03")] + #[code = "MS03"] NotSpecifiedReasonAgentGenerated, /// Reason is provided as narrative information in the additional reason information - #[strum(serialize = "NARR")] + #[code = "NARR"] Narrative, /// No response from Beneficiary - #[strum(serialize = "NOAS")] + #[code = "NOAS"] NoAnswerFromCustomer, /// Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing - #[strum(serialize = "NOCM")] + #[code = "NOCM"] NotCompliant, /// Original SCT never received - #[strum(serialize = "NOOR")] + #[code = "NOOR"] NoOriginalTransactionReceived, /// The card payment is fraudulent (lost and stolen fraud) and was processed as EMV transaction without PIN verification - #[strum(serialize = "PINL")] + #[code = "PINL"] PINLiabilityShift, /// Bank Identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode) - #[strum(serialize = "RC01")] + #[code = "RC01"] BankIdentifierIncorrect, /// Debtor bank identifier is invalid or missing - #[strum(serialize = "RC03")] + #[code = "RC03"] InvalidDebtorBankIdentifier, /// Creditor bank identifier is invalid or missing - #[strum(serialize = "RC04")] + #[code = "RC04"] InvalidCreditorBankIdentifier, /// Incorrrect BIC of the beneficiary Bank in the SCTR - #[strum(serialize = "RC07")] + #[code = "RC07"] InvalidCreditorBICIdentifier, /// ClearingSystemMemberidentifier is invalid or missing - #[strum(serialize = "RC08")] + #[code = "RC08"] InvalidClearingSystemMemberIdentifier, /// Intermediary Agent is invalid or missing - #[strum(serialize = "RC11")] + #[code = "RC11"] InvalidIntermediaryAgent, /// Transaction reference is not unique within the message - #[strum(serialize = "RF01")] + #[code = "RF01"] NotUniqueTransactionReference, /// Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing - #[strum(serialize = "RR01")] + #[code = "RR01"] MissingDebtorAccountOrIdentification, /// Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing - #[strum(serialize = "RR02")] + #[code = "RR02"] MissingDebtorNameOrAddress, /// Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing - #[strum(serialize = "RR03")] + #[code = "RR03"] MissingCreditorNameOrAddress, /// Regulatory Reason - #[strum(serialize = "RR04")] + #[code = "RR04"] RegulatoryReason, /// Regulatory or Central Bank Reporting information missing, incomplete or invalid - #[strum(serialize = "RR05")] + #[code = "RR05"] RegulatoryInformationInvalid, /// Tax information missing, incomplete or invalid - #[strum(serialize = "RR06")] + #[code = "RR06"] TaxInformationInvalid, /// Remittance information structure does not comply with rules for payment type - #[strum(serialize = "RR07")] + #[code = "RR07"] RemittanceInformationInvalid, /// Remittance information truncated to comply with rules for payment type - #[strum(serialize = "RR08")] + #[code = "RR08"] RemittanceInformationTruncated, /// Structured creditor reference invalid or missing - #[strum(serialize = "RR09")] + #[code = "RR09"] InvalidStructuredCreditorReference, /// Invalid or missing identification of a bank proprietary service - #[strum(serialize = "RR11")] + #[code = "RR11"] InvalidDebtorAgentServiceIdentification, /// Invalid or missing identification required within a particular country or payment type - #[strum(serialize = "RR12")] + #[code = "RR12"] InvalidPartyIdentification, /// Return following investigation request and no remediation possible - #[strum(serialize = "RUTA")] + #[code = "RUTA"] ReturnUponUnableToApply, /// Due to specific service offered by the Debtor Agent - #[strum(serialize = "SL01")] + #[code = "SL01"] SpecificServiceOfferedByDebtorAgent, /// Due to specific service offered by the Creditor Agent - #[strum(serialize = "SL02")] + #[code = "SL02"] SpecificServiceOfferedByCreditorAgent, /// Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet) - #[strum(serialize = "SL11")] + #[code = "SL11"] CreditorNotOnWhitelistOfDebtor, /// Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist” - #[strum(serialize = "SL12")] + #[code = "SL12"] CreditorOnBlacklistOfDebtor, /// Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent - #[strum(serialize = "SL13")] + #[code = "SL13"] MaximumNumberOfDirectDebitTransactionsExceeded, /// Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent - #[strum(serialize = "SL14")] + #[code = "SL14"] MaximumDirectDebitTransactionAmountExceeded, /// Payment is stopped by account holder - #[strum(serialize = "SP01")] + #[code = "SP01"] PaymentStopped, /// Previously stopped by means of a stop payment advise - #[strum(serialize = "SP02")] + #[code = "SP02"] PreviouslyStopped, /// The card payment is returned since a cash amount rendered was not correct or goods or a service was not rendered to the customer, e - #[strum(serialize = "SVNR")] + #[code = "SVNR"] ServiceNotRendered, /// Associated message was received after agreed processing cut-off time - #[strum(serialize = "TM01")] + #[code = "TM01"] CutOffTime, /// Return following direct debit being removed from tracking process - #[strum(serialize = "TRAC")] + #[code = "TRAC"] RemovedFromTracking, /// Payment is not justified - #[strum(serialize = "UPAY")] + #[code = "UPAY"] UnduePayment, } -impl ExternalReturnReasonCode { - pub fn description(&self) -> &'static str { - match self { - Self::IncorrectAccountNumber => "Format of the account number specified is not correct", - Self::InvalidDebtorAccountNumber => "Debtor account number invalid or missing", - Self::InvalidCreditorAccountNumber => "Wrong IBAN in SCT", - Self::ClosedAccountNumber => { - "Account number specified has been closed on the bank of account's books" - } - Self::BlockedAccount => { - "Account specified is blocked, prohibiting posting of transactions against it" - } - Self::ClosedCreditorAccountNumber => "Creditor account number closed", - Self::InvalidDebtorAccountType => "Debtor account type is missing or invalid", - Self::InvalidAgent => "An agent in the payment chain is invalid", - Self::AccountDetailsChanged => "Account details have changed", - Self::AccountInSequestration => "Account is in sequestration", - Self::AccountInLiquidation => "Account is in liquidation", - Self::TransactionForbidden => { - "Transaction forbidden on this type of account (formerly NoAgreement)" - } - Self::InvalidBankOperationCode => { - "Bank Operation code specified in the message is not valid for receiver" - } - Self::UnsuccesfulDirectDebit => "Debtor account cannot be debited for a generic reason", - Self::IncorrectAgent => "Agent in the payment workflow is incorrect", - Self::ZeroAmount => "Specified message amount is equal to zero", - Self::NotAllowedAmount => { - "Specific transaction/message amount is greater than allowed maximum" - } - Self::NotAllowedCurrency => { - "Specified message amount is an non processable currency outside of existing agreement" - } - Self::InsufficientFunds => { - "Amount of funds available to cover specified message amount is insufficient" - } - Self::Duplication => "Duplication", - Self::TooLowAmount => "Specified transaction amount is less than agreed minimum", - Self::BlockedAmount => { - "Amount specified in message has been blocked by regulatory authorities" - } - Self::WrongAmount => "Amount received is not the amount agreed or expected", - Self::InvalidControlSum => "Sum of instructed amounts does not equal the control sum", - Self::AlreadyReturnedTransaction => "Already returned original SCT", - Self::InconsistenWithEndCustomer => { - "Identification of end customer is not consistent with associated account number, organisation ID or private ID" - } - Self::MissingCreditorAddress => { - "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)" - } - Self::UnrecognisedInitiatingParty => { - "Party who initiated the message is not recognised by the end customer" - } - Self::UnknownEndCustomer => { - "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books" - } - Self::MissingDebtorAddress => { - "Specification of debtor's address, which is required for payment, is missing/not correct" - } - Self::BankError => "Returned as a result of a bank error", - Self::InvalidDebtorCountry => "Debtor country code is missing or invalid", - Self::InvalidCreditorCountry => "Creditor country code is missing or invalid", - Self::InvalidDebtorIdentificationCode => { - "Debtor or Ultimate Debtor identification code missing or invalid" - } - Self::InvalidCreditorIdentificationCode => { - "Creditor or Ultimate Creditor identification code missing or invalid" - } - Self::AuthorisationCancelled => "Authorisation is cancelled", - Self::CreditorBankIsNotRegistered => { - "Creditor bank is not registered under this BIC in the CSM" - } - Self::CashNotPickedUp => { - "Cash not picked up by Creditor or cash could not be delivered to Creditor" - } - Self::IncorrectCurrency => "Currency of the payment is incorrect", - Self::RequestedByCustomer => "Cancellation requested by the Debtor", - Self::NoCustomerCreditTransferReceived => { - "Return of Covering Settlement due to the underlying Credit Transfer details not being received" - } - Self::DebtorBankIsNotRegistered => { - "Debtor bank is not registered under this BIC in the CSM" - } - Self::ReturnForTechnicalReason => { - "Return following technical problems resulting in erroneous transaction" - } - Self::InvalidDate => "Invalid date (eg, wrong settlement date)", - Self::ChequeExpired => { - "Cheque has been issued but not deposited and is considered expired" - } - Self::FutureDateNotSupported => "Future date not supported", - Self::DuplicatePayment => "Payment is a duplicate of another payment", - Self::CorrespondentBankNotPossible => "Correspondent bank not possible", - Self::BalanceInfoRequest => "Balance of payments complementary info is requested", - Self::SettlementFailed => "Settlement of the transaction has failed", - Self::EMVLiabilityShift => { - "The card payment is fraudulent and was not processed with EMV technology for an EMV card" - } - Self::ERIOptionNotSupported => { - "The Extended Remittance Information (ERI) option is not supported" - } - Self::InvalidPaymentTypeInformation => "Payment Type Information is missing or invalid", - Self::InvalidServiceLevelCode => "Service Level code is missing or invalid", - Self::InvalidLocalInstrumentCode => "Local Instrument code is missing or invalid", - Self::InvalidCategoryPurposeCode => "Category Purpose code is missing or invalid", - Self::InvalidPurpose => "Purpose is missing or invalid", - Self::FollowingCancellationRequest => "Return following a cancellation request", - Self::Fraud => "Returned as a result of fraud", - Self::FinalResponseMandateCancelled => { - "Final response/tracking is recalled as mandate is cancelled" - } - Self::CreditPendingFunds => { - "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover" - } - Self::NoMandate => "No Mandate", - Self::MissingMandatoryInformationInMandate => { - "Mandate related information data required by the scheme is missing" - } - Self::CollectionNotDue => { - "Creditor or creditor's agent should not have collected the direct debit" - } - Self::RefundRequestByEndCustomer => "Return of funds requested by end customer", - Self::EndCustomerDeceased => "End customer is deceased", - Self::NotSpecifiedReasonCustomerGenerated => { - "Reason has not been specified by end customer" - } - Self::NotSpecifiedReasonAgentGenerated => "Reason has not been specified by agent", - Self::Narrative => { - "Reason is provided as narrative information in the additional reason information" - } - Self::NoAnswerFromCustomer => "No response from Beneficiary", - Self::NotCompliant => { - "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing" - } - Self::NoOriginalTransactionReceived => "Original SCT never received", - Self::PINLiabilityShift => { - "The card payment is fraudulent (lost and stolen fraud) and was processed as EMV transaction without PIN verification" - } - Self::BankIdentifierIncorrect => { - "Bank Identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)" - } - Self::InvalidDebtorBankIdentifier => "Debtor bank identifier is invalid or missing", - Self::InvalidCreditorBankIdentifier => "Creditor bank identifier is invalid or missing", - Self::InvalidCreditorBICIdentifier => { - "Incorrrect BIC of the beneficiary Bank in the SCTR" - } - Self::InvalidClearingSystemMemberIdentifier => { - "ClearingSystemMemberidentifier is invalid or missing" - } - Self::InvalidIntermediaryAgent => "Intermediary Agent is invalid or missing", - Self::NotUniqueTransactionReference => { - "Transaction reference is not unique within the message" - } - Self::MissingDebtorAccountOrIdentification => { - "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing" - } - Self::MissingDebtorNameOrAddress => { - "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing" - } - Self::MissingCreditorNameOrAddress => { - "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing" - } - Self::RegulatoryReason => "Regulatory Reason", - Self::RegulatoryInformationInvalid => { - "Regulatory or Central Bank Reporting information missing, incomplete or invalid" - } - Self::TaxInformationInvalid => "Tax information missing, incomplete or invalid", - Self::RemittanceInformationInvalid => { - "Remittance information structure does not comply with rules for payment type" - } - Self::RemittanceInformationTruncated => { - "Remittance information truncated to comply with rules for payment type" - } - Self::InvalidStructuredCreditorReference => { - "Structured creditor reference invalid or missing" - } - Self::InvalidDebtorAgentServiceIdentification => { - "Invalid or missing identification of a bank proprietary service" - } - Self::InvalidPartyIdentification => { - "Invalid or missing identification required within a particular country or payment type" - } - Self::ReturnUponUnableToApply => { - "Return following investigation request and no remediation possible" - } - Self::SpecificServiceOfferedByDebtorAgent => { - "Due to specific service offered by the Debtor Agent" - } - Self::SpecificServiceOfferedByCreditorAgent => { - "Due to specific service offered by the Creditor Agent" - } - Self::CreditorNotOnWhitelistOfDebtor => { - "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet)" - } - Self::CreditorOnBlacklistOfDebtor => { - "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”" - } - Self::MaximumNumberOfDirectDebitTransactionsExceeded => { - "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent" - } - Self::MaximumDirectDebitTransactionAmountExceeded => { - "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent" - } - Self::PaymentStopped => "Payment is stopped by account holder", - Self::PreviouslyStopped => "Previously stopped by means of a stop payment advise", - Self::ServiceNotRendered => { - "The card payment is returned since a cash amount rendered was not correct or goods or a service was not rendered to the customer, e" - } - Self::CutOffTime => { - "Associated message was received after agreed processing cut-off time" - } - Self::RemovedFromTracking => { - "Return following direct debit being removed from tracking process" - } - Self::UnduePayment => "Payment is not justified", - } - } -} diff --git a/src/iso20022/mod.rs b/src/iso20022/mod.rs @@ -17,5 +17,160 @@ * <http://www.gnu.org/licenses/> */ +use taler_enum_meta::EnumMeta; + pub mod bank_tx_code; +pub mod camt; pub mod external_code; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Description, Str)] +#[allow(non_camel_case_types)] +pub enum HacAction { + /// File submitted to the bank + FILE_UPLOAD, + /// File downloaded from the bank + FILE_DOWNLOAD, + /// Electronic signature submitted to the bank + ES_UPLOAD, + /// Electronic signature downloaded from the bank + ES_DOWNLOAD, + /// Signature verification + ES_VERIFICATION, + /// Forwarding to EDS + VEU_FORWARDING, + /// EDS signature verification + VEU_VERIFICATION, + /// Forwarded for postprocessing + VEU_VERIFICATION_END, + /// Cancellation of EDS order + VEU_CANCEL_ORDER, + /// Additional information + ADDITIONAL, + /// HAC end of order (positive) + ORDER_HAC_FINAL_POS, + /// HAC end of order (negative) + ORDER_HAC_FINAL_NEG, + // Not in the spec but Credit Suisse test suite use it + /// HAC end of order + ORDER_HAC_FINAL, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Description, Str)] +pub enum ChargeBearer { + /// BorneByDebtor + DEBT, + /// BorneByCreditor + CRED, + /// Shared + SHAR, + /// SLEV + SLEV, +} + +#[cfg(test)] +pub mod test { + use tracing::info; + + use crate::iso20022::camt::parse_txs; + + #[test] + pub fn sample() { + taler_test_utils::setup_tracing(); + let mut samples = Vec::new(); + for entry in std::fs::read_dir("testbench/sample").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + for entry in std::fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + samples.push((entry.path(), entry.file_name())); + } + } else { + samples.push((path, entry.file_name())); + } + } + for (path, name) in samples { + let content = std::fs::read_to_string(&path).unwrap(); + let name = name.to_string_lossy(); + + info!("Parse sample {path:?}"); + + if name.contains("hac") { + // TODO + } else if name.contains("camt") { + parse_txs(&content).unwrap(); + } else if name.contains("pain002") { + // TODO + } else if name.contains("pain001") { + // TODO + } else { + panic!("Unsupported file type {name}") + } + } + } + + #[test] + pub fn logs() { + taler_test_utils::setup_tracing(); + + if !std::fs::exists("testbench/test").unwrap() { + return; + } + for platform in std::fs::read_dir("testbench/test") + .unwrap() + .map(Result::unwrap) + { + let path = platform.path(); + if !path.is_dir() || platform.file_name() == "platform" { + continue; + } + + // List logs + let mut logs = Vec::new(); + for date in std::fs::read_dir(path).unwrap().map(Result::unwrap) { + let path = date.path(); + if !path.is_dir() { + continue; + } + for tx in std::fs::read_dir(path).unwrap().map(Result::unwrap) { + let payload = tx.path().join("payload"); + if payload.exists() { + logs.extend( + std::fs::read_dir(payload) + .unwrap() + .map(|it| it.unwrap().path()), + ); + } + let payload = tx.path().join("payload.xml"); + if payload.exists() { + logs.push(payload); + } + } + } + for path in logs { + let content = std::fs::read_to_string(&path).unwrap(); + let path = path.to_string_lossy(); + + info!("Parse sample {path:?}"); + + if path.contains("HAC") { + // TODO + } else if path.contains("HKD") { + // TODO + } else if path.contains("HAA") { + // TODO + } else if path.contains("camt") { + parse_txs(&content).unwrap(); + } else if path.contains("pain.002") { + // TODO + } else if path.contains("pain.001") { + // TODO + } else { + panic!("Unsupported file type {path}") + } + } + } + } +} diff --git a/src/key_management.rs b/src/key_management.rs @@ -33,11 +33,11 @@ use crate::{ EbicsResponse, common::{DataEncryptionInfo, EbicsLogger, decrypt_and_decompress_payload}, config::{EbicsHostCfg, EbicsKeysCfg}, - crypto::{rsa_private_from_x509_certificate_from, x509_certificate_from_rsa_private}, + crypto::{rsa_private_from_b64_x509_certificate, x509_certificate_from_rsa_private}, ebics_code::EbicsReturnCode, keys::{self, BankPubKeysFile, ClientPriKeysFile}, post_to_bank, - xml::{XmlReader, XmlWriter}, + xml::{self, Xml, XmlAccess as _, XmlWriter}, xml_build, xml_el, xml_sign::sign_ebics, }; @@ -113,40 +113,39 @@ pub async fn hpb( .ok_or_fail(order.name())? .expect("{order}: missing order data"); - fn rsa_pub_key(xml: XmlReader) -> PublicEncryptingKey { - let der = xml.one("X509Data").one("X509Certificate").b64(); - rsa_private_from_x509_certificate_from(&der) + fn rsa_pub_key(xml: Xml) -> xml::Result<PublicEncryptingKey> { + xml.one("X509Data") + .one("X509Certificate") + .decode(rsa_private_from_b64_x509_certificate) } - Ok(XmlReader::parse( + Ok(Xml::parse_str( &order_data, "HPBResponseOrderData", |root| { - let auth_pub_info = root.one("AuthenticationPubKeyInfo"); - let version = auth_pub_info.one("AuthenticationVersion"); - let version = version.text(); + let auth_pub = root.one("AuthenticationPubKeyInfo")?; + let version = auth_pub.one("AuthenticationVersion")?.text(); assert_eq!( version, "X002", "Expected authentication version X002 got unsupported {version}" ); - let auth_pub = rsa_pub_key(auth_pub_info); + let auth_pub = rsa_pub_key(auth_pub)?; - let enc_pub_info = root.one("EncryptionPubKeyInfo"); - let version = enc_pub_info.one("EncryptionVersion"); - let version = version.text(); + let enc_pub = root.one("EncryptionPubKeyInfo")?; + let version = enc_pub.one("EncryptionVersion")?.text(); assert_eq!( version, "E002", "Expected encryption version E002 got unsupported {version}" ); - let enc_pub = rsa_pub_key(enc_pub_info); + let enc_pub = rsa_pub_key(enc_pub)?; - BankPubKeysFile { + Ok(BankPubKeysFile { bank_authentication_public_key: auth_pub, bank_encryption_public_key: enc_pub, accepted: false, - } + }) }, - )) + )?) } pub async fn key_management( @@ -286,7 +285,7 @@ pub async fn key_management( msg }; let res = post_to_bank(cfg.base_url.as_str(), http, signed).await?; - Ok(XmlReader::parse( + Ok(Xml::parse_str( &res, "ebicsKeyManagementResponse", |root| { @@ -294,18 +293,16 @@ pub async fn key_management( .one_signed("header") .one("mutable") .one("ReturnCode") - .text() - .parse() - .unwrap(); - let body = root.one("body"); - let bank_code = body.one_signed("ReturnCode").text().parse().unwrap(); - let content = if let Some(data) = body.opt("DataTransfer") { - let info = data.one_signed("DataEncryptionInfo"); + .parse()?; + let body = root.one("body")?; + let bank_code = body.one_signed("ReturnCode").parse()?; + let content = if let Some(data) = body.opt("DataTransfer")? { + let info = data.one_signed("DataEncryptionInfo")?; let info = DataEncryptionInfo { - transaction_key: info.one("TransactionKey").b64(), - bank_pub_digest: info.one("EncryptionPubKeyDigest").b64(), + transaction_key: info.one("TransactionKey").b64()?, + bank_pub_digest: info.one("EncryptionPubKeyDigest").b64()?, }; - let chunk = data.one("OrderData").b64(); + let chunk = data.one("OrderData").b64()?; let decoded = decrypt_and_decompress_payload( &client.encryption_private_key, info, @@ -315,11 +312,11 @@ pub async fn key_management( } else { None }; - EbicsResponse { + Ok(EbicsResponse { technical_code, bank_code, content, - } + }) }, - )) + )?) } diff --git a/src/lib.rs b/src/lib.rs @@ -36,8 +36,9 @@ use crate::{ ebics_code::EbicsReturnCode, key_management::{Order, hpb, submit_client_keys}, keys::{load_bank_keys, load_client_keys, persist_client_keys}, + xml::XmlAccess as _, }; -use crate::{keys::ClientPriKeysFile, xml::XmlReader}; +use crate::{keys::ClientPriKeysFile, xml::Xml}; pub mod api; pub mod common; @@ -148,26 +149,21 @@ pub async fn hev(http: &Client, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<Versio } ); let res = post_to_bank(cfg.base_url.as_str(), http, msg).await?; - XmlReader::parse(&res, "ebicsHEVResponse", |root| { - let technical_code = root - .one("SystemReturnCode") - .one("ReturnCode") - .text() - .parse() - .unwrap(); + Xml::parse_str(&res, "ebicsHEVResponse", |root| { + let technical_code = root.one("SystemReturnCode").one("ReturnCode").parse()?; let versions: Vec<_> = root - .each("VersionNumber") + .many("VersionNumber") .map(|n| VersionNumber { - number: n.text().parse().unwrap(), - schema: n.attr("ProtocolVersion").to_owned(), + number: n.text().to_owned(), + schema: n.attr("ProtocolVersion").expect("TODO").to_owned(), }) .collect(); - EbicsResponse { + Ok(EbicsResponse { technical_code, bank_code: EbicsReturnCode::EBICS_OK, content: versions, - } - }) + }) + })? .ok_or_fail(phase) } diff --git a/src/model.rs b/src/model.rs @@ -70,8 +70,8 @@ impl From<TransferState> for SubmissionState { } /// ID for incoming transactions -#[derive(Debug, Clone)] -pub struct IncomingId { +#[derive(Clone, PartialEq, Eq)] +pub struct InId { /** ISO20022 UETR */ pub uetr: Option<Uuid>, /// ISO20022 TxID @@ -80,7 +80,7 @@ pub struct IncomingId { pub acct_svcr_ref: Option<CompactString>, } -impl IncomingId { +impl InId { pub fn new( uetr: Option<Uuid>, tx_id: Option<CompactString>, @@ -103,7 +103,7 @@ impl IncomingId { } } -impl std::fmt::Display for IncomingId { +impl std::fmt::Display for InId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_char('(')?; let mut prepend = false; @@ -131,41 +131,61 @@ impl std::fmt::Display for IncomingId { } } +impl std::fmt::Debug for InId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /// ID for outgoing transactions -pub struct OutgoingId { +#[derive(Clone, PartialEq, Eq)] +pub struct OutId { /// Unique msg ID generated by libeufin-nexus /// ISO20022 MessageId pub msg_id: Option<CompactString>, /// Unique end-to-end ID generated by libeufin-nexus /// ISO20022 EndToEndId or MessageId (retrocompatibility) - pub end_to_end_id: Option<CompactString>, + pub e2e_id: Option<CompactString>, /// Unique end-to-end ID generated by the bank /// ISO20022 AcctSvcrRef - pub acct_svcr_ref: Option<CompactString>, + pub sref: Option<CompactString>, } -impl OutgoingId { +impl OutId { + pub fn new( + msg_id: Option<CompactString>, + e2e_id: Option<CompactString>, + acct_svcr_ref: Option<CompactString>, + ) -> Self { + assert!(msg_id.is_some() || e2e_id.is_some() || acct_svcr_ref.is_some()); + Self { + msg_id, + e2e_id, + sref: acct_svcr_ref, + } + } + pub fn r#ref(&self) -> CompactString { - self.end_to_end_id + self.e2e_id .clone() - .or(self.acct_svcr_ref.clone()) - .or(self.acct_svcr_ref.clone()) + .or(self.sref.clone()) + .or(self.sref.clone()) .expect("must be at least one ref") } } -impl std::fmt::Display for OutgoingId { +impl std::fmt::Display for OutId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_char('(')?; let mut prepend = false; if let Some(msg_id) = &self.msg_id - && self.msg_id != self.end_to_end_id + && self.msg_id != self.e2e_id { f.write_str("msg=")?; f.write_str(msg_id)?; prepend = true; } - if let Some(end_to_end_id) = &self.end_to_end_id { + if let Some(end_to_end_id) = &self.e2e_id { if prepend { f.write_char(' ')?; } @@ -173,7 +193,7 @@ impl std::fmt::Display for OutgoingId { f.write_str(end_to_end_id)?; prepend = true; } - if let Some(acct_svcr_ref) = &self.acct_svcr_ref { + if let Some(acct_svcr_ref) = &self.sref { if prepend { f.write_char(' ')?; } @@ -185,14 +205,21 @@ impl std::fmt::Display for OutgoingId { } } +impl std::fmt::Debug for OutId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /// ID for outgoing batches +#[derive(Clone, PartialEq, Eq)] pub struct BatchId { /// Unique msg ID generated by libeufin-nexus /// ISO20022 MessageId pub msg_id: CompactString, /// Unique end-to-end ID generated by the bank /// ISO20022 AcctSvcrRef - pub acct_svcr_ref: Option<CompactString>, + pub sref: Option<CompactString>, } impl BatchId { @@ -205,7 +232,7 @@ impl std::fmt::Display for BatchId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("(msg=")?; f.write_str(&self.msg_id)?; - if let Some(acct_svcr_ref) = &self.acct_svcr_ref { + if let Some(acct_svcr_ref) = &self.sref { f.write_str("ref=")?; f.write_str(acct_svcr_ref)?; } @@ -214,18 +241,24 @@ impl std::fmt::Display for BatchId { } } +impl std::fmt::Debug for BatchId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /// ISO20022 incoming payment -#[derive(Debug, Clone)] -pub struct IncomingPayment { - pub id: IncomingId, +#[derive(Clone, PartialEq, Eq)] +pub struct InTx { + pub id: InId, pub amount: Amount, - pub credit_fee: Option<Amount>, - pub subject: Option<String>, + pub credit_fee: Amount, + pub subject: Option<Box<str>>, pub execution_time: Timestamp, pub debtor: Option<PaytoURI>, } -impl IncomingPayment { +impl InTx { pub fn with_execution_time(self, execution_time: Timestamp) -> Self { Self { execution_time, @@ -234,7 +267,7 @@ impl IncomingPayment { } } -impl Display for IncomingPayment { +impl Display for InTx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { id, @@ -245,7 +278,7 @@ impl Display for IncomingPayment { debtor, } = self; write!(f, "IN {execution_time} {amount}")?; - if let Some(credit_fee) = credit_fee { + if !credit_fee.is_zero() { write!(f, "-{credit_fee}")?; } write!(f, " {id}")?; @@ -259,17 +292,24 @@ impl Display for IncomingPayment { } } +impl std::fmt::Debug for InTx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /// ISO20022 outgoing payment -pub struct OutgoingPayment { - pub id: OutgoingId, +#[derive(Clone, PartialEq, Eq)] +pub struct OutTx { + pub id: OutId, pub amount: Amount, - pub debit_fee: Option<Amount>, - pub subject: Option<String>, + pub debit_fee: Amount, + pub subject: Option<Box<str>>, pub execution_time: Timestamp, pub creditor: Option<PaytoURI>, } -impl OutgoingPayment { +impl OutTx { pub fn with_execution_time(self, execution_time: Timestamp) -> Self { Self { execution_time, @@ -279,8 +319,8 @@ impl OutgoingPayment { pub fn with_e2e_id(self, end_to_end_id: impl Into<CompactString>) -> Self { Self { - id: OutgoingId { - end_to_end_id: Some(end_to_end_id.into()), + id: OutId { + e2e_id: Some(end_to_end_id.into()), ..self.id }, ..self @@ -289,7 +329,7 @@ impl OutgoingPayment { pub fn with_msg_id(self, msg_id: impl Into<CompactString>) -> Self { Self { - id: OutgoingId { + id: OutId { msg_id: Some(msg_id.into()), ..self.id }, @@ -298,7 +338,7 @@ impl OutgoingPayment { } } -impl Display for OutgoingPayment { +impl Display for OutTx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { id, @@ -309,7 +349,7 @@ impl Display for OutgoingPayment { creditor, } = self; write!(f, "OUT {execution_time} {amount}")?; - if let Some(debit_fee) = debit_fee { + if !debit_fee.is_zero() { write!(f, "-{debit_fee}")?; } write!(f, " {id}")?; @@ -323,14 +363,21 @@ impl Display for OutgoingPayment { } } +impl std::fmt::Debug for OutTx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /** ISO20022 outgoing batch */ -pub struct OutgoingBatch { +#[derive(Clone, PartialEq, Eq)] +pub struct OutBatch { /** ISO20022 MessageId */ pub msg_id: CompactString, pub execution_time: Timestamp, } -impl Display for OutgoingBatch { +impl Display for OutBatch { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { msg_id, @@ -341,7 +388,47 @@ impl Display for OutgoingBatch { } } +impl std::fmt::Debug for OutBatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + +/** ISO20022 outgoing reversal */ +#[derive(Clone, PartialEq, Eq)] +pub struct OutReversal { + /** ISO20022 EndToEndId */ + pub e2e_id: CompactString, + /** ISO20022 MessageId */ + pub msg_id: Option<CompactString>, + pub reason: Box<str>, + pub execution_time: Timestamp, +} + +impl Display for OutReversal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + e2e_id, + msg_id, + reason, + execution_time, + } = self; + // TODO fmt date + match msg_id { + Some(msg_id) => write!(f, "BATCH {execution_time} {msg_id}.{e2e_id}: {reason}"), + None => write!(f, "BATCH {execution_time} {e2e_id}: {reason}"), + } + } +} + +impl std::fmt::Debug for OutReversal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + /** Batch of initiated outgoing payment to sent together */ +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PaymentBatch { pub id: u64, pub msg_id: CompactString, @@ -351,6 +438,7 @@ pub struct PaymentBatch { } /** Initiated outgoing transaction */ +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InitiatedPayment { pub id: u64, pub amount: Amount, @@ -360,20 +448,21 @@ pub struct InitiatedPayment { pub end_to_end_id: CompactString, } +#[derive(Clone, PartialEq, Eq)] pub enum Tx { - In(IncomingPayment), - Out(OutgoingPayment), - Batch(OutgoingBatch), - Reversal, + In(InTx), + Out(OutTx), + Batch(OutBatch), + Reversal(OutReversal), } impl Tx { pub fn execution_time(&self) -> &Timestamp { match self { - Tx::In(IncomingPayment { execution_time, .. }) - | Tx::Out(OutgoingPayment { execution_time, .. }) - | Tx::Batch(OutgoingBatch { execution_time, .. }) => execution_time, - Tx::Reversal => todo!(), + Tx::In(InTx { execution_time, .. }) + | Tx::Out(OutTx { execution_time, .. }) + | Tx::Batch(OutBatch { execution_time, .. }) + | Tx::Reversal(OutReversal { execution_time, .. }) => execution_time, } } } @@ -384,7 +473,13 @@ impl Display for Tx { Tx::In(incoming_payment) => incoming_payment.fmt(f), Tx::Out(outgoing_payment) => outgoing_payment.fmt(f), Tx::Batch(outgoing_batch) => outgoing_batch.fmt(f), - Tx::Reversal => todo!(), + Tx::Reversal(outgoing_reversal) => outgoing_reversal.fmt(f), } } } + +impl std::fmt::Debug for Tx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} diff --git a/src/worker.rs b/src/worker.rs @@ -35,14 +35,14 @@ use crate::{ register_in_talerable, register_out_tx, }, }, - model::{IncomingPayment, OutgoingBatch, OutgoingPayment, Tx}, + model::{InTx, OutBatch, OutTx, Tx}, rand_ebics_id, }; pub async fn register_incoming( db: &PgPool, cfg: &NexusIngestConfig, - payment: &IncomingPayment, + payment: &InTx, ) -> sqlx::Result<()> { let log_res = |res: InResult, kind: &str, suffix: &str| { let fmt = std::fmt::from_fn(|f| { @@ -85,11 +85,9 @@ pub async fn register_incoming( let res = register_in(db, payment).await?; log_res(res, "", &format!("ignored bounce: {cause}")); } else { - let mut bounce_amount = payment.amount.clone(); - if let Some(credit_fee) = &payment.credit_fee - && cfg.bounce_deduce_fee - { - if let Some(res) = bounce_amount.try_sub(credit_fee) { + let mut bounce_amount = payment.amount; + if !payment.credit_fee.is_zero() && cfg.bounce_deduce_fee { + if let Some(res) = bounce_amount.try_sub(&payment.credit_fee) { bounce_amount = res } else { let res = register_in(db, payment).await?; @@ -192,7 +190,7 @@ pub async fn register_incoming( pub async fn register_outgoing( db: &PgPool, - payment: &OutgoingPayment, + payment: &OutTx, ) -> sqlx::Result<OutgoingRegistrationResult> { let metadata = payment .subject @@ -214,7 +212,7 @@ pub async fn register_outgoing( pub async fn register_outgoing_batch( db: &PgPool, currency: &Currency, - batch: &OutgoingBatch, + batch: &OutBatch, ) -> sqlx::Result<()> { info!("{batch}"); let txs = unsettled_tx_in_batch(db, currency, &batch.msg_id, &batch.execution_time).await?; @@ -238,7 +236,7 @@ pub async fn register_tx(db: &PgPool, cfg: &NexusIngestConfig, tx: &Tx) -> sqlx: Tx::Batch(batch) => { register_outgoing_batch(db, &cfg.currency, batch).await?; } - Tx::Reversal => todo!(), + Tx::Reversal(_) => todo!(), } } Ok(()) diff --git a/src/xml.rs b/src/xml.rs @@ -17,10 +17,10 @@ * <http://www.gnu.org/licenses/> */ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use base64::{Engine, prelude::BASE64_STANDARD}; -use roxmltree::Document; +use roxmltree::{Document, Node}; #[macro_export] macro_rules! xml_el { @@ -139,97 +139,237 @@ impl std::fmt::Write for XmlWriter { } } -pub struct XmlReader<'node, 'input> { - node: roxmltree::Node<'node, 'input>, +#[derive(Debug)] +pub enum XmlError { + Xml(roxmltree::Error), + Root(Box<str>, Box<str>), + Parent(Box<str>), + MissingEl(Box<str>), + MissingAttr(Box<str>, Box<str>), + Duplicate(Box<str>, usize), + Parse(Box<str>, Box<str>), } -impl<'node, 'input> XmlReader<'node, 'input> { - pub fn parse<F, R>(raw: &str, tag: &str, f: F) -> R +impl Display for XmlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Xml(e) => e.fmt(f), + Self::Root(expected, got) => write!(f, "expected root '{expected}' got '{got}'"), + Self::Parent(path) => write!(f, "not parent for element '{path}'"), + Self::MissingEl(path) => write!(f, "missing element '{path}'"), + Self::MissingAttr(path, name) => write!(f, "missing attribute '{name}' on <{path}>"), + Self::Duplicate(path, nb) => write!(f, "expected one '{path}', got {nb}"), + Self::Parse(path, err) => write!(f, "malformed '{path}': {err}"), + } + } +} + +impl std::error::Error for XmlError {} + +pub type Result<T> = std::result::Result<T, XmlError>; + +#[derive(Debug, Clone, Copy)] +pub struct Xml<'xml> { + pub node: Node<'xml, 'xml>, +} + +impl<'xml> Xml<'xml> { + pub fn parse_str<F, R>(raw: &str, tag: &str, f: F) -> Result<R> where R: 'static, - F: for<'local> FnOnce(XmlReader<'local, '_>) -> R, + F: for<'local> FnOnce(Xml<'local>) -> Result<R>, { - let xml = Document::parse(raw).unwrap(); + let xml = Document::parse(raw).map_err(XmlError::Xml)?; Self::parse_doc(xml, tag, f) } - pub fn parse_doc<F, R>(xml: Document, tag: &str, f: F) -> R + pub fn parse_doc<F, R>(xml: Document, tag: &str, f: F) -> Result<R> where R: 'static, - F: for<'local> FnOnce(XmlReader<'local, '_>) -> R, + F: for<'local> FnOnce(Xml<'local>) -> Result<R>, { let root = xml.root_element(); - assert!( - root.has_tag_name(tag), - "{} != {tag}", - root.tag_name().name() - ); - let node = XmlReader { node: root }; + if !root.has_tag_name(tag) { + return Err(XmlError::Root(tag.into(), root.tag_name().name().into())); + } + let node = Xml { node: root }; let res = f(node); drop(xml); res } - pub fn attr(&self, name: &str) -> &str { - self.node.attribute(name).unwrap() - } - - pub fn one(&self, tag: &str) -> XmlReader<'node, 'input> { - let mut iter = self - .node - .children() - .filter(|children| children.has_tag_name(tag)); - let Some(node) = iter.next() else { - panic!( - "expected unique '{}.{tag}', got none", - self.node.tag_name().name() - ); - }; - if iter.next().is_some() { - let count = iter.count() + 2; - panic!( - "expected unique '{}.{tag}', got {count}", - self.node.tag_name().name() - ); + fn path(self, tag: Option<&str>) -> Box<str> { + let mut ancestors = Vec::new(); + let mut cur = Some(self.node); + while let Some(n) = cur { + if n.is_element() { + ancestors.push(n); + } + cur = n.parent(); + } + let mut buf = String::new(); + for n in ancestors.into_iter().rev() { + // Add prefix if it exists + if let Some(prefix) = n.tag_name().namespace().and_then(|ns| n.lookup_prefix(ns)) { + buf.push_str(prefix); + buf.push(':'); + } + + buf.push_str(n.tag_name().name()); + buf.push('.'); } - XmlReader { node } - } - - pub fn opt(&self, tag: &str) -> Option<XmlReader<'node, 'input>> { - let mut iter = self - .node - .children() - .filter(|children| children.has_tag_name(tag)); - let node = iter.next()?; - if iter.next().is_some() { - let count = iter.count() + 2; - panic!( - "expected optional '{}.{tag}', got {count}", - self.node.tag_name().name() - ); + match tag { + Some(t) => buf.push_str(t), + None => { + buf.pop(); + } } - Some(XmlReader { node }) + buf.into() + } + + pub fn parse_err(self, err: impl Display) -> XmlError { + XmlError::Parse(self.path(None), err.to_string().into_boxed_str()) } - pub fn one_signed(&self, tag: &str) -> XmlReader<'node, 'input> { - let one = self.one(tag); - assert_eq!(one.node.attribute("authenticate"), Some("true")); - one + pub fn parent(self) -> Result<Xml<'xml>> { + Ok(Self { + node: self + .node + .parent() + .ok_or_else(|| XmlError::Parent(self.path(None)))?, + }) } - pub fn each(&self, tag: &str) -> impl Iterator<Item = XmlReader<'node, 'input>> { - self.node - .children() - .filter_map(move |node| node.has_tag_name(tag).then_some(XmlReader { node })) + fn children(self, tag: &str, signed: bool) -> impl Iterator<Item = Node<'xml, 'xml>> { + self.node.children().filter(move |n| { + n.has_tag_name(tag) && (!signed || n.attribute("authenticate") == Some("true")) + }) } - pub fn text(&self) -> &str { + fn opt_inner(self, tag: &str, signed: bool) -> Result<Option<Xml<'xml>>> { + let mut iter = self.children(tag, signed); + match (iter.next(), iter.next()) { + (None, _) => Ok(None), + (Some(_), Some(_)) => Err(XmlError::Duplicate(self.path(Some(tag)), iter.count() + 2)), + (Some(node), None) => Ok(Some(Xml { node })), + } + } + + fn one_inner(self, tag: &str, signed: bool) -> Result<Xml<'xml>> { + self.opt_inner(tag, signed) + .transpose() + .unwrap_or_else(|| Err(XmlError::MissingEl(self.path(Some(tag))))) + } + + pub fn many(self, tag: &str) -> impl Iterator<Item = Xml<'xml>> { + self.children(tag, false).map(|node| Xml { node }) + } + + pub fn text(self) -> &'xml str { self.node.text().unwrap_or_default() } +} - pub fn b64(&self) -> Vec<u8> { - let encoded = self.text(); - BASE64_STANDARD.decode(encoded).unwrap() +pub trait XmlAccess<'xml>: Sized { + type TryOut<T>; + type OptOut<T>; + + fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::TryOut<T>>; + fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) + -> Result<Self::OptOut<T>>; + + fn one(self, tag: &'xml str) -> Result<Self::TryOut<Xml<'xml>>> { + self.lift(|n| n.one_inner(tag, false)) + } + + fn one_signed(self, tag: &'xml str) -> Result<Self::TryOut<Xml<'xml>>> { + self.lift(|n| n.one_inner(tag, true)) + } + + fn opt(self, tag: &'xml str) -> Result<Self::OptOut<Xml<'xml>>> { + self.opt_lift(|n| n.opt_inner(tag, false)) + } + + fn opt_signed(self, tag: &'xml str) -> Result<Self::OptOut<Xml<'xml>>> { + self.opt_lift(|n| n.opt_inner(tag, true)) + } + + fn attr(self, name: &str) -> Result<Self::TryOut<&'xml str>> { + self.lift(|n| { + n.node + .attribute(name) + .ok_or_else(|| XmlError::MissingAttr(n.path(None), name.into())) + }) + } + + fn decode<T, E: Display>( + self, + lambda: impl FnOnce(&str) -> std::result::Result<T, E>, + ) -> Result<Self::TryOut<T>> { + // TODO error not a node text ? + self.lift(|n| lambda(n.text()).map_err(|e| n.parse_err(e))) + } + + fn parse<T: FromStr>(self) -> Result<Self::TryOut<T>> + where + T::Err: Display, + { + self.decode(T::from_str) + } + + fn b64(self) -> Result<Self::TryOut<Vec<u8>>> { + self.decode(|it| BASE64_STANDARD.decode(it)) + } +} + +impl<'xml> XmlAccess<'xml> for Xml<'xml> { + type TryOut<T> = T; + type OptOut<T> = Option<T>; + + fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::TryOut<T>> { + f(self) + } + + fn opt_lift<T>( + self, + f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>, + ) -> Result<Self::OptOut<T>> { + self.lift(f) + } +} + +impl<'xml> XmlAccess<'xml> for Result<Xml<'xml>> { + type TryOut<T> = T; + type OptOut<T> = Option<T>; + + fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::TryOut<T>> { + self?.lift(f) + } + + fn opt_lift<T>( + self, + f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>, + ) -> Result<Self::OptOut<T>> { + self.lift(f) + } +} + +impl<'xml> XmlAccess<'xml> for Result<Option<Xml<'xml>>> { + type TryOut<T> = Option<T>; + type OptOut<T> = Option<T>; + + fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::TryOut<T>> { + self?.map(|it| it.lift(f)).transpose() + } + + fn opt_lift<T>( + self, + f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>, + ) -> Result<Self::OptOut<T>> { + match self? { + Some(xml) => xml.opt_lift(f), + None => Ok(None), + } } } diff --git a/testbench/sample/postfinance/camt054_P_CH2909000000250094239_1111111119_0_2022030911011199_v2019.xml b/testbench/sample/postfinance/camt054_P_CH2909000000250094239_1111111119_0_2022030911011199_v2019.xml @@ -59,7 +59,7 @@ <AcctSvcrRef>2000000000000000</AcctSvcrRef> <InstrId>1006265-25bbb3b1a</InstrId> <EndToEndId>NOTPROVIDED</EndToEndId> - <UETR>b009c997-97b3-4a9c-803c-d645a7276b0</UETR> + <UETR>b009c997-97b3-4a9c-803c-d645a7276b00</UETR> <Prtry> <Tp>00</Tp> <Ref>00000000000000000000020</Ref>