libeufin

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

commit 20dc138f25b2790edc07e4902791615d3cff4413
parent 21aee9fc8810296ccf5cbebb8d28e11030fa7d5d
Author: Antoine A <>
Date:   Wed, 17 Jun 2026 12:57:53 +0200

nexus: update to match kotlin behavior with QR IBAN

Diffstat:
MCargo.lock | 12++++++------
Mlibeufin-nexus/src/api.rs | 34+++++++++++++++++++++++-----------
Mlibeufin-nexus/src/config.rs | 3+++
Mlibeufin-nexus/src/db/payment.rs | 12++++++------
Mlibeufin-nexus/src/db/transfer.rs | 2+-
Mlibeufin-nexus/src/lib.rs | 12++++++++++--
Mlibeufin-nexus/src/test.rs | 2+-
Mlibeufin-nexus/src/testing.rs | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtestbench/conf/integration.conf | 4++--
9 files changed, 119 insertions(+), 42 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "http-client" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" dependencies = [ "compact_str", "futures-util", @@ -3463,7 +3463,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" dependencies = [ "aws-lc-rs", "axum", @@ -3490,12 +3490,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" dependencies = [ "anyhow", "aws-lc-rs", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" dependencies = [ "proc-macro2", "quote", @@ -3534,7 +3534,7 @@ dependencies = [ [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#9db73672548899f1626c9c8ab854f4b0b26a02bc" +source = "git+git://git.taler.net/taler-rust.git/#20d95dafe4abacee0dc9f1ec1400958de632ef88" dependencies = [ "aws-lc-rs", "axum", diff --git a/libeufin-nexus/src/api.rs b/libeufin-nexus/src/api.rs @@ -50,6 +50,7 @@ use taler_common::{ error_code::ErrorCode, types::{ amount::{Amount, Currency}, + iban::IBAN, payto::{FullIbanPayto, IbanPayto, PaytoURI}, timestamp::TalerTimestamp, }, @@ -70,19 +71,26 @@ pub struct NexusApi { pub pool: sqlx::PgPool, pub currency: Currency, pub payto: FullIbanPayto, + pub qr_iban: Option<IBAN>, pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub taler_out_channel: Sender<i64>, } impl NexusApi { - pub async fn start(pool: sqlx::PgPool, payto: FullIbanPayto, currency: Currency) -> Self { + pub async fn start( + pool: sqlx::PgPool, + currency: Currency, + payto: FullIbanPayto, + qr_iban: Option<IBAN>, + ) -> Self { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let taler_out_channel = Sender::new(0); let tmp = Self { pool: pool.clone(), payto, + qr_iban, currency, in_channel: in_channel.clone(), taler_in_channel: taler_in_channel.clone(), @@ -261,10 +269,13 @@ impl PreparedTransfer for NexusApi { async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { let creditor = IbanPayto::try_from(&req.credit_account)?; - if *creditor != *self.payto { + let reference_number = if creditor.iban == self.payto.iban { + None + } else if Some(creditor.iban) == self.qr_iban { + Some(subject_fmt_qr_bill(req.authorization_pub.as_ref())) + } else { return Err(failure_code(ErrorCode::BANK_UNKNOWN_CREDITOR)); - } - let reference_number = subject_fmt_qr_bill(req.authorization_pub.as_ref()); + }; match transfer_register( &self.pool, req.r#type.into(), @@ -272,19 +283,20 @@ impl PreparedTransfer for NexusApi { &req.authorization_pub, &req.authorization_sig, req.recurrent, - &reference_number, + reference_number.as_deref(), &Timestamp::now(), ) .await? { RegistrationResult::Success => ApiResult::Ok(RegistrationResponse { - subjects: vec![ + subjects: vec![if let Some(qr_reference_number) = reference_number { TransferSubject::QrBill { credit_amount: req.credit_amount, - qr_reference_number: reference_number, - }, - simple_subject(req), - ], + qr_reference_number, + } + } else { + simple_subject(req) + }], expiration: TalerTimestamp::Never, }), RegistrationResult::ReservePubReuse => { @@ -332,7 +344,7 @@ pub mod test { pub async fn api_setup() -> (Router, PgPool) { let (_, pool) = db_test_setup(CONFIG_SOURCE).await; - let api = Arc::new(NexusApi::start(pool.clone(), EXCHANGE.clone(), CURR).await); + let api = Arc::new(NexusApi::start(pool.clone(), CURR, EXCHANGE.clone(), None).await); let server = Router::new() .wire_gateway(api.clone(), AuthMethod::None) .prepared_transfer(api.clone()) diff --git a/libeufin-nexus/src/config.rs b/libeufin-nexus/src/config.rs @@ -38,6 +38,7 @@ use taler_common::{ config::{Config, ValueErr}, types::{ amount::{Amount, Currency}, + iban::IBAN, payto::{BankID, FullIbanPayto}, utils::date_to_utc_ts, }, @@ -188,6 +189,7 @@ impl NexusSubmitCfg { pub struct NexusEbicsConfig { pub account: FullIbanPayto, + pub qr_iban: Option<IBAN>, pub dialect: Dialect, } @@ -203,6 +205,7 @@ impl NexusEbicsConfig { &s.str("name").require()?, ), dialect: s.parse("bank dialect", "bank_dialect").require()?, + qr_iban: s.parse("IBAN", "qr_iban").opt()?, }) } } diff --git a/libeufin-nexus/src/db/payment.rs b/libeufin-nexus/src/db/payment.rs @@ -732,7 +732,7 @@ mod test { &auth_pub, &auth_sig, false, - &reference_number, + None, &Timestamp::now() ) .await @@ -780,7 +780,7 @@ mod test { &auth_pub, &auth_sig, true, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await @@ -818,7 +818,7 @@ mod test { &auth_pub, &auth_sig, true, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await @@ -856,7 +856,7 @@ mod test { &auth_pub, &auth_sig, false, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await @@ -904,7 +904,7 @@ mod test { &auth_pub, &auth_sig, true, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await @@ -942,7 +942,7 @@ mod test { &auth_pub, &auth_sig, true, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await diff --git a/libeufin-nexus/src/db/transfer.rs b/libeufin-nexus/src/db/transfer.rs @@ -39,7 +39,7 @@ pub async fn transfer_register( auth_pub: &EddsaPublicKey, auth_sig: &EddsaSignature, recurrent: bool, - reference_number: &str, + reference_number: Option<&str>, timestamp: &Timestamp, ) -> sqlx::Result<RegistrationResult> { serialized!( diff --git a/libeufin-nexus/src/lib.rs b/libeufin-nexus/src/lib.rs @@ -562,8 +562,16 @@ pub async fn run(cfg: &Config, cmd: Cmd) -> anyhow::Result<()> { } } else { let pool = pool(&cfg.db_cfg).await?; - let payto = &cfg.ebics()?.account; - let api = Arc::new(NexusApi::start(pool, payto.clone(), cfg.currency).await); + let ebics_cfg = cfg.ebics()?; + let api = Arc::new( + NexusApi::start( + pool, + cfg.currency, + ebics_cfg.account.clone(), + ebics_cfg.qr_iban, + ) + .await, + ); let mut server = Router::new(); if let Some(it) = cfg.wire_cfg { server = server diff --git a/libeufin-nexus/src/test.rs b/libeufin-nexus/src/test.rs @@ -131,7 +131,7 @@ async fn prepare(db: &PgPool) -> String { &key, &sig, false, - &reference_number, + Some(&reference_number), &Timestamp::now() ) .await diff --git a/libeufin-nexus/src/testing.rs b/libeufin-nexus/src/testing.rs @@ -34,7 +34,7 @@ use taler_common::{ types::{ amount::Amount, iban::{Country, IBAN}, - payto::TransferIbanPayto, + payto::{BankID, ParsedPayto, PaytoImpl}, }, }; use tracing::debug; @@ -81,8 +81,20 @@ pub enum TestingCmd { #[arg(long)] subject: Option<CompactString>, + /// The payment reference, credit-payto 'ch-qrr' parameter takes precedence + #[arg(long)] + ch_qrr: Option<CompactString>, + + /// The credited account IBAN payto URI + #[arg(long)] + credit_payto: Option<ParsedPayto<BankID>>, + /// The debited account IBAN payto URI - payto: TransferIbanPayto, + #[arg(long)] + debit_payto: Option<ParsedPayto<BankID>>, + + /// The debited account IBAN payto URI + payto: Option<ParsedPayto<BankID>>, }, #[clap(subcommand)] List(ListCmd), @@ -133,20 +145,30 @@ impl TestingCmd { amount, credit_fee, subject, + ch_qrr, + credit_payto, + debit_payto, payto, } => { let cfg = NexusCfg::parse(cfg)?; + let ebics_cfg = cfg.ebics()?; let db = pool(&cfg.db_cfg).await?; - let subject = payto - .subject + let amount = credit_payto .as_ref() - .or(subject.as_ref()) - .ok_or(anyhow!("Missing subject"))?; - let amount = payto - .amount - .as_ref() - .or(amount.as_ref()) + .and_then(|p| p.amount) + .or_else(|| payto.as_ref().and_then(|p| p.amount)) + .or(amount) .ok_or(anyhow!("Missing amount"))?; + let subject = credit_payto + .as_ref() + .and_then(|p| p.subject.clone()) + .or_else(|| payto.as_ref().and_then(|p| p.subject.clone())) + .or_else(|| subject.clone()); + let reference = credit_payto + .as_ref() + .and_then(|p| p.ch_qrr.clone()) + .or_else(|| payto.as_ref().and_then(|p| p.ch_qrr.clone())) + .or_else(|| ch_qrr.clone()); if cfg.currency != amount.currency { bail!( @@ -155,16 +177,48 @@ impl TestingCmd { amount.currency ); } + + if let Some(ref credit_payto) = credit_payto { + if reference.is_some() { + if let Some(qr_iban) = ebics_cfg.qr_iban { + if credit_payto.iban != qr_iban { + bail!( + "Creditor must be the exchange QRR account expected {qr_iban} got {}", + credit_payto.iban + ) + } + } else { + bail!("QR reference is not supported without a configured QR IBAN") + } + } else { + if credit_payto.iban != ebics_cfg.account.iban { + bail!( + "Creditor must be the exchange expected {} got {}", + ebics_cfg.account.iban, + credit_payto.iban + ) + } + } + } + register_incoming( &db, &cfg.ingest()?, &InTx { id: InId::new(None, Some(rand_ebics_id()), None), - amount: *amount, + amount, credit_fee: credit_fee.unwrap_or(Amount::zero(&cfg.currency)), - subject: Some(subject.clone().into_string()), + subject: reference.or(subject).map(|it| it.into_string()), execution_time: Timestamp::now(), - debtor: Some(payto.as_uri()), + debtor: Some( + debit_payto + .map(|it| it.as_uri()) + .or(payto.map(|it| it.as_uri())) + .unwrap_or_else(|| { + IBAN::random(ebics_cfg.account.iban.country()) + .as_full_uri("Testing Account") + }), + ), }, ) .await?; diff --git a/testbench/conf/integration.conf b/testbench/conf/integration.conf @@ -24,8 +24,8 @@ USER_ID = myuser PARTNER_ID = myorg # Account information -IBAN = myiban -BIC = mybic +IBAN = CH7789144474425692816 +BIC = SANDBOXX NAME = myname [libeufin-bankdb-postgres]