commit ef707257df28e301cc91c74cef54e8a4dc698113 parent 5ede102532d7995f8e953ab18c7941ef69348780 Author: Antoine A <> Date: Mon, 7 Sep 2026 15:55:44 +0200 nexus: migrate registration test Diffstat:
24 files changed, 1402 insertions(+), 219 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock @@ -476,7 +476,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -610,6 +610,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -653,18 +659,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -895,7 +901,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -936,11 +942,17 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "2a7a45518d2863d18aa47f4a0cf9faec2aa4304cc09df5e41299f276b3ad135e" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -1123,7 +1135,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1349,7 +1361,7 @@ dependencies = [ [[package]] name = "http-client" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" dependencies = [ "compact_str", "futures-util", @@ -1597,9 +1609,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.1" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1616,9 +1628,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is_terminal_polyfill" @@ -1738,9 +1750,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -1882,8 +1894,10 @@ dependencies = [ "clap", "compact_str", "const_format", + "indexmap", "jiff", "libeufin-ebics", + "pretty_assertions", "regex", "serde_json", "shlex", @@ -1924,7 +1938,7 @@ dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.9.3", + "redox_syscall 0.9.4", ] [[package]] @@ -2070,6 +2084,34 @@ dependencies = [ ] [[package]] +name = "multiversion" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "target-features", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] name = "nix" version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2307,9 +2349,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] @@ -2555,9 +2597,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" dependencies = [ "bitflags 2.13.1", ] @@ -2749,9 +2791,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "aws-lc-rs", "log", @@ -2915,7 +2957,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3398,9 +3440,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -3451,7 +3493,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" dependencies = [ "aws-lc-rs", "axum", @@ -3479,12 +3521,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" dependencies = [ "anyhow", "aws-lc-rs", @@ -3514,17 +3556,17 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" +source = "git+git://git.taler.net/taler-rust.git/#18ef68372e0ee690dd625bb99dc277bc12e1894c" dependencies = [ "aws-lc-rs", "axum", @@ -3550,6 +3592,12 @@ dependencies = [ ] [[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + +[[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3607,7 +3655,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3698,7 +3746,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4105,9 +4153,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -4118,9 +4166,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -4128,9 +4176,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4138,31 +4186,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -4619,7 +4667,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs @@ -930,7 +930,7 @@ pub mod test { for challenge in challenges { let info = if challenge.tan_info == "REDACTED" { - "+456" + "+4567" } else { &challenge.tan_info }; diff --git a/libeufin-bank/src/api/account.rs b/libeufin-bank/src/api/account.rs @@ -1257,7 +1257,7 @@ pub mod test { "is_public": true, "is_taler_exchange": true, "contact_data": { - "phone": "+456", + "phone": "+4567", "email": "test@gmail.com" }, "cashout_payto_uri": payto, @@ -1305,7 +1305,7 @@ pub mod test { }, cashout_payto_uri: Some(payto.as_full_uri("John Smith")), contact_data: ChallengeContactData { - phone: Maybe::Some("+456".into()), + phone: Maybe::Some("+4567".into()), email: Maybe::Some("test@gmail.com".into()) }, conversion_rate: Some(acc.conversion_rate.clone().unwrap()), diff --git a/libeufin-bank/src/api/withdrawal.rs b/libeufin-bank/src/api/withdrawal.rs @@ -20,7 +20,11 @@ use std::sync::Arc; use axum::{ - Json, Router, extract::State, http::StatusCode, response::{IntoResponse, NoContent}, routing::{get, post}, + Json, Router, + extract::State, + http::StatusCode, + response::{IntoResponse, NoContent}, + routing::{get, post}, }; use compact_str::CompactString; use jiff::Timestamp; @@ -379,7 +383,8 @@ pub fn withdrawal_api() -> Router<Arc<BankState>> { Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) } SelectionResult::UnknownAccount => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT).with_status(StatusCode::CONFLICT)) + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT) + .with_status(StatusCode::CONFLICT)) } SelectionResult::AccountIsNotExchange => { Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) diff --git a/libeufin-ebics/src/iso20022/camt.rs b/libeufin-ebics/src/iso20022/camt.rs @@ -20,7 +20,11 @@ use std::{fmt::Write as _, str::FromStr}; use compact_str::CompactString; -use jiff::{Timestamp, civil, tz::TimeZone}; +use jiff::{ + Timestamp, + civil::{self}, + tz::TimeZone, +}; use taler_common::types::{ amount::{Amount, Currency}, iban::IBAN, @@ -539,7 +543,7 @@ pub fn parse_camt(xml: &[u8]) -> xml::Result<Vec<AccountTransactions>> { pub mod test { use std::str::FromStr; - use jiff::{Timestamp, civil::Date}; + use jiff::civil::Date; use taler_common::types::{ amount::{Amount, Currency}, iban::IBAN, @@ -552,10 +556,6 @@ pub mod test { model::{InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, }; - pub fn date_to_timestamp(date: &str) -> Timestamp { - date_to_utc_ts(&Date::from_str(date).unwrap()) - } - fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI { IbanPayto::new(BankID { iban: iban.as_ref().parse().expect("invalid IBAN"), @@ -596,7 +596,7 @@ pub mod test { amount: Amount::from_str(amount).unwrap(), debit_fee: Amount::from_str(debit_fee).unwrap(), subject: subject.map(Into::into), - execution_time: date_to_timestamp(execution_time), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), creditor: creditor.map(|(iban, name)| iban_payto(iban, name)), }) } @@ -618,7 +618,7 @@ pub mod test { amount: Amount::from_str(amount).unwrap(), credit_fee: Amount::from_str(credit_fee).unwrap(), subject: subject.map(Into::into), - execution_time: date_to_timestamp(execution_time), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), debtor: debtor.map(|(iban, name)| iban_payto(iban, name)), }) } @@ -633,14 +633,14 @@ pub mod test { e2e_id: e2e_id.parse().unwrap(), msg_id: msg_id.map(Into::into), reason: reason.into(), - execution_time: date_to_timestamp(execution_time), + 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_timestamp(execution_time), + execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), }) } diff --git a/libeufin-ebics/src/iso20022/hac.rs b/libeufin-ebics/src/iso20022/hac.rs @@ -71,6 +71,7 @@ impl Display for CustomerAck { write!(f, " {}", std::fmt::from_fn(|f| self.msg_fmt(f))) } } + /** Parse HAC pain.002 XML file */ pub fn parse_hac(xml: &[u8]) -> xml::Result<Vec<CustomerAck>> { Xml::parse(xml, "Document", |root| { diff --git a/libeufin-ebics/src/iso20022/model.rs b/libeufin-ebics/src/iso20022/model.rs @@ -112,7 +112,6 @@ impl OutId { 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, diff --git a/libeufin-ebics/src/iso20022/pain001.rs b/libeufin-ebics/src/iso20022/pain001.rs @@ -178,17 +178,16 @@ pub fn create_pain001( #[cfg(test)] mod test { + use jiff::civil::Date; use taler_common::types::{ amount::amount, payto::{BankID, FullIbanPayto}, + utils::date_to_utc_ts, }; use crate::{ dialect::Dialect, - iso20022::{ - camt::test::date_to_timestamp, - pain001::{Pain001Msg, Pain001Tx, create_pain001}, - }, + iso20022::pain001::{Pain001Msg, Pain001Tx, create_pain001}, }; #[test] @@ -203,7 +202,7 @@ mod test { let msg = Pain001Msg { msg_id: "MESSAGE_ID", - timestamp: &date_to_timestamp("2024-09-09"), + timestamp: &date_to_utc_ts(&Date::constant(2024, 09, 09)), debtor: &FullIbanPayto::new( BankID { iban: "CH7789144474425692816".parse().expect("invalid IBAN"), diff --git a/libeufin-nexus/Cargo.toml b/libeufin-nexus/Cargo.toml @@ -25,6 +25,8 @@ sqlx.workspace = true compact_str.workspace = true uuid.workspace = true shlex.workspace = true +pretty_assertions.workspace = true +indexmap = "2" url = "2.5" regex = "1.12" const_format = { version = "0.2", features = ["rust_1_83"] } diff --git a/libeufin-nexus/conf/auth.conf b/libeufin-nexus/conf/auth.conf @@ -8,7 +8,7 @@ IBAN = CH7789144474425692816 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/fetch.conf b/libeufin-nexus/conf/fetch.conf @@ -8,7 +8,7 @@ IBAN = CH7789144474425692816 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/gls.conf b/libeufin-nexus/conf/gls.conf @@ -12,7 +12,7 @@ USER_ID = PFC00563 PARTNER_ID = PFC00563 IBAN = DE84500105177118117964 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/maerki_baumann.conf b/libeufin-nexus/conf/maerki_baumann.conf @@ -8,7 +8,7 @@ USER_ID = PFC00563 PARTNER_ID = PFC00563 IBAN = CH7389144832588726658 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [nexus-fetch] diff --git a/libeufin-nexus/conf/mini.conf b/libeufin-nexus/conf/mini.conf @@ -8,7 +8,7 @@ IBAN = CH7789144474425692816 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/skip.conf b/libeufin-nexus/conf/skip.conf @@ -8,7 +8,7 @@ IBAN = CH7789144474425692816 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/test.conf b/libeufin-nexus/conf/test.conf @@ -9,7 +9,7 @@ QR_IBAN = CH4431999123000889012 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/conf/valiant.conf b/libeufin-nexus/conf/valiant.conf @@ -12,7 +12,7 @@ USER_ID = PFC00563 PARTNER_ID = PFC00563 IBAN = CH7389144832588726658 -BIC = BIC +BIC = AAAABBCC123 NAME = myname [libeufin-nexusdb-postgres] diff --git a/libeufin-nexus/src/config.rs b/libeufin-nexus/src/config.rs @@ -187,6 +187,7 @@ impl NexusSubmitCfg { } } +#[derive(Debug, Clone)] pub struct NexusEbicsConfig { pub account: FullIbanPayto, pub qr_iban: Option<IBAN>, @@ -225,8 +226,8 @@ impl NexusSetupConfig { } } -pub struct NexusCfg<'a> { - pub cfg: &'a Config, +pub struct NexusCfg { + pub cfg: Config, pub currency: Currency, pub account_type: AccountType, pub keys: OnceCell<NexusKeysCfg>, @@ -241,8 +242,8 @@ pub struct NexusCfg<'a> { pub serve_cfg: Serve, } -impl<'a> NexusCfg<'a> { - pub fn parse(cfg: &'a Config) -> Result<Self, ValueErr> { +impl<'a> NexusCfg { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { let s = cfg.section("nexus-ebics"); Ok(Self { currency: s.currency("currency").require()?, @@ -257,7 +258,7 @@ impl<'a> NexusCfg<'a> { ebics: OnceCell::new(), setup: OnceCell::new(), db_cfg: parse_db_cfg(cfg)?, - cfg, + cfg: cfg.clone(), }) } @@ -266,7 +267,7 @@ impl<'a> NexusCfg<'a> { if let Some(keys) = self.keys.get() { return Ok(keys); } - let keys = NexusKeysCfg::parse(self.cfg)?; + let keys = NexusKeysCfg::parse(&self.cfg)?; self.keys.set(keys).ok(); Ok(self.keys.get().unwrap()) } @@ -276,7 +277,7 @@ impl<'a> NexusCfg<'a> { if let Some(host) = self.host.get() { return Ok(host); } - let host = NexusHostCfg::parse(self.cfg)?; + let host = NexusHostCfg::parse(&self.cfg)?; self.host.set(host).ok(); Ok(self.host.get().unwrap()) } @@ -286,7 +287,7 @@ impl<'a> NexusCfg<'a> { if let Some(fetch) = self.fetch.get() { return Ok(fetch); } - let fetch = NexusFetchCfg::parse(self.cfg, &self.currency)?; + let fetch = NexusFetchCfg::parse(&self.cfg, &self.currency)?; self.fetch.set(fetch).ok(); Ok(self.fetch.get().unwrap()) } @@ -296,7 +297,7 @@ impl<'a> NexusCfg<'a> { if let Some(submit) = self.submit.get() { return Ok(submit); } - let submit = NexusSubmitCfg::parse(self.cfg)?; + let submit = NexusSubmitCfg::parse(&self.cfg)?; self.submit.set(submit).ok(); Ok(self.submit.get().unwrap()) } @@ -306,7 +307,7 @@ impl<'a> NexusCfg<'a> { if let Some(ebics) = self.ebics.get() { return Ok(ebics); } - let ebics = NexusEbicsConfig::parse(self.cfg)?; + let ebics = NexusEbicsConfig::parse(&self.cfg)?; self.ebics.set(ebics).ok(); Ok(self.ebics.get().unwrap()) } @@ -316,7 +317,7 @@ impl<'a> NexusCfg<'a> { if let Some(setup) = self.setup.get() { return Ok(setup); } - let setup = NexusSetupConfig::parse(self.cfg)?; + let setup = NexusSetupConfig::parse(&self.cfg)?; self.setup.set(setup).ok(); Ok(self.setup.get().unwrap()) } diff --git a/libeufin-nexus/src/fetch.rs b/libeufin-nexus/src/fetch.rs @@ -72,7 +72,7 @@ use crate::{ pub async fn ebics_fetch( ebics: &EbicsClient<'_>, - cfg: &NexusCfg<'_>, + cfg: &NexusCfg, client: &ClientKeys, bank: &BankKeys, db: &PgPool, @@ -83,108 +83,10 @@ pub async fn ebics_fetch( transient_checkpoint: bool, ) -> anyhow::Result<()> { let ebics_cfg = cfg.ebics()?; - - let register_file = async |doc: &OrderDoc, xml: Vec<u8>| -> anyhow::Result<()> { - match doc { - OrderDoc::acknowledgement => { - for ack in parse_hac(&xml)? { - debug!(target: "fetch", "{ack}"); - if let Some(order_id) = &ack.order_id { - match ack.action { - HacAction::ORDER_HAC_FINAL_POS => { - if let Some(msg_id) = order_success(db, order_id).await? { - info!(target: "fetch", "Batch {msg_id} order {order_id} accepted at {}", ack.timestamp); - } - } - HacAction::ORDER_HAC_FINAL_NEG => { - if let Some((msg_id, msg)) = order_failure(db, order_id).await? { - info!(target: "fetch", "Batch {msg_id} order {order_id} refused at {}{}", ack.timestamp, std::fmt::from_fn( |f| if let Some(msg) = &msg { - write!(f, ": {msg}") - } else { - Ok(()) - })); - } - } - _ => { - order_step(db, order_id, &ack.to_string()).await?; - } - } - } - } - } - OrderDoc::status => { - let msg_status = parse_pain002(&xml)?; - debug!(target: "fetch", "{msg_status}"); - if let Some(code) = msg_status.status { - let msg = msg_status.msg(); - batch_status_update( - db, - &msg_status.id, - match code { - PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => { - SubmissionState::success - } - PaymentGroupStatus::Rejected => { - error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id); - SubmissionState::success - } - _ => SubmissionState::pending, - }, - &msg, - ) - .await?; - } - for p_status in msg_status.payments { - if p_status.id != "NOTPROVIDED" { - warn!(target: "fetch", "Unexpected payment status for {}.{}", msg_status.id, p_status.id); - } else if let Some(code) = p_status.status { - let msg = p_status.msg(); - batch_status_update( - db, - &msg_status.id, - match code { - PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => { - SubmissionState::success - } - PaymentGroupStatus::Rejected => { - error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id); - SubmissionState::success - } - _ => SubmissionState::pending - }, - &msg, - ) - .await?; - } - for tx_status in p_status.txs { - let msg = tx_status.msg(); - tx_status_update( - db, - &tx_status.e2e_id, - &msg_status.id, - match tx_status.status { - PaymentTransactionStatus::Rejected | PaymentTransactionStatus::Blocked => { - error!(target: "fetch", "Transaction {} failed: {msg}", tx_status.e2e_id); - SubmissionState::permanent_failure - } - _ => SubmissionState::pending - }, - &msg, - ) - .await?; - } - } - } - OrderDoc::report | OrderDoc::statement | OrderDoc::notification => { - register_camt(db, cfg, &xml).await?; - } - } - Ok(()) - }; - let register_payload = async |doc: &OrderDoc, content: Vec<u8>| -> anyhow::Result<()> { + let register_payload = async |doc: &OrderDoc, content: &[u8]| -> anyhow::Result<()> { // Unzip payload if necessary match doc { - OrderDoc::acknowledgement => register_file(doc, content).await, + OrderDoc::acknowledgement => register_file(db, cfg, doc, content).await, OrderDoc::status | OrderDoc::report | OrderDoc::statement | OrderDoc::notification => { let mut z = zip::ZipArchive::new(Cursor::new(content))?; for i in 0..z.len() { @@ -192,7 +94,7 @@ pub async fn ebics_fetch( trace!(target: "fetch", "parse {}", file.name()); let mut buf = Vec::new(); file.read_to_end(&mut buf)?; - register_file(doc, buf).await?; + register_file(db, cfg, doc, &buf).await?; } Ok(()) } @@ -221,7 +123,7 @@ pub async fn ebics_fetch( &since.map(|it| (it, Timestamp::now())), transient && peek, async |content| { - register_payload(&doc, content) + register_payload(&doc, &content) .await .map_err(|e| EbicsErrKind::Custom(e.to_string().into())) }, @@ -395,7 +297,110 @@ pub async fn ebics_fetch( Ok(()) } -pub async fn register_camt(db: &PgPool, cfg: &NexusCfg<'_>, xml: &[u8]) -> anyhow::Result<usize> { +async fn register_file( + db: &PgPool, + cfg: &NexusCfg, + doc: &OrderDoc, + xml: &[u8], +) -> anyhow::Result<()> { + match doc { + OrderDoc::acknowledgement => { + for ack in parse_hac(xml)? { + debug!(target: "fetch", "{ack}"); + if let Some(order_id) = &ack.order_id { + match ack.action { + HacAction::ORDER_HAC_FINAL_POS => { + if let Some(msg_id) = order_success(db, order_id).await? { + info!(target: "fetch", "Batch {msg_id} order {order_id} accepted at {}", ack.timestamp); + } + } + HacAction::ORDER_HAC_FINAL_NEG => { + if let Some((msg_id, msg)) = order_failure(db, order_id).await? { + info!(target: "fetch", "Batch {msg_id} order {order_id} refused at {}{}", ack.timestamp, std::fmt::from_fn( |f| if let Some(msg) = &msg { + write!(f, ": {msg}") + } else { + Ok(()) + })); + } + } + _ => { + order_step(db, order_id, &ack.to_string()).await?; + } + } + } + } + } + OrderDoc::status => { + let msg_status = parse_pain002(xml)?; + debug!(target: "fetch", "{msg_status}"); + if let Some(code) = msg_status.status { + let msg = msg_status.msg(); + batch_status_update( + db, + &msg_status.id, + match code { + PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => { + SubmissionState::success + } + PaymentGroupStatus::Rejected => { + error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id); + SubmissionState::success + } + _ => SubmissionState::pending, + }, + &msg, + ) + .await?; + } + for p_status in msg_status.payments { + if p_status.id != "NOTPROVIDED" { + warn!(target: "fetch", "Unexpected payment status for {}.{}", msg_status.id, p_status.id); + } else if let Some(code) = p_status.status { + let msg = p_status.msg(); + batch_status_update( + db, + &msg_status.id, + match code { + PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => { + SubmissionState::success + } + PaymentGroupStatus::Rejected => { + error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id); + SubmissionState::success + } + _ => SubmissionState::pending, + }, + &msg, + ) + .await?; + } + for tx_status in p_status.txs { + let msg = tx_status.msg(); + tx_status_update( + db, + &tx_status.e2e_id, + &msg_status.id, + match tx_status.status { + PaymentTransactionStatus::Rejected | PaymentTransactionStatus::Blocked => { + error!(target: "fetch", "Transaction {} failed: {msg}", tx_status.e2e_id); + SubmissionState::permanent_failure + } + _ => SubmissionState::pending + }, + &msg, + ) + .await?; + } + } + } + OrderDoc::report | OrderDoc::statement | OrderDoc::notification => { + register_camt(db, cfg, xml).await?; + } + } + Ok(()) +} + +pub async fn register_camt(db: &PgPool, cfg: &NexusCfg, xml: &[u8]) -> anyhow::Result<usize> { let account = &cfg.ebics()?.account; let ingest_cfg = cfg.ingest()?; let mut nb_tx = 0; @@ -632,8 +637,1120 @@ pub async fn register_tx(db: &PgPool, cfg: &NexusIngestCfg, tx: &Tx) -> sqlx::Re Tx::Batch(batch) => { register_outgoing_batch(db, &cfg.currency, batch).await?; } - Tx::Reversal(_) => todo!(), + Tx::Reversal(reversal) => { + error!(target: "fetch", "{reversal}"); + tx_status_update( + db, + &reversal.e2e_id, + reversal.msg_id.as_deref().unwrap_or_default(), + SubmissionState::permanent_failure, + &format!("Payment bounces: {}", reversal.reason), + ) + .await?; + } } } Ok(()) } + +#[cfg(test)] +pub mod test { + use std::str::FromStr as _; + + use compact_str::CompactString; + use indexmap::IndexMap; + use jiff::{Timestamp, civil::Date}; + use libeufin_ebics::{ + dialect::Dialect, + ebics::order::OrderDoc, + iso20022::model::{InId, InTx, OutId, OutTx}, + }; + use sqlx::{PgPool, Row as _, postgres::PgRow}; + use taler_api::db::TypeHelper; + use taler_common::{ + config::Config, + types::{ + amount::{Amount, Currency, amount}, + payto::{BankID, IbanPayto, PaytoImpl, PaytoURI}, + utils::date_to_utc_ts, + }, + }; + + use crate::{ + CONFIG_SOURCE, batch_pain001, + config::NexusCfg, + db::{ + initiated::{batch_initiated, batch_sub_success, initiate, initiated_submittable}, + test::db_setup, + }, + fetch::register_file, + gen_init_pay, + model::{ + Initiated, PaymentBatch, + SubmissionState::{self, *}, + }, + }; + + async fn setup() -> (PgPool, NexusCfg) { + setup_cfg("test.conf").await + } + + async fn setup_cfg(name: &str) -> (PgPool, NexusCfg) { + let cfg = Config::load(CONFIG_SOURCE, Some(format!("conf/{name}"))).unwrap(); + let cfg = NexusCfg::parse(&cfg).unwrap(); + let (_, pool) = db_setup().await; + (pool, cfg) + } + + fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI { + IbanPayto::new(BankID { + iban: iban.as_ref().parse().expect("invalid IBAN"), + bic: None, + }) + .as_full_uri(name.as_ref()) + } + + 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)>, + ) -> InTx { + 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_out( + id: (Option<&str>, Option<&str>, Option<&str>), + amount: &str, + debit_fee: &str, + subject: Option<&str>, + execution_time: &str, + creditor: Option<(&str, &str)>, + ) -> OutTx { + 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)), + } + } + + /** Register batches of initiated payments for reconcciliation */ + async fn batches(pool: &PgPool, batches: &[(&str, &[Initiated])]) -> Vec<PaymentBatch> { + let mut tmp = Vec::<PaymentBatch>::new(); + for (name, txs) in batches { + for tx in *txs { + initiate( + pool, + &tx.amount, + &tx.subject, + &tx.creditor, + &tx.initiation_time, + &tx.e2e_id, + ) + .await + .unwrap(); + } + batch_initiated(pool, &Timestamp::now(), name, false) + .await + .unwrap(); + for batch in initiated_submittable(pool, &Currency::CHF).await.unwrap() { + batch_sub_success( + pool, + batch.id, + &Timestamp::now(), + &name.replace("BATCH", "ORDER"), + ) + .await + .unwrap(); + tmp.push(batch); + } + } + tmp + } + + /** Register an XML sample into the database */ + async fn register(pool: &PgPool, cfg: &NexusCfg, path: &str, doc: &OrderDoc) { + register_file( + pool, + cfg, + doc, + &std::fs::read(format!("../libeufin-ebics/sample/platform/{path}")).unwrap(), + ) + .await + .unwrap() + } + + /** Check database content */ + async fn check( + db: &PgPool, + status: &[( + &'static str, + SubmissionState, + &[(&'static str, SubmissionState)], + )], + incoming: &[InTx], + outgoing: &[OutTx], + bounced: &[OutTx], + ) { + // Check batch status + let batch_status = sqlx::query_as::<_, (CompactString, SubmissionState)>( + "SELECT message_id, status FROM initiated_outgoing_batches ORDER BY initiated_outgoing_batch_id" + ).fetch_all(db).await.unwrap(); + + assert_eq!( + status + .iter() + .map(|it| (it.0.into(), it.1)) + .collect::<Vec<_>>(), + batch_status + ); + + // Check transactions status + let batch_tx = sqlx::query_as::<_, (CompactString, CompactString, SubmissionState)>( + " + SELECT message_id, end_to_end_id, initiated_outgoing_transactions.status + FROM initiated_outgoing_transactions + JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id) + ORDER BY initiated_outgoing_batch_id, initiated_outgoing_transaction_id + ", + ) + .fetch_all(db) + .await + .unwrap() + .into_iter() + .fold( + IndexMap::<_, Vec<_>>::new(), + |mut outer, (key, value, third)| { + outer.entry(key).or_default().push((value, third)); + outer + }, + ); + pretty_assertions::assert_eq!( + status + .iter() + .map(|it| (it.0, it.2.to_vec())) + .collect::<Vec<_>>(), + batch_tx + .iter() + .map(|it| ( + it.0.as_str(), + it.1.iter() + .map(|it| (it.0.as_str(), it.1)) + .collect::<Vec<_>>() + )) + .collect::<Vec<_>>() + ); + + // Check incoming transactions + let incoming_tx = sqlx::query( + " + SELECT + uetr + ,tx_id + ,acct_svcr_ref + ,amount + ,credit_fee + ,subject + ,execution_time + ,debit_payto + FROM incoming_transactions + ORDER BY incoming_transaction_id + ", + ) + .try_map(|r: PgRow| { + Ok(InTx { + id: InId::new( + r.try_get("uetr")?, + r.try_get("tx_id")?, + r.try_get("acct_svcr_ref")?, + ), + amount: r.try_get_amount("amount", &Currency::CHF)?, + credit_fee: r + .try_get_opt_amount("credit_fee", &Currency::CHF)? + .unwrap_or(Amount::zero(&Currency::CHF)), + subject: r.try_get("subject")?, + execution_time: r.try_get_timestamp("execution_time")?, + debtor: r.try_get_opt_parse("debit_payto")?, + }) + }) + .fetch_all(db) + .await + .unwrap(); + pretty_assertions::assert_eq!(incoming, incoming_tx); + + // Check outgoing transactions + let outgoing_tx = sqlx::query( + " + SELECT end_to_end_id + ,acct_svcr_ref + ,amount + ,debit_fee + ,subject + ,execution_time + ,credit_payto + FROM outgoing_transactions + ORDER BY outgoing_transaction_id + ", + ) + .try_map(|r: PgRow| { + Ok(OutTx { + id: OutId::new( + None, + r.try_get("end_to_end_id")?, + r.try_get("acct_svcr_ref")?, + ), + amount: r.try_get_amount("amount", &Currency::CHF)?, + debit_fee: r + .try_get_opt_amount("debit_fee", &Currency::CHF)? + .unwrap_or(Amount::zero(&Currency::CHF)), + subject: r.try_get("subject")?, + execution_time: r.try_get_timestamp("execution_time")?, + creditor: r.try_get_opt_parse("credit_payto")?, + }) + }) + .fetch_all(db) + .await + .unwrap(); + pretty_assertions::assert_eq!(outgoing, outgoing_tx); + + // Check outgoing transactions + let bounced_tx = sqlx::query( + " + SELECT end_to_end_id + ,amount + ,subject + ,credit_payto + FROM initiated_outgoing_transactions + JOIN bounced_transactions USING (initiated_outgoing_transaction_id) + ", + ) + .try_map(|r: PgRow| { + Ok(OutTx { + id: OutId::new(None, None, None), + amount: r.try_get_amount("amount", &Currency::CHF)?, + debit_fee: Amount::zero(&Currency::CHF), + subject: r.try_get("subject")?, + execution_time: Timestamp::UNIX_EPOCH, + creditor: r.try_get_opt_parse("credit_payto")?, + }) + }) + .fetch_all(db) + .await + .unwrap(); + pretty_assertions::assert_eq!(bounced, bounced_tx) + } + + #[tokio::test] + async fn pain001() { + let (db, cfg) = setup().await; + let mut batches = batches( + &db, + &[( + "MESSAGE_ID", + &[ + gen_init_pay!("TX_FIRST", "Test 42", amount("EUR:42")), + gen_init_pay!("TX_SECOND", "Test 5.11", amount("EUR:5.11")), + gen_init_pay!("TX_THIRD", "Test 0.21", amount("EUR:0.21")), + ], + )], + ) + .await; + let batch = &mut batches[0]; + let mut cfg = (*cfg.ebics().unwrap()).clone(); + for dialect in Dialect::entries { + cfg.dialect = *dialect; + pretty_assertions::assert_eq!( + std::fs::read_to_string(format!( + "../libeufin-ebics/sample/platform/{dialect}_pain001.xml" + )) + .unwrap(), + batch_pain001( + batch, + &cfg, + date_to_utc_ts(&Date::constant(2024, 9, 9)), + false + ) + .unwrap() + ); + } + } + + /** HAC order id test */ + #[tokio::test] + async fn hac() { + let (db, cfg) = setup().await; + batches( + &db, + &[ + ( + "BATCH_SUCCESS", + &[ + gen_init_pay!("BATCH_SUCCESS_0"), + gen_init_pay!("BATCH_SUCCESS_1"), + ], + ), + ( + "BATCH_FAILURE", + &[ + gen_init_pay!("BATCH_FAILURE_0"), + gen_init_pay!("BATCH_FAILURE_1"), + ], + ), + ], + ) + .await; + + register(&db, &cfg, "hac.xml", &OrderDoc::acknowledgement).await; + + check( + &db, + &[ + ( + "BATCH_SUCCESS", + success, + &[("BATCH_SUCCESS_0", pending), ("BATCH_SUCCESS_1", pending)], + ), + ( + "BATCH_FAILURE", + permanent_failure, + &[ + ("BATCH_FAILURE_0", permanent_failure), + ("BATCH_FAILURE_1", permanent_failure), + ], + ), + ], + &[], + &[], + &[], + ) + .await; + } + + /** CreditSuisse dialect test */ + #[tokio::test] + async fn cs() { + let (db, cfg) = setup().await; + batches( + &db, + &[( + "05BD4C5B4A2649B5B08F6EF6A31F197A", + &[ + gen_init_pay!("AQCXNCPWD8PHW5JTN65Y5XTF7R"), + gen_init_pay!("EE9SX76FC5YSC657EK3GMVZ9TC"), + gen_init_pay!("V5B3MXPEWES9VQW1JDRD6VAET4"), + gen_init_pay!("M9NGRCAC1FBX3ENX3XEDEPJ2JW"), + ], + )], + ) + .await; + + register(&db, &cfg, "pain002_part.xml", &OrderDoc::status).await; + + check( + &db, + &[( + "05BD4C5B4A2649B5B08F6EF6A31F197A", + success, + &[ + ("AQCXNCPWD8PHW5JTN65Y5XTF7R", permanent_failure), + ("EE9SX76FC5YSC657EK3GMVZ9TC", permanent_failure), + ("V5B3MXPEWES9VQW1JDRD6VAET4", permanent_failure), + ("M9NGRCAC1FBX3ENX3XEDEPJ2JW", pending), + ], + )], + &[], + &[], + &[], + ) + .await; + } + + /** Valiant dialect test */ + #[tokio::test] + async fn valiant() { + let (db, cfg) = setup_cfg("valiant.conf").await; + batches( + &db, + &[ + ( + "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L", + &[gen_init_pay!("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE")], + ), + ( + "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", + &[ + gen_init_pay!("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"), + gen_init_pay!("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"), + gen_init_pay!("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"), + gen_init_pay!("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"), + ], + ), + ( + "X166701F6RV59LP71RVWVIW9SV2AFZYLG4", + &[gen_init_pay!("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX")], + ), + ( + "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P", + &[gen_init_pay!("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7")], + ), + ( + "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X", + &[ + gen_init_pay!("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"), + gen_init_pay!("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"), + gen_init_pay!("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"), + ], + ), + ], + ) + .await; + + register(&db, &cfg, "valiant_camt052.xml", &OrderDoc::report).await; + + check( + &db, + &[ + ( + "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L", + success, + &[("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE", success)], + ), + ( + "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U", + success, + &[ + ("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8", success), + ("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN", success), + ("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7", success), + ("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28", success), + ], + ), + ( + "X166701F6RV59LP71RVWVIW9SV2AFZYLG4", + success, + &[("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX", late_failure)], + ), + ( + "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P", + success, + &[("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7", success)], + ), + ( + "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X", + success, + &[ + ("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ", success), + ("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y", late_failure), + ("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE", success), + ], + ), + ], + &[ + 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_in( + ( + Some("7b76d488-05d5-44ab-9d77-31d4165ec158"), + Some("00204EQY370"), + Some("ZV20251118/685062/1"), + ), + "CHF:4.55", + "CHF:0", + Some("TEST"), + "2025-11-18", + None, + ), + ], + &[ + tx_out( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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_out( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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_out( + ( + None, + 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( + ( + None, + 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_out( + (None, None, None), + "CHF:0.85", + "CHF:0", + Some("bounce 51030655601.0001: missing reserve public key"), + "1970-01-01", + Some(("CH7389144832588726658", "Grothoff Hans")), + )], + ) + .await; + } + + /** GLS dialect test */ + #[tokio::test] + async fn gls() { + let (db, cfg) = setup_cfg("gls.conf").await; + batches( + &db, + &[ + ("COMPAT_SUCCESS", &[gen_init_pay!("COMPAT_SUCCESS")]), + ("COMPAT_FAILURE", &[gen_init_pay!("COMPAT_FAILURE")]), + ( + "BATCH_SINGLE_SUCCESS", + &[gen_init_pay!("FD622SMXKT5QWSAHDY0H8NYG3G")], + ), + // JEYMR3OYZTFM7505OWWENFPAH53LNOWJHS + ( + "BATCH_SINGLE_FAILURE", + &[gen_init_pay!("DAFC3NEE4T48WVC560T76ABA2C")], + ), + ( + "BATCH_SINGLE_RETURN", + &[gen_init_pay!("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5")], + ), + ( + "BATCH_MANY_SUCCESS", + &[ + gen_init_pay!("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"), + gen_init_pay!("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"), + gen_init_pay!("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"), + gen_init_pay!("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"), + ], + ), + ( + "BATCH_MANY_PART", + &[ + gen_init_pay!("27SK3166EG36SJ7VP7VFYP0MW8"), + gen_init_pay!("KGTDBASWTJ6JM89WXD3Q5KFQC4"), + gen_init_pay!("8XK8Z7RAX224FGWK832FD40GYC"), + ], + ), + ( + "BATCH_MANY_FAILURE", + &[ + gen_init_pay!("4XTPKWE4A9V90PRQJCT8Z3MQZ8"), + gen_init_pay!("3VZZHVYJ6XP2SNPKWF4D4YVHNG"), + ], + ), + ], + ) + .await; + + register(&db, &cfg, "gls_camt052.xml", &OrderDoc::report).await; + register(&db, &cfg, "gls_camt053.xml", &OrderDoc::statement).await; + // TODO camt054 with missing id before and after + + check( + &db, + &[ + ("COMPAT_SUCCESS", success, &[("COMPAT_SUCCESS", success)]), + ( + "COMPAT_FAILURE", + pending, + &[("COMPAT_FAILURE", permanent_failure)], + ), + ( + "BATCH_SINGLE_SUCCESS", + success, + &[("FD622SMXKT5QWSAHDY0H8NYG3G", success)], + ), + ( + "BATCH_SINGLE_FAILURE", // TODO success + pending, + &[("DAFC3NEE4T48WVC560T76ABA2C", pending)], // TODO failure + ), + ( + "BATCH_SINGLE_RETURN", + success, + &[("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", late_failure)], + ), + ( + "BATCH_MANY_SUCCESS", + success, + &[ + ("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V", success), + ("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW", success), + ("35M1268GW5ZFHS5JCB41UKDQNPMD40T849", success), + ("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1", success), + ], + ), + ( + "BATCH_MANY_PART", + success, + &[ + ("27SK3166EG36SJ7VP7VFYP0MW8", success), + ("KGTDBASWTJ6JM89WXD3Q5KFQC4", permanent_failure), + ("8XK8Z7RAX224FGWK832FD40GYC", permanent_failure), + ], + ), + ( + "BATCH_MANY_FAILURE", // TODO success + pending, + &[ + ("4XTPKWE4A9V90PRQJCT8Z3MQZ8", pending), // TODO failure + ("3VZZHVYJ6XP2SNPKWF4D4YVHNG", pending), // TODO failure + ], + ), + ], + &[tx_in( + ( + None, + Some("BYLADEM1WOR-G2910276709458A2"), + Some("2024041210041357000"), + ), + "CHF:3", + "CHF:0", + Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"), + "2024-04-12", + Some(("DE84500105177118117964", "John Smith")), + )], + &[ + tx_out( + (None, Some("COMPAT_SUCCESS"), Some("2024041801514102000")), + "CHF:2", + "CHF:0", + Some("TestABC123"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + tx_out( + ( + None, + Some("FD622SMXKT5QWSAHDY0H8NYG3G"), + Some("2024090216552232000"), + ), + "CHF:1.1", + "CHF:0", + Some("single 2024-09-02T14:29:52.875253314Z"), + "2024-09-02", + Some(("DE89500105173198527518", "Grothoff Hans")), + ), + tx_out( + ( + None, + Some("YF5QBARGQ0MNY0VK59S477VDG4"), + Some("2024041810552821000"), + ), + "CHF:1.1", + "CHF:0", + Some("Simple tx"), + "2024-04-18", + Some(("DE20500105172419259181", "John Smith")), + ), + tx_out( + (None, Some("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"), None), + "CHF:44", + "CHF:0", + Some("init payment"), + "2024-09-20", + Some(("CH4189144589712575493", "Test")), + ), + tx_out( + (None, Some("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"), None), + "CHF:44", + "CHF:0", + Some("init payment"), + "2024-09-20", + Some(("CH4189144589712575493", "Test")), + ), + tx_out( + (None, Some("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"), None), + "CHF:44", + "CHF:0", + Some("init payment"), + "2024-09-20", + Some(("CH4189144589712575493", "Test")), + ), + tx_out( + (None, Some("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"), None), + "CHF:44", + "CHF:0", + Some("init payment"), + "2024-09-20", + Some(("CH4189144589712575493", "Test")), + ), + tx_out( + ( + None, + Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"), + Some("2024092100252498000"), + ), + "CHF:0.42", + "CHF:0", + Some("This should fail because bad iban"), + "2024-09-23", + Some(("DE18500105173385245163", "John Smith")), + ), + tx_out( + (None, Some("27SK3166EG36SJ7VP7VFYP0MW8"), None), + "CHF:44", + "CHF:0", + Some("init payment"), + "2024-09-04", + Some(("CH4189144589712575493", "Test")), + ), + ], + &[], + ) + .await; + } + + /** Maerki Baumann dialect test */ + #[tokio::test] + async fn maerki_baumann() { + let (db, cfg) = setup_cfg("maerki_baumann.conf").await; + batches( + &db, + &[( + "BATCH_SINGLE_REPORTING", + &[ + gen_init_pay!("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"), + gen_init_pay!("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"), + gen_init_pay!("A09R35EW0359SZ51464E7TC37A0P2CBK04"), + gen_init_pay!("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"), + ], + )], + ) + .await; + + register( + &db, + &cfg, + "maerki_baumann_camt053.xml", + &OrderDoc::statement, + ) + .await; + + check( + &db, + &[( + "BATCH_SINGLE_REPORTING", + success, + &[ + ("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H", success), + ("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH", success), + ("A09R35EW0359SZ51464E7TC37A0P2CBK04", success), + ("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT", success), + ], + )], + &[ + 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_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_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")), + ), + ], + &[ + tx_out( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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( + ( + None, + 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_out( + (None, None, Some("GB20241220/205792/1")), + "CHF:3000", + "CHF:0", + None, + "2024-12-20", + None, + ), + ], + &[ + tx_out( + (None, None, None), + "CHF:1", + "CHF:0", + Some("bounce 7371795e-62fa-42dd-93b7-da89cc120faa: missing reserve public key"), + "1970-01-01", + Some(("CH7389144832588726658", "Mr Test")), + ), + tx_out( + (None, None, None), + "CHF:0.5", + "CHF:0", + Some("bounce 50523424675.0001: missing reserve public key"), + "1970-01-01", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + (None, None, None), + "CHF:0.05", + "CHF:0", + Some("bounce f203fbb4-6e13-4c78-9b2a-d852fea6374a: missing reserve public key"), + "1970-01-01", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + (None, None, None), + "CHF:0.1", + "CHF:0", + Some("bounce 81b0d8c6-a677-4577-b75e-a639dcc03681: missing reserve public key"), + "1970-01-01", + Some(("CH7389144832588726658", "Grothoff Hans")), + ), + tx_out( + (None, None, None), + "CHF:1.38", + "CHF:0", + Some("bounce F000787951230001: restricted account"), + "1970-01-01", + Some(("DE20500105172419259181", "Mr German")), + ), + ], + ) + .await; + } +} diff --git a/libeufin-nexus/src/lib.rs b/libeufin-nexus/src/lib.rs @@ -179,11 +179,12 @@ pub struct Args { pub fn batch_pain001( batch: &PaymentBatch, cfg: &NexusEbicsConfig, + now: Timestamp, instant: bool, ) -> Result<String, EbicsErrKind> { let msg = Pain001Msg { msg_id: &batch.msg_id, - timestamp: &Timestamp::now(), + timestamp: &now, debtor: &cfg.account, sum: batch.sum, txs: batch @@ -205,7 +206,7 @@ pub fn batch_pain001( pub async fn ebics_submit( ebics: &EbicsClient<'_>, - cfg: &NexusCfg<'_>, + cfg: &NexusCfg, client: &ClientKeys, bank: &BankKeys, db: &PgPool, @@ -219,7 +220,7 @@ pub async fn ebics_submit( instant: bool| -> Result<CompactString, EbicsError> { let ctx = EbicsCtx::new(order); - let xml = batch_pain001(batch, ebics_cfg, instant).ctx(&ctx)?; + let xml = batch_pain001(batch, ebics_cfg, Timestamp::now(), instant).ctx(&ctx)?; ebics.upload(client, bank, order, xml.as_bytes()).await }; @@ -312,7 +313,7 @@ pub async fn ebics_submit( pub async fn ebics_setup( ebics: &EbicsClient<'_>, - cfg: &NexusCfg<'_>, + cfg: &NexusCfg, db: &PgPool, force_keys_resubmission: bool, generate_registration_pdf: bool, diff --git a/libeufin-nexus/src/manual.rs b/libeufin-nexus/src/manual.rs @@ -68,7 +68,7 @@ pub enum ManualCmd { } impl ManualCmd { - pub async fn run(self, db: &PgPool, cfg: &NexusCfg<'_>) -> anyhow::Result<()> { + pub async fn run(self, db: &PgPool, cfg: &NexusCfg) -> anyhow::Result<()> { match self { ManualCmd::Export { out } => { let out: &mut dyn Write = if out == "-" { @@ -95,7 +95,7 @@ impl ManualCmd { format!("{datetime}-{}.xml", batch.msg_id), FileOptions::DEFAULT, )?; - let xml = batch_pain001(&batch, cfg.ebics()?, false)?; + let xml = batch_pain001(&batch, cfg.ebics()?, Timestamp::now(), false)?; zip.write_all(xml.as_bytes())?; write!(&mut metadata, "\nbatch {}:", batch.msg_id)?; for tx in batch.payments { diff --git a/libeufin-nexus/src/test.rs b/libeufin-nexus/src/test.rs @@ -47,7 +47,7 @@ use crate::{ pub const CURR: Currency = Currency::KUDOS; pub static EXCHANGE: LazyLock<FullIbanPayto> = LazyLock::new(|| { - FullIbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Exchange").unwrap() + FullIbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test").unwrap() }); pub static CLIENT: LazyLock<FullIbanPayto> = LazyLock::new(|| { FullIbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith").unwrap() @@ -71,19 +71,29 @@ pub fn gen_out_pay(subject: impl Into<String>) -> OutTx { } } -/** Generates a payment initiation, given its subject and end-to-end ID */ -pub fn gen_init_pay( - end_to_end_id: impl Into<CompactString>, - subject: impl Into<String>, -) -> Initiated { - Initiated { - id: 0, - amount: Amount::new(&CURR, 44, 0), - creditor: EXCHANGE.clone(), - subject: subject.into(), - initiation_time: Timestamp::now(), - e2e_id: end_to_end_id.into(), - } +/** Generates a payment initiation for testing */ +#[macro_export] +macro_rules! gen_init_pay { + ($end_to_end_id:expr) => { + gen_init_pay!($end_to_end_id, "init payment") + }; + ($end_to_end_id:expr, $subject:expr) => { + gen_init_pay!( + $end_to_end_id, + $subject, + taler_common::types::amount::Amount::new(&$crate::test::CURR, 44, 0) + ) + }; + ($end_to_end_id:expr, $subject:expr, $amount:expr) => { + Initiated { + id: 0, + amount: $amount, + creditor: $crate::test::EXCHANGE.clone(), + subject: $subject.into(), + initiation_time: jiff::Timestamp::now(), + e2e_id: $end_to_end_id.into(), + } + }; } /** Generates an incoming payment, given its subject */ @@ -103,7 +113,7 @@ pub async fn gen_initiate( end_to_end_id: impl Into<CompactString>, subject: impl Into<String>, ) -> PaymentInitiationResult { - let init = gen_init_pay(end_to_end_id, subject); + let init = gen_init_pay!(end_to_end_id, subject); initiate( db, &init.amount, diff --git a/testbench/src/lib.rs b/testbench/src/lib.rs @@ -66,7 +66,7 @@ mod test { let check_cmds = async |msg: &str| { for cmd in nexus_cmds { let tmp: libeufin_nexus::Args = clap_parse(cmd).unwrap(); - let res = libeufin_nexus::run(cfg.cfg, tmp.cmd).await.unwrap_err(); + let res = libeufin_nexus::run(&cfg.cfg, tmp.cmd).await.unwrap_err(); assert_eq!( res.to_string(), msg.replace("SETUPCMD", "libeufin-nexus ebics-setup") @@ -74,7 +74,7 @@ mod test { } for cmd in sync_cmds { let tmp: libeufin_ebisync::Args = clap_parse(cmd).unwrap(); - let res = libeufin_ebisync::run(cfg.cfg, tmp.cmd).await.unwrap_err(); + let res = libeufin_ebisync::run(&cfg.cfg, tmp.cmd).await.unwrap_err(); assert_eq!( res.to_string(), msg.replace("SETUPCMD", "libeufin-ebisync setup") @@ -84,7 +84,7 @@ mod test { let check_all_cmds = async |msg: &str| { for cmd in nexus_all_cmds { let tmp: libeufin_nexus::Args = clap_parse(cmd).unwrap(); - let res = libeufin_nexus::run(cfg.cfg, tmp.cmd).await.unwrap_err(); + let res = libeufin_nexus::run(&cfg.cfg, tmp.cmd).await.unwrap_err(); assert_eq!( res.to_string(), msg.replace("SETUPCMD", "libeufin-nexus ebics-setup") @@ -92,7 +92,7 @@ mod test { } for cmd in sync_all_cmds { let tmp: libeufin_ebisync::Args = clap_parse(cmd).unwrap(); - let res = libeufin_ebisync::run(cfg.cfg, tmp.cmd).await.unwrap_err(); + let res = libeufin_ebisync::run(&cfg.cfg, tmp.cmd).await.unwrap_err(); assert_eq!( res.to_string(), msg.replace("SETUPCMD", "libeufin-ebisync setup") diff --git a/testbench/src/main.rs b/testbench/src/main.rs @@ -261,7 +261,7 @@ async fn main() -> anyhow::Result<()> { }; let payto = TransferIbanPayto::from_str(payto).unwrap(); let ebics_log = format!("--debug-ebics testbench/test/{}", cmd.platform); - let run = async |args: &str| cmd.component.run(cfg.cfg, args).await; + let run = async |args: &str| cmd.component.run(&cfg.cfg, args).await; let auto_setup = async || { let client = load_client_keys(ebics.client.as_ref()).unwrap(); let bank = load_bank_keys(ebics.bank.as_ref()).unwrap();