commit 50720217452d49dd88ae5992bc4a08f0732cd69f
parent 81b9419c3a46a9e5f8a1d7ce3982ac0e1bb478d1
Author: Antoine A <>
Date: Fri, 24 Apr 2026 10:37:59 +0200
nexus: add camt parser
Diffstat:
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>