libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }