taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 02ba8844ac5e62515dd6b4bbba900cf27cf4582b
parent 70339a8b15cc3b103fb93048a8e9e99891280836
Author: Antoine A <>
Date:   Tue, 16 Dec 2025 15:40:31 +0100

cyclos: link outgoing transactions to initiated and chargeback

Diffstat:
Mtaler-cyclos/db/cyclos-0001.sql | 12+++++++-----
Mtaler-cyclos/db/cyclos-procedures.sql | 21+++++++++++----------
Mtaler-cyclos/src/bin/cyclos-harness.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Mtaler-cyclos/src/cyclos_api/api.rs | 2+-
Mtaler-cyclos/src/cyclos_api/types.rs | 8++++++++
Mtaler-cyclos/src/db.rs | 175++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtaler-cyclos/src/worker.rs | 17+++++++++--------
Ataler-cyclos/tests/api.rs | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 38+++++++++++++++++++-------------------
9 files changed, 388 insertions(+), 71 deletions(-)

diff --git a/taler-cyclos/db/cyclos-0001.sql b/taler-cyclos/db/cyclos-0001.sql @@ -24,6 +24,7 @@ COMMENT ON TYPE taler_amount IS 'Stores an amount, fraction is in units of 1/100 CREATE TABLE tx_in( tx_in_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY, transfer_id INT8 UNIQUE, + tx_id INT8, amount taler_amount NOT NULL, subject TEXT NOT NULL, debit_account INT8 NOT NULL, @@ -36,6 +37,7 @@ COMMENT ON TABLE tx_in IS 'Incoming transactions'; CREATE TABLE tx_out( tx_out_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY, transfer_id INT8 UNIQUE, + tx_id INT8, amount taler_amount NOT NULL, subject TEXT NOT NULL, credit_account INT8 NOT NULL, @@ -43,7 +45,7 @@ CREATE TABLE tx_out( valued_at INT8 NOT NULL, registered_at INT8 NOT NULL ); -COMMENT ON TABLE tx_in IS 'Outgoing transactions'; +COMMENT ON TABLE tx_out IS 'Outgoing transactions'; CREATE TYPE incoming_type AS ENUM ('reserve' ,'kyc', 'wad'); @@ -61,7 +63,7 @@ CREATE TABLE taler_in( END ) ); -COMMENT ON TABLE tx_in IS 'Incoming talerable transactions'; +COMMENT ON TABLE taler_in IS 'Incoming talerable transactions'; CREATE UNIQUE INDEX taler_in_unique_reserve_pub ON taler_in (metadata) WHERE type = 'reserve'; @@ -70,7 +72,7 @@ CREATE TABLE taler_out( wtid BYTEA NOT NULL UNIQUE CHECK (LENGTH(wtid)=32), exchange_base_url TEXT NOT NULL ); -COMMENT ON TABLE tx_in IS 'Outgoing talerable transactions'; +COMMENT ON TABLE taler_out IS 'Outgoing talerable transactions'; CREATE TYPE transfer_status AS ENUM( 'pending', @@ -95,7 +97,7 @@ CREATE TABLE initiated( tx_out_id INT8 UNIQUE REFERENCES tx_out(tx_out_id) ON DELETE CASCADE, initiated_at INT8 NOT NULL ); -COMMENT ON TABLE tx_in IS 'Initiated outgoing transactions'; +COMMENT ON TABLE initiated IS 'Initiated outgoing transactions'; CREATE TABLE transfer( initiated_id INT8 PRIMARY KEY REFERENCES initiated(initiated_id) ON DELETE CASCADE, @@ -111,7 +113,7 @@ CREATE TABLE bounced( chargeback_id INT8 NOT NULL UNIQUE, reason TEXT NOT NULL ); -COMMENT ON TABLE tx_in IS 'Bounced transactions'; +COMMENT ON TABLE bounced IS 'Bounced transactions'; CREATE TABLE kv( key TEXT NOT NULL UNIQUE PRIMARY KEY, diff --git a/taler-cyclos/db/cyclos-procedures.sql b/taler-cyclos/db/cyclos-procedures.sql @@ -40,6 +40,7 @@ $do$; CREATE FUNCTION register_tx_in( IN in_transfer_id INT8, + IN in_tx_id INT8, IN in_amount taler_amount, IN in_subject TEXT, IN in_debit_account INT8, @@ -80,6 +81,7 @@ END IF; out_valued_at = in_valued_at; INSERT INTO tx_in ( transfer_id, + tx_id, amount, subject, debit_account, @@ -88,6 +90,7 @@ INSERT INTO tx_in ( registered_at ) VALUES ( in_transfer_id, + in_tx_id, in_amount, in_subject, in_debit_account, @@ -117,6 +120,7 @@ COMMENT ON FUNCTION register_tx_in IS 'Register an incoming transaction idempote CREATE FUNCTION register_tx_out( IN in_transfer_id INT8, + IN in_tx_id INT8, IN in_amount taler_amount, IN in_subject TEXT, IN in_credit_account INT8, @@ -144,6 +148,7 @@ END IF; -- Insert new outgoing transaction INSERT INTO tx_out ( transfer_id, + tx_id, amount, subject, credit_account, @@ -152,6 +157,7 @@ INSERT INTO tx_out ( registered_at ) VALUES ( in_transfer_id, + in_tx_id, in_amount, in_subject, in_credit_account, @@ -169,8 +175,8 @@ SET tx_out_id = out_tx_row_id, status = 'success', status_msg = NULL -WHERE tx_id = in_transfer_id; -- This will not work, should we pass the transaction id ? -IF FOUND THEN +WHERE tx_id = in_tx_id; +IF FOUND OR EXISTS(SELECT FROM bounced WHERE chargeback_id = in_transfer_id) THEN out_result = 'known'; ELSE out_result = 'recovered'; @@ -192,13 +198,7 @@ IF in_wtid IS NOT NULL THEN PERFORM pg_notify('taler_out', out_tx_row_id || ''); END IF; ELSIF in_bounced IS NOT NULL THEN - --UPDATE initiated - --SET - -- tx_out_id = out_tx_row_id, - -- status = 'success', - -- status_msg = NULL - --FROM bounced JOIN tx_in USING (tx_in_id) - --WHERE initiated.initiated_id = bounced.initiated_id AND tx_in.transfer_id = in_transfer_id; + -- TODO Reconstruct bounces ? END IF; END $$; COMMENT ON FUNCTION register_tx_out IS 'Register an outgoing transaction idempotently'; @@ -300,6 +300,7 @@ END $$; CREATE FUNCTION register_bounced_tx_in( IN in_transfer_id INT8, + IN in_tx_id INT8, IN in_amount taler_amount, IN in_subject TEXT, IN in_debit_account INT8, @@ -317,7 +318,7 @@ BEGIN -- Register incoming transaction idempotently SELECT register_tx_in.out_tx_row_id, register_tx_in.out_new INTO out_tx_row_id, out_tx_new -FROM register_tx_in(in_transfer_id, in_amount, in_subject, in_debit_account, in_debit_name, in_valued_at, NULL, NULL, in_now); +FROM register_tx_in(in_transfer_id, in_tx_id, in_amount, in_subject, in_debit_account, in_debit_name, in_valued_at, NULL, NULL, in_now); -- Register the bounce INSERT INTO bounced ( diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs @@ -295,6 +295,30 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { harness.worker().await?; balance.expect_sub(amount).await; + step("Test transfer transactions"); + let amount = decimal("3.5"); + // Init a transfer to client + let transfer_id = harness + .custom_transfer( + amount, + &FullCyclosPayto::new(CyclosId(harness.client_id), "Client".to_string()), + ) + .await; + // Check transfer pending + harness + .expect_transfer_status(transfer_id, TransferState::pending, None) + .await; + // Should send + harness.worker().await?; + // Wait for transaction to finalize + balance.expect_sub(amount).await; + // Should register + harness.worker().await?; + // Check transfer is now successful + harness + .expect_transfer_status(transfer_id, TransferState::success, None) + .await; + step("Test transfer to self"); // Init a transfer to self let transfer_id = harness @@ -314,6 +338,25 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { ) .await; + step("Test transfer to unknown account"); + // Init a transfer to self + let transfer_id = harness + .custom_transfer( + decimal("10.1"), + &FullCyclosPayto::new(CyclosId(42), "Unknown".to_string()), + ) + .await; + // Should failed + harness.worker().await?; + // Check transfer failed + harness + .expect_transfer_status( + transfer_id, + TransferState::permanent_failure, + Some("unknown BasicUser 42"), + ) + .await; + step("Test unexpected outgoing"); // Manual tx from the exchange let amount = decimal("4"); @@ -324,6 +367,7 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { // Wait for transaction to finalize balance.expect_sub(amount).await; harness.worker().await?; + step("Finish"); Ok(()) diff --git a/taler-cyclos/src/cyclos_api/api.rs b/taler-cyclos/src/cyclos_api/api.rs @@ -75,7 +75,7 @@ fn parse<'de, T: Deserialize<'de>>(str: &'de str) -> Result<T, ErrKind> { async fn json_body<T: DeserializeOwned>(res: reqwest::Response) -> Result<T, ErrKind> { // TODO check content type? let body = res.text().await?; - //println!("{body}"); + // println!("{body}"); let parsed = parse(&body)?; Ok(parsed) } diff --git a/taler-cyclos/src/cyclos_api/types.rs b/taler-cyclos/src/cyclos_api/types.rs @@ -100,6 +100,7 @@ pub struct HistoryItem { pub ty: Type, pub description: Option<String>, pub related_account: RelatedAccount, + pub transaction: Option<TransactionMini> } #[derive(Debug, Deserialize)] @@ -139,6 +140,13 @@ pub struct Transaction { pub ty: Type, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionMini { + pub id: CyclosId, + pub kind: TxKind, +} + #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "camelCase")] /// Indicates the reason the transfer was created. diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs @@ -41,7 +41,8 @@ use crate::{CyclosId, FullCyclosPayto}; #[derive(Debug, Clone)] pub struct TxIn { - pub id: u64, + pub transfer_id: u64, + pub tx_id: Option<u64>, pub amount: Decimal, pub subject: String, pub debtor: FullCyclosPayto, @@ -51,15 +52,20 @@ pub struct TxIn { impl Display for TxIn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { - id, + transfer_id, + tx_id, amount, subject, debtor, valued_at, } = self; + let tx_id = match tx_id { + Some(id) => format_args!(":{}", *id), + None => format_args!(""), + }; write!( f, - "{valued_at} {id} {amount} ({} {}) '{subject}'", + "{valued_at} {transfer_id}{tx_id} {amount} ({} {}) '{subject}'", debtor.0, debtor.name ) } @@ -67,7 +73,8 @@ impl Display for TxIn { #[derive(Debug, Clone)] pub struct TxOut { - pub id: u64, + pub transfer_id: u64, + pub tx_id: Option<u64>, pub amount: Decimal, pub subject: String, pub creditor: FullCyclosPayto, @@ -77,15 +84,20 @@ pub struct TxOut { impl Display for TxOut { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { - id, + transfer_id, + tx_id, amount, subject, creditor, valued_at, } = self; + let tx_id = match tx_id { + Some(id) => format_args!(":{}", *id), + None => format_args!(""), + }; write!( f, - "{valued_at} {id} {amount} ({} {}) '{subject}'", + "{valued_at} {transfer_id}{tx_id} {amount} ({} {}) '{subject}'", creditor.0, creditor.name ) } @@ -141,7 +153,7 @@ pub async fn register_tx_in_admin( sqlx::query( " SELECT out_reserve_pub_reuse, out_tx_row_id, out_valued_at, out_new - FROM register_tx_in(NULL, ($1, $2)::taler_amount, $3, $4, $5, $6, $7, $8, $6) + FROM register_tx_in(NULL, NULL, ($1, $2)::taler_amount, $3, $4, $5, $6, $7, $8, $6) ", ) .bind_decimal(&tx.amount) @@ -175,10 +187,11 @@ pub async fn register_tx_in( sqlx::query( " SELECT out_reserve_pub_reuse, out_tx_row_id, out_valued_at, out_new - FROM register_tx_in($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9, $10) + FROM register_tx_in($1, $2, ($3, $4)::taler_amount, $5, $6, $7, $8, $9, $10, $11) ", ) - .bind(tx.id as i64) + .bind(tx.transfer_id as i64) + .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) .bind(tx.debtor.0 as i64) @@ -236,10 +249,11 @@ pub async fn register_tx_out( let query = sqlx::query( " SELECT out_result, out_tx_row_id - FROM register_tx_out($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9, $10, $11) + FROM register_tx_out($1, $2, ($3, $4)::taler_amount, $5, $6, $7, $8, $9, $10, $11, $12) ", ) - .bind(tx.id as i64) + .bind(tx.transfer_id as i64) + .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) .bind(tx.creditor.0 as i64) @@ -331,10 +345,11 @@ pub async fn register_bounced_tx_in( sqlx::query( " SELECT out_tx_row_id, out_tx_new - FROM register_bounced_tx_in($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9, $10) + FROM register_bounced_tx_in($1, $2, ($3, $4)::taler_amount, $5, $6, $7, $8, $9, $10, $11) ", ) - .bind(tx.id as i64) + .bind(tx.transfer_id as i64) + .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) .bind(tx.debtor.0 as i64) @@ -750,7 +765,7 @@ mod test { use taler_common::{ api_common::{EddsaPublicKey, HashCode, ShortHashCode}, api_params::{History, Page}, - api_wire::TransferRequest, + api_wire::{TransferRequest, TransferState, TransferStatus}, types::{ amount::{Currency, amount, decimal}, payto::payto, @@ -764,8 +779,8 @@ mod test { constants::CONFIG_SOURCE, cyclos_payto, db::{ - self, AddIncomingResult, AddOutgoingResult, BounceResult, TransferResult, TxIn, - TxInAdmin, TxOut, TxOutKind, + self, AddIncomingResult, AddOutgoingResult, BounceResult, Initiated, TransferResult, + TxIn, TxInAdmin, TxOut, TxOutKind, }, }; @@ -823,7 +838,8 @@ mod test { let now = now_sql_stable_timestamp(); let later = now + Span::new().hours(2); let tx = TxIn { - id: now.as_microsecond() as u64, + transfer_id: now.as_microsecond() as u64, + tx_id: None, amount: decimal("10"), subject: "subject".to_owned(), debtor: cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"), @@ -864,7 +880,7 @@ mod test { db::register_tx_in( db, &TxIn { - id: later.as_microsecond() as u64, + transfer_id: later.as_microsecond() as u64, valued_at: later, ..tx }, @@ -1008,7 +1024,7 @@ mod test { let (mut db, pool) = setup().await; async fn routine(db: &mut PgConnection, first: &TxOutKind, second: &TxOutKind) { - let id = sqlx::query("SELECT count(*) + 1 FROM tx_out") + let transfer_id = sqlx::query("SELECT count(*) + 1 FROM tx_out") .try_map(|r: PgRow| r.try_get_u64(0)) .fetch_one(&mut *db) .await @@ -1016,7 +1032,8 @@ mod test { let now = now_sql_stable_timestamp(); let later = now + Span::new().hours(2); let tx = TxOut { - id, + transfer_id, + tx_id: Some(transfer_id), amount: decimal("10"), subject: "subject".to_owned(), creditor: cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"), @@ -1041,7 +1058,7 @@ mod test { .unwrap(), TransferResult::Success { .. } )); - db::initiated_submit_success(&mut *db, 1, &Timestamp::now(), id) + db::initiated_submit_success(&mut *db, 1, &Timestamp::now(), transfer_id) .await .expect("status success"); @@ -1052,7 +1069,7 @@ mod test { .expect("register tx out"), AddOutgoingResult { result: db::RegisterResult::known, - row_id: id, + row_id: transfer_id, } ); // Idempotent @@ -1070,7 +1087,7 @@ mod test { .expect("register tx out"), AddOutgoingResult { result: db::RegisterResult::idempotent, - row_id: id, + row_id: transfer_id, } ); // Recovered @@ -1078,7 +1095,8 @@ mod test { db::register_tx_out( &mut *db, &TxOut { - id: id + 1, + transfer_id: transfer_id + 1, + tx_id: Some(transfer_id + 1), valued_at: later, ..tx.clone() }, @@ -1089,7 +1107,7 @@ mod test { .expect("register tx out"), AddOutgoingResult { result: db::RegisterResult::recovered, - row_id: id + 1, + row_id: transfer_id + 1, } ); } @@ -1271,7 +1289,8 @@ mod test { db::register_bounced_tx_in( &mut db, &TxIn { - id: 12, + transfer_id: 12, + tx_id: None, amount, subject: "subject".to_owned(), debtor: payto.clone(), @@ -1293,7 +1312,8 @@ mod test { db::register_bounced_tx_in( &mut db, &TxIn { - id: 12, + transfer_id: 12, + tx_id: None, amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), @@ -1316,7 +1336,8 @@ mod test { db::register_bounced_tx_in( &mut db, &TxIn { - id: 13, + transfer_id: 13, + tx_id: None, amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), @@ -1336,6 +1357,104 @@ mod test { } #[tokio::test] + async fn status() { + let (mut db, _) = setup().await; + let cyclos_payto = cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"); + + async fn check_status( + db: &mut PgConnection, + id: u64, + status: TransferState, + msg: Option<&str>, + ) { + let transfer = db::transfer_by_id(db, id, &CURRENCY) + .await + .unwrap() + .unwrap(); + assert_eq!( + (status, msg), + (transfer.status, transfer.status_msg.as_deref()) + ); + } + + // Unknown transfer + db::initiated_submit_permanent_failure(&mut db, 1, &Timestamp::now(), "msg") + .await + .unwrap(); + db::initiated_submit_success(&mut db, 1, &Timestamp::now(), 12) + .await + .unwrap(); + + // Failure + db::make_transfer( + &mut db, + &TransferRequest { + request_uid: HashCode::rand(), + amount: amount(format!("{}:1", *CURRENCY)), + exchange_base_url: url("https://exchange.test.com/"), + wtid: ShortHashCode::rand(), + credit_account: payto( + "payto://iban/HU02162000031000164800000000?receiver-name=name", + ), + }, + &cyclos_payto, + &Timestamp::now(), + ) + .await + .expect("transfer"); + check_status(&mut db, 1, TransferState::pending, None).await; + db::initiated_submit_permanent_failure(&mut db, 1, &Timestamp::now(), "error status") + .await + .unwrap(); + check_status( + &mut db, + 1, + TransferState::permanent_failure, + Some("error status"), + ) + .await; + + // Success + db::make_transfer( + &mut db, + &TransferRequest { + request_uid: HashCode::rand(), + amount: amount(format!("{}:2", *CURRENCY)), + exchange_base_url: url("https://exchange.test.com/"), + wtid: ShortHashCode::rand(), + credit_account: payto( + "payto://iban/HU02162000031000164800000000?receiver-name=name", + ), + }, + &cyclos_payto, + &Timestamp::now(), + ) + .await + .expect("transfer"); + check_status(&mut db, 2, TransferState::pending, None).await; + db::initiated_submit_success(&mut db, 2, &Timestamp::now(), 3) + .await + .unwrap(); + check_status(&mut db, 2, TransferState::pending, None).await; + db::register_tx_out( + &mut db, + &TxOut { + transfer_id: 2, + tx_id: Some(3), + amount: decimal("2"), + subject: "".to_string(), + creditor: cyclos_payto, + valued_at: Timestamp::now(), + }, + &TxOutKind::Simple, + &Timestamp::now(), + ) + .await + .unwrap(); + check_status(&mut db, 2, TransferState::success, None).await; + } + + #[tokio::test] async fn batch() { let (mut db, _) = setup().await; let start = Timestamp::now(); diff --git a/taler-cyclos/src/worker.rs b/taler-cyclos/src/worker.rs @@ -86,7 +86,6 @@ impl Worker<'_> { // TODO fail_point("init-tx")?; match res { Ok(tx) => { - dbg!(tx.date); // Update transaction status, on failure the initiated transaction will be orphan db::initiated_submit_success( &mut *self.db, @@ -95,11 +94,12 @@ impl Worker<'_> { tx.id.0, ) .await?; + trace!(target: "worker", "init tx {}", tx.id); } Err(e) => { let msg = match e.kind { ErrKind::Unknown(NotFoundError { entity_type, key }) => { - format!("Unknown {entity_type} {key}") + format!("unknown {entity_type} {key}") } ErrKind::Forbidden(err) => err.to_string(), _ => return Err(e.into()), @@ -124,7 +124,7 @@ impl Worker<'_> { async fn ingest_in(&mut self, tx: TxIn) -> WorkerResult { match self.account_type { AccountType::Exchange => { - let transfer = self.client.transfer(tx.id).await?; + let transfer = self.client.transfer(tx.transfer_id).await?; let bounce = async |db: &mut PgConnection, reason: &str| -> Result<(), WorkerError> { @@ -220,7 +220,7 @@ impl Worker<'_> { async fn ingest_out(&mut self, tx: TxOut) -> WorkerResult { match self.account_type { AccountType::Exchange => { - let transfer = self.client.transfer(tx.id).await?; + let transfer = self.client.transfer(tx.transfer_id).await?; let kind = if let Ok(subject) = subject::parse_outgoing(&tx.subject) { TxOutKind::Talerable(subject) @@ -264,8 +264,7 @@ impl Worker<'_> { warn!(target: "worker", "out malformed (recovered) {tx}") } TxOutKind::Bounce(_) => { - // Chargeback are not stored as initiated and are therefor always recovered - info!(target: "worker", "out bounce {tx}") + warn!(target: "worker", "out bounce (recovered) {tx}") } TxOutKind::Talerable(_) => { warn!(target: "worker", "out (recovered) {tx}") @@ -308,7 +307,8 @@ pub fn extract_tx_info(tx: HistoryItem) -> Tx { }; if tx.amount.starts_with("-") { Tx::Out(TxOut { - id: *tx.id, + transfer_id: *tx.id, + tx_id: tx.transaction.map(|it| *it.id), amount, subject: tx.description.unwrap_or_default(), creditor: payto, @@ -316,7 +316,8 @@ pub fn extract_tx_info(tx: HistoryItem) -> Tx { }) } else { Tx::In(TxIn { - id: *tx.id, + transfer_id: *tx.id, + tx_id: tx.transaction.map(|it| *it.id), amount, subject: tx.description.unwrap_or_default(), debtor: payto, diff --git a/taler-cyclos/tests/api.rs b/taler-cyclos/tests/api.rs @@ -0,0 +1,141 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER 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. + + TALER 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 + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::sync::Arc; + +use jiff::{Timestamp, Zoned}; +use sqlx::PgPool; +use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; +use taler_common::{ + api_common::ShortHashCode, + api_revenue::RevenueConfig, + api_wire::{OutgoingHistory, TransferState, WireConfig}, + types::{amount::amount, payto::payto, url}, +}; +use taler_test_utils::{ + Router, db_test_setup, + routine::{admin_add_incoming_routine, revenue_routine, routine_pagination, transfer_routine}, + server::TestServer, +}; + +/* + +async fn setup() -> (Router, PgPool) { + let pool = db_test_setup(CONFIG_SOURCE).await; + let api = Arc::new( + MagnetApi::start( + pool.clone(), + payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), + ) + .await, + ); + let server = Router::new() + .wire_gateway(api.clone(), AuthMethod::None) + .revenue(api, AuthMethod::None) + .finalize(); + + (server, pool) +} + +#[tokio::test] +async fn config() { + let (server, _) = setup().await; + server + .get("/taler-wire-gateway/config") + .await + .assert_ok_json::<WireConfig>(); + server + .get("/taler-revenue/config") + .await + .assert_ok_json::<RevenueConfig>(); +} + +#[tokio::test] +async fn transfer() { + let (server, _) = setup().await; + transfer_routine( + &server, + TransferState::pending, + &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), + ) + .await; +} + +#[tokio::test] +async fn outgoing_history() { + let (server, pool) = setup().await; + routine_pagination::<OutgoingHistory, _>( + &server, + "/taler-wire-gateway/history/outgoing", + |it| { + it.outgoing_transactions + .into_iter() + .map(|it| *it.row_id as i64) + .collect() + }, + |_, i| { + let acquire = pool.acquire(); + async move { + let mut conn = acquire.await.unwrap(); + let now = Zoned::now().date(); + db::register_tx_out( + &mut *conn, + &db::TxOut { + code: i as u64, + amount: amount("EUR:10"), + subject: "subject".to_owned(), + creditor: magnet_payto( + "payto://iban/HU30162000031000163100000000?receiver-name=name", + ), + value_date: now, + status: TxStatus::Completed, + }, + &TxOutKind::Talerable(OutgoingSubject( + ShortHashCode::rand(), + url("https://exchange.test"), + )), + &Timestamp::now(), + ) + .await + .unwrap(); + } + }, + ) + .await; +} + +#[tokio::test] +async fn admin_add_incoming() { + let (server, _) = setup().await; + admin_add_incoming_routine( + &server, + &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), + true, + ) + .await; +} + +#[tokio::test] +async fn revenue() { + let (server, _) = setup().await; + revenue_routine( + &server, + &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), + true, + ) + .await; +} +*/ +\ No newline at end of file diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -393,25 +393,6 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { balance.expect(-34).await; harness.worker().await?; - step("Test transfer to self"); - // Init a transfer to self - let transfer_id = harness - .custom_transfer( - 101, - &FullHuPayto::new(harness.exchange.iban.clone(), "Self".to_string()), - ) - .await; - // Should failed - harness.worker().await?; - // Check transfer failed - harness - .expect_transfer_status( - transfer_id, - TransferState::permanent_failure, - Some("409 FORRAS_SZAMLA_ESZAMLA_EGYEZIK 'A forrás és az ellenszámla egyezik!'"), - ) - .await; - step("Test transfer transactions"); // Init a transfer to client let transfer_id = harness @@ -435,6 +416,25 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { .expect_transfer_status(transfer_id, TransferState::success, None) .await; + step("Test transfer to self"); + // Init a transfer to self + let transfer_id = harness + .custom_transfer( + 101, + &FullHuPayto::new(harness.exchange.iban.clone(), "Self".to_string()), + ) + .await; + // Should failed + harness.worker().await?; + // Check transfer failed + harness + .expect_transfer_status( + transfer_id, + TransferState::permanent_failure, + Some("409 FORRAS_SZAMLA_ESZAMLA_EGYEZIK 'A forrás és az ellenszámla egyezik!'"), + ) + .await; + step("Test transfer to unknown account"); let transfer_id = harness.custom_transfer(103, &unknown_account).await; harness.worker().await?;