cashout.rs (6312B)
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 use jiff::Timestamp; 21 use sqlx::{ 22 Arguments, PgPool, QueryBuilder, Row, 23 postgres::{PgArguments, PgRow}, 24 }; 25 use taler_api::{ 26 db::{BindHelper, TypeHelper, page}, 27 serialized, 28 }; 29 use taler_common::{ 30 api::{ShortHashCode, params::Page}, 31 types::amount::{Amount, Currency}, 32 }; 33 34 use crate::api::cashout::{CashoutInfo, CashoutStatus, CashoutStatusResponse, GlobalCashoutInfo}; 35 36 /** Result of cashout operation creation */ 37 pub enum CreationResult { 38 Success(u64), 39 UnderMin, 40 BadConversion, 41 AccountNotFound, 42 AccountIsExchange, 43 BalanceInsufficient, 44 RequestUidReuse, 45 NoCashoutPayto, 46 TanRequired, 47 } 48 49 /** Create a new cashout operation */ 50 pub async fn create( 51 db: &PgPool, 52 username: &str, 53 request_uid: &ShortHashCode, 54 debit: &Amount, 55 credit: &Amount, 56 subject: &str, 57 timestamp: &Timestamp, 58 is2fa: bool, 59 ) -> sqlx::Result<CreationResult> { 60 serialized!( 61 sqlx::query( 62 " 63 SELECT 64 out_bad_conversion, 65 out_account_not_found, 66 out_account_is_exchange, 67 out_balance_insufficient, 68 out_request_uid_reuse, 69 out_no_cashout_payto, 70 out_tan_required, 71 out_cashout_id, 72 out_under_min 73 FROM cashout_create($1,$2,$3,$4,$5,$6,$7) 74 ", 75 ) 76 .bind(username) 77 .bind(request_uid) 78 .bind(debit) 79 .bind(credit) 80 .bind(subject) 81 .bind_timestamp(timestamp) 82 .bind(is2fa) 83 .try_map(|r: PgRow| { 84 Ok(if r.try_get_flag("out_under_min")? { 85 CreationResult::UnderMin 86 } else if r.try_get_flag("out_bad_conversion")? { 87 CreationResult::BadConversion 88 } else if r.try_get_flag("out_account_not_found")? { 89 CreationResult::AccountNotFound 90 } else if r.try_get_flag("out_account_is_exchange")? { 91 CreationResult::AccountIsExchange 92 } else if r.try_get_flag("out_balance_insufficient")? { 93 CreationResult::BalanceInsufficient 94 } else if r.try_get_flag("out_request_uid_reuse")? { 95 CreationResult::RequestUidReuse 96 } else if r.try_get_flag("out_no_cashout_payto")? { 97 CreationResult::NoCashoutPayto 98 } else if r.try_get_flag("out_tan_required")? { 99 CreationResult::TanRequired 100 } else { 101 CreationResult::Success(r.try_get_u64("out_cashout_id")?) 102 }) 103 }) 104 .fetch_one(db) 105 ) 106 } 107 108 /** Get status of cashout operation [id] owned by [username] */ 109 pub async fn get_for_user( 110 db: &PgPool, 111 regional: &Currency, 112 fiat: &Currency, 113 username: &str, 114 id: u64, 115 ) -> sqlx::Result<Option<CashoutStatusResponse>> { 116 serialized!( 117 sqlx::query( 118 " 119 SELECT 120 amount_debit 121 ,amount_credit 122 ,cashout_operations.subject 123 ,creation_time 124 ,transaction_date as confirmation_date 125 FROM cashout_operations 126 JOIN bank_accounts ON bank_account=bank_account_id 127 JOIN customers ON owning_customer_id=customer_id 128 LEFT JOIN bank_account_transactions ON local_transaction=bank_transaction_id 129 WHERE cashout_id=$1 AND username=$2 130 ", 131 ) 132 .bind(id as i64) 133 .bind(username) 134 .try_map(|r: PgRow| { 135 Ok(CashoutStatusResponse { 136 amount_debit: r.try_get_amount("amount_debit", regional)?, 137 amount_credit: r.try_get_amount("amount_credit", fiat)?, 138 subject: r.try_get("subject")?, 139 creation_time: r.try_get_timestamp("creation_time")?.into(), 140 confirmation_time: r 141 .try_get_opt_timestamp("confirmation_date")? 142 .map(Into::into), 143 }) 144 }) 145 .fetch_optional(db) 146 ) 147 } 148 149 /** Get a page of all cashout operations */ 150 pub async fn page_all(db: &PgPool, params: &Page) -> sqlx::Result<Vec<GlobalCashoutInfo>> { 151 page( 152 db, 153 params, 154 "cashout_id", 155 || { 156 QueryBuilder::new( 157 " 158 SELECT cashout_id, username 159 FROM cashout_operations 160 JOIN bank_accounts ON bank_account=bank_account_id 161 JOIN customers ON owning_customer_id=customer_id 162 WHERE 163 ", 164 ) 165 }, 166 |r| { 167 Ok(GlobalCashoutInfo { 168 cashout_id: r.try_get_u64("cashout_id")?, 169 username: r.try_get("username")?, 170 status: CashoutStatus::confirmed, 171 }) 172 }, 173 ) 174 .await 175 } 176 177 /** Get a page of all cashout operations */ 178 pub async fn page_for_user( 179 db: &PgPool, 180 username: &str, 181 params: &Page, 182 ) -> sqlx::Result<Vec<CashoutInfo>> { 183 page( 184 db, 185 params, 186 "cashout_id", 187 || { 188 let mut params = PgArguments::default(); 189 params.add(username).unwrap(); 190 QueryBuilder::with_arguments( 191 " 192 SELECT cashout_id 193 FROM cashout_operations 194 WHERE bank_account=( 195 SELECT bank_account_id 196 FROM bank_accounts JOIN customers ON owning_customer_id=customer_id 197 WHERE deleted_at IS NULL AND username = $1 198 ) AND 199 ", 200 params, 201 ) 202 }, 203 |r| { 204 Ok(CashoutInfo { 205 cashout_id: r.try_get_u64("cashout_id")?, 206 status: CashoutStatus::confirmed, 207 }) 208 }, 209 ) 210 .await 211 }