tx.rs (8836B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 //! Data access logic for transactions 21 22 use jiff::Timestamp; 23 use sqlx::{PgPool, QueryBuilder, Row, postgres::PgRow}; 24 use taler_api::{ 25 db::{BindHelper, TypeHelper, history}, 26 notification::NotificationChannel, 27 serialized, 28 subject::{IncomingKey, IncomingSubject, parse_incoming_unstructured}, 29 }; 30 use taler_common::{ 31 api::{ShortHashCode, params::History, revenue::RevenueIncomingBankTransaction}, 32 types::amount::{Amount, Currency}, 33 }; 34 35 use crate::{ 36 api::tx::BankAccountTransactionInfo, 37 payto::{BankPayto, PaytoCtx, sql_bank_payto}, 38 }; 39 40 /** Result status of bank transaction creation */ 41 pub enum TxResult { 42 Success(u64), 43 UnknownCreditor, 44 AdminCreditor, 45 UnknownDebtor, 46 BothPartySame, 47 BalanceInsufficient, 48 BadAmount, 49 TanRequired, 50 RequestUidReuse, 51 } 52 53 /** Create a new transaction */ 54 pub async fn create( 55 db: &PgPool, 56 credit_account_payto: BankPayto, 57 debit_account_username: &str, 58 subject: &str, 59 amount: Amount, 60 timestamp: &Timestamp, 61 is2fa: bool, 62 request_uid: &Option<ShortHashCode>, 63 wire_transfer_fees: Amount, 64 min_amount: Amount, 65 max_amount: Amount, 66 ) -> sqlx::Result<TxResult> { 67 let (ty, metadata, cause) = match parse_incoming_unstructured(subject) { 68 Ok(IncomingSubject::Key(IncomingKey { ty, key })) => (Some(ty), Some(key), None), 69 Ok(IncomingSubject::AdminBalanceAdjust) => ( 70 None, 71 None, 72 Some("unsupported admin balance adjust".to_string()), 73 ), 74 Err(e) => (None, None, Some(format!("malformed metadata - {e}"))), 75 }; 76 serialized!( 77 sqlx::query( 78 " 79 SELECT 80 out_creditor_not_found 81 ,out_debtor_not_found 82 ,out_same_account 83 ,out_balance_insufficient 84 ,out_bad_amount 85 ,out_request_uid_reuse 86 ,out_tan_required 87 ,out_credit_bank_account_id 88 ,out_debit_bank_account_id 89 ,out_credit_row_id 90 ,out_debit_row_id 91 ,out_creditor_is_exchange 92 ,out_debtor_is_exchange 93 ,out_creditor_admin 94 ,out_idempotent 95 FROM bank_transaction($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::taler_incoming_type,$12,$13) 96 ", 97 ) 98 .bind(credit_account_payto.canonical()) 99 .bind(debit_account_username) 100 .bind(subject) 101 .bind(amount) 102 .bind_timestamp(timestamp) 103 .bind(is2fa) 104 .bind(request_uid) 105 .bind(wire_transfer_fees) 106 .bind(min_amount) 107 .bind(max_amount) 108 .bind(ty.as_ref().map(|it| it.as_ref())) 109 .bind(metadata) 110 .bind(&cause) 111 .try_map(|r: PgRow| { 112 Ok(if r.try_get_flag("out_creditor_not_found")? { 113 TxResult::UnknownCreditor 114 } else if r.try_get_flag("out_debtor_not_found")? { 115 TxResult::UnknownDebtor 116 } else if r.try_get_flag("out_same_account")? { 117 TxResult::BothPartySame 118 } else if r.try_get_flag("out_balance_insufficient")? { 119 TxResult::BalanceInsufficient 120 } else if r.try_get_flag("out_bad_amount")? { 121 TxResult::BadAmount 122 } else if r.try_get_flag("out_creditor_admin")? { 123 TxResult::AdminCreditor 124 } else if r.try_get_flag("out_request_uid_reuse")? { 125 TxResult::RequestUidReuse 126 } else if r.try_get_flag("out_idempotent")? { 127 TxResult::Success(r.try_get_u64("out_debit_row_id")?) 128 } else if r.try_get_flag("out_tan_required")? { 129 TxResult::TanRequired 130 } else { 131 TxResult::Success(r.try_get_u64("out_debit_row_id")?) 132 }) 133 }) 134 .fetch_one(db) 135 ) 136 } 137 138 // /** Get transaction [rowId] owned by [username] */ 139 pub async fn get_by_id( 140 db: &PgPool, 141 currency: &Currency, 142 ctx: &PaytoCtx, 143 id: u64, 144 username: &str, 145 ) -> sqlx::Result<Option<BankAccountTransactionInfo>> { 146 serialized!( 147 sqlx::query(" 148 SELECT 149 creditor_payto 150 ,creditor_name 151 ,debtor_payto 152 ,debtor_name 153 ,subject 154 ,amount 155 ,transaction_date 156 ,direction 157 ,bank_transaction_id 158 FROM bank_account_transactions 159 JOIN bank_accounts ON bank_account_transactions.bank_account_id=bank_accounts.bank_account_id 160 JOIN customers ON customer_id=owning_customer_id 161 WHERE bank_transaction_id=$1 AND username=$2 162 ") 163 .bind(id as i64) 164 .bind(username) 165 .try_map(|r: PgRow| Ok(BankAccountTransactionInfo { 166 creditor_payto_uri: sql_bank_payto(&r, ctx, "creditor_payto", "creditor_name")?, 167 debtor_payto_uri: sql_bank_payto(&r, ctx, "debtor_payto", "debtor_name")?, 168 direction: r.try_get("direction")?, 169 subject: r.try_get("subject")?, 170 amount: r.try_get_amount("amount", currency)?, 171 date: r.try_get_timestamp("transaction_date")?.into(), 172 row_id: r.try_get_u64("bank_transaction_id")? 173 })) 174 .fetch_optional(db) 175 ) 176 } 177 178 /** Pool [accountId] transactions history */ 179 pub async fn pool_history( 180 db: &PgPool, 181 ctx: &PaytoCtx, 182 currency: &Currency, 183 channel: &NotificationChannel<u64, i64>, 184 params: &History, 185 account_id: u64, 186 ) -> sqlx::Result<Vec<BankAccountTransactionInfo>> { 187 history( 188 db, 189 "bank_transaction_id", 190 params, 191 || channel.subscribe(account_id), 192 || { 193 let mut query = QueryBuilder::new( 194 "SELECT 195 bank_transaction_id 196 ,transaction_date 197 ,amount 198 ,debtor_payto 199 ,debtor_name 200 ,creditor_payto 201 ,creditor_name 202 ,subject 203 ,direction 204 FROM bank_account_transactions 205 WHERE bank_account_id=", 206 ); 207 query.push_bind(account_id as i64).push(" AND "); 208 query 209 }, 210 |r| { 211 Ok(BankAccountTransactionInfo { 212 creditor_payto_uri: sql_bank_payto(&r, ctx, "creditor_payto", "creditor_name")?, 213 debtor_payto_uri: sql_bank_payto(&r, ctx, "debtor_payto", "debtor_name")?, 214 direction: r.try_get("direction")?, 215 subject: r.try_get("subject")?, 216 amount: r.try_get_amount("amount", currency)?, 217 date: r.try_get_timestamp("transaction_date")?.into(), 218 row_id: r.try_get_u64("bank_transaction_id")?, 219 }) 220 }, 221 ) 222 .await 223 } 224 225 /** Query [accountId] history of incoming transactions to its account */ 226 pub async fn revenue_history( 227 db: &PgPool, 228 ctx: &PaytoCtx, 229 currency: &Currency, 230 channel: &NotificationChannel<u64, i64>, 231 params: &History, 232 account_id: u64, 233 ) -> sqlx::Result<Vec<RevenueIncomingBankTransaction>> { 234 history( 235 db, 236 "bank_transaction_id", 237 params, 238 || channel.subscribe(account_id), 239 || { 240 let mut query = QueryBuilder::new( 241 "SELECT 242 bank_transaction_id 243 ,transaction_date 244 ,amount 245 ,debtor_payto 246 ,debtor_name 247 ,subject 248 FROM bank_account_transactions 249 WHERE direction='credit' AND bank_account_id=", 250 ); 251 query.push_bind(account_id as i64).push(" AND "); 252 query 253 }, 254 |r| { 255 Ok(RevenueIncomingBankTransaction { 256 debit_account: sql_bank_payto(&r, ctx, "debtor_payto", "debtor_name")?.as_uri(), 257 subject: r.try_get("subject")?, 258 amount: r.try_get_amount("amount", currency)?, 259 date: r.try_get_timestamp("transaction_date")?.into(), 260 row_id: r.try_get_u64("bank_transaction_id")?, 261 credit_fee: None, 262 }) 263 }, 264 ) 265 .await 266 }