taler-rust

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

commit 347379a20e932c6c52eee5d6f48c5d98c8f7a8cb
parent 33961b8399dde8a021e0a1eb849f685e7ffd9963
Author: Antoine A <>
Date:   Wed,  5 Feb 2025 17:22:39 +0100

magnet-bank: db bounce logic

Diffstat:
Mtaler-magnet-bank/db/schema.sql | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtaler-magnet-bank/src/db.rs | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 166 insertions(+), 3 deletions(-)

diff --git a/taler-magnet-bank/db/schema.sql b/taler-magnet-bank/db/schema.sql @@ -98,6 +98,13 @@ CREATE TABLE transfer( ); COMMENT ON TABLE transfer IS 'Wire Gateway transfers'; +CREATE TABLE bounced( + tx_in_id INT8 NOT NULL UNIQUE REFERENCES tx_in(tx_in_id) ON DELETE CASCADE, + initiated_id INT8 NOT NULL UNIQUE REFERENCES initiated(initiated_id) ON DELETE CASCADE, + reason TEXT NOT NULL +); +COMMENT ON TABLE tx_in IS 'Bounced transactions'; + CREATE FUNCTION register_tx_in( IN in_code INT8, IN in_amount taler_amount, @@ -326,4 +333,59 @@ BEGIN WHERE initiated_id = in_initiated_id; END IF; END IF; -END $$; -\ No newline at end of file +END $$; + +CREATE FUNCTION bounce( + IN in_tx_in_id INT8, + IN in_amount taler_amount, + IN in_reason TEXT, + IN in_timestamp INT8, + OUT out_tx_row_id INT8, + OUT out_timestamp INT8 +) +LANGUAGE plpgsql AS $$ +DECLARE +local_debit_account TEXT; +local_debit_name TEXT; +local_magnet_code INT8; +BEGIN +-- Check if already bounce +SELECT initiated_id, created + INTO out_tx_row_id, out_timestamp + FROM bounced JOIN initiated USING (initiated_id) + WHERE tx_in_id = in_tx_in_id; + +-- Else initiate the bounce transaction +IF NOT FOUND THEN + -- Get incoming transaction bank ID and creditor + SELECT debit_account, debit_name, magnet_code + INTO local_debit_account, local_debit_name, local_magnet_code + FROM tx_in + WHERE tx_in_id = in_tx_in_id; + -- Initiate the bounce transaction + INSERT INTO initiated ( + amount, + subject, + credit_account, + credit_name, + created + ) VALUES ( + in_amount, + 'bounce: ' || local_magnet_code, + local_debit_account, + local_debit_name, + in_timestamp + ) + RETURNING initiated_id, created INTO out_tx_row_id, out_timestamp; + -- Register the bounce + INSERT INTO bounced ( + tx_in_id, + initiated_id, + reason + ) VALUES ( + in_tx_in_id, + out_tx_row_id, + in_reason + ); +END IF; +END$$; +\ No newline at end of file diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -293,6 +293,39 @@ pub async fn make_transfer<'a>( .await } +#[derive(Debug, PartialEq, Eq)] +pub struct BounceResult { + id: u64, + timestamp: Timestamp, +} + +pub async fn bounce<'a>( + db: impl PgExecutor<'a>, + tx_in: u64, + amount: &Amount, + reason: &str, + timestamp: &Timestamp, +) -> sqlx::Result<BounceResult> { + sqlx::query( + " + SELECT out_tx_row_id, out_timestamp + FROM bounce($1, ($2, $3)::taler_amount, $4, $5) + ", + ) + .bind(tx_in as i64) + .bind_amount(amount) + .bind(reason) + .bind_timestamp(timestamp) + .try_map(|r: PgRow| { + Ok(BounceResult { + id: r.try_get_u64(0)?, + timestamp: r.try_get_timestamp(1)?, + }) + }) + .fetch_one(db) + .await +} + pub async fn transfer_page<'a>( db: impl PgExecutor<'a>, status: &Option<TransferState>, @@ -595,7 +628,7 @@ mod test { constant::CURRENCY, db::{ self, make_transfer, register_tx_in, register_tx_in_admin, register_tx_out, - AddIncomingResult, RegisteredTx, TransferResult, TxIn, TxOut, + AddIncomingResult, BounceResult, Initiated, RegisteredTx, TransferResult, TxIn, TxOut, }, magnet_payto, }; @@ -1026,6 +1059,74 @@ mod test { } #[tokio::test] + async fn bounce() { + let (mut db, _) = setup().await; + + let amount = amount("HUF:10"); + let payto = magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name"); + let timestamp = Timestamp::now_stable(); + + // Empty db + assert!(db::pending_batch(&mut db, &timestamp) + .await + .unwrap() + .is_empty()); + + // Insert + assert_eq!( + register_tx_in( + &mut db, + &TxIn { + code: 12, + amount: amount.clone(), + subject: "subject".to_owned(), + debtor: payto.clone(), + timestamp + }, + &None + ) + .await + .expect("register tx in"), + AddIncomingResult::Success(RegisteredTx { + new: true, + row_id: 1, + timestamp + }) + ); + // Bounce + assert_eq!( + db::bounce(&mut db, 1, &amount, "good reason", &timestamp) + .await + .expect("bounce"), + BounceResult { + id: 1, + timestamp: timestamp + } + ); + // Idempotent + assert_eq!( + db::bounce(&mut db, 1, &amount, "good reason", &timestamp) + .await + .expect("bounce"), + BounceResult { + id: 1, + timestamp: timestamp + } + ); + + // Batch + assert_eq!( + db::pending_batch(&mut db, &timestamp).await.unwrap(), + &[Initiated { + id: 1, + amount, + subject: "bounce: 12".to_owned(), + creditor: payto + }] + ); + } + + #[tokio::test] async fn status() { let (mut db, _) = setup().await;