commit 347379a20e932c6c52eee5d6f48c5d98c8f7a8cb
parent 33961b8399dde8a021e0a1eb849f685e7ffd9963
Author: Antoine A <>
Date: Wed, 5 Feb 2025 17:22:39 +0100
magnet-bank: db bounce logic
Diffstat:
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, ×tamp)
+ .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", ×tamp)
+ .await
+ .expect("bounce"),
+ BounceResult {
+ id: 1,
+ timestamp: timestamp
+ }
+ );
+ // Idempotent
+ assert_eq!(
+ db::bounce(&mut db, 1, &amount, "good reason", ×tamp)
+ .await
+ .expect("bounce"),
+ BounceResult {
+ id: 1,
+ timestamp: timestamp
+ }
+ );
+
+ // Batch
+ assert_eq!(
+ db::pending_batch(&mut db, ×tamp).await.unwrap(),
+ &[Initiated {
+ id: 1,
+ amount,
+ subject: "bounce: 12".to_owned(),
+ creditor: payto
+ }]
+ );
+ }
+
+ #[tokio::test]
async fn status() {
let (mut db, _) = setup().await;