libeufin

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

TransactionDAO.kt (8630B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2023, 2024, 2025, 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 package tech.libeufin.bank.db
     21 
     22 import org.slf4j.Logger
     23 import org.slf4j.LoggerFactory
     24 import tech.libeufin.bank.BankAccountTransactionInfo
     25 import tech.libeufin.common.*
     26 import tech.libeufin.common.db.*
     27 import java.sql.Types
     28 import java.time.Instant
     29 
     30 private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-tx-dao")
     31 
     32 /** Data access logic for transactions */
     33 class TransactionDAO(private val db: Database) {
     34     /** Result status of bank transaction creation */
     35     sealed interface BankTransactionResult {
     36         data class Success(val id: Long): BankTransactionResult
     37         data object UnknownCreditor: BankTransactionResult
     38         data object AdminCreditor: BankTransactionResult
     39         data object UnknownDebtor: BankTransactionResult
     40         data object BothPartySame: BankTransactionResult
     41         data object BalanceInsufficient: BankTransactionResult
     42         data object BadAmount: BankTransactionResult
     43         data object TanRequired: BankTransactionResult
     44         data object RequestUidReuse: BankTransactionResult
     45     }
     46 
     47     /** Create a new transaction */
     48     suspend fun create(
     49         creditAccountPayto: Payto,
     50         debitAccountUsername: String,
     51         subject: String,
     52         amount: TalerAmount,
     53         timestamp: Instant,
     54         is2fa: Boolean,
     55         requestUid: ShortHashCode?,
     56         wireTransferFees: TalerAmount,
     57         minAmount: TalerAmount,
     58         maxAmount: TalerAmount
     59     ): BankTransactionResult {
     60         val (type: IncomingType?, metadata: EddsaPublicKey?, bounceCause: String?) = runCatching { parseIncomingSubject(subject) }.fold(
     61             onSuccess = { metadata -> 
     62                 if (metadata is IncomingSubject.AdminBalanceAdjust) {
     63                     Triple(null, null, "unsupported admin balance adjust")
     64                 } else {
     65                     Triple(metadata.type, metadata.key, null)
     66                 }
     67             },
     68             onFailure = { e -> Triple(null, null, "malformed metadata - ${e.message}") }
     69         )
     70         return db.serializable("""
     71             SELECT 
     72                 out_creditor_not_found 
     73                 ,out_debtor_not_found
     74                 ,out_same_account
     75                 ,out_balance_insufficient
     76                 ,out_bad_amount
     77                 ,out_request_uid_reuse
     78                 ,out_tan_required
     79                 ,out_credit_bank_account_id
     80                 ,out_debit_bank_account_id
     81                 ,out_credit_row_id
     82                 ,out_debit_row_id
     83                 ,out_creditor_is_exchange 
     84                 ,out_debtor_is_exchange
     85                 ,out_creditor_admin
     86                 ,out_idempotent
     87             FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?,?,(?,?)::taler_amount,(?,?)::taler_amount,(?,?)::taler_amount,?::taler_incoming_type,?,?)
     88         """
     89         ) {
     90             bind(creditAccountPayto.canonical)
     91             bind(debitAccountUsername)
     92             bind(subject)
     93             bind(amount)
     94             bind(timestamp)
     95             bind(is2fa)
     96             bind(requestUid)
     97             bind(wireTransferFees)
     98             bind(minAmount)
     99             bind(maxAmount)
    100             bind(type)
    101             bind(metadata)
    102             bind(bounceCause)
    103             one {
    104                 when {
    105                     it.getBoolean("out_creditor_not_found") -> BankTransactionResult.UnknownCreditor
    106                     it.getBoolean("out_debtor_not_found") -> BankTransactionResult.UnknownDebtor
    107                     it.getBoolean("out_same_account") -> BankTransactionResult.BothPartySame
    108                     it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BalanceInsufficient
    109                     it.getBoolean("out_bad_amount") -> BankTransactionResult.BadAmount
    110                     it.getBoolean("out_creditor_admin") -> BankTransactionResult.AdminCreditor
    111                     it.getBoolean("out_request_uid_reuse") -> BankTransactionResult.RequestUidReuse
    112                     it.getBoolean("out_idempotent") -> BankTransactionResult.Success(it.getLong("out_debit_row_id"))
    113                     it.getBoolean("out_tan_required") -> BankTransactionResult.TanRequired
    114                     else -> BankTransactionResult.Success(it.getLong("out_debit_row_id"))
    115                 }
    116             }
    117         }
    118     }
    119     
    120     /** Get transaction [rowId] owned by [username] */
    121     suspend fun get(rowId: Long, username: String): BankAccountTransactionInfo? = db.serializable(
    122         """
    123         SELECT 
    124             creditor_payto
    125             ,creditor_name
    126             ,debtor_payto
    127             ,debtor_name
    128             ,subject
    129             ,(amount).val AS amount_val
    130             ,(amount).frac AS amount_frac
    131             ,transaction_date
    132             ,direction
    133             ,bank_transaction_id
    134         FROM bank_account_transactions
    135             JOIN bank_accounts ON bank_account_transactions.bank_account_id=bank_accounts.bank_account_id
    136             JOIN customers ON customer_id=owning_customer_id 
    137         WHERE bank_transaction_id=? AND username=?
    138         """
    139     ) {
    140         bind(rowId)
    141         bind(username)
    142         oneOrNull {
    143             BankAccountTransactionInfo(
    144                 creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx),
    145                 debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
    146                 amount = it.getAmount("amount", db.bankCurrency),
    147                 direction = it.getEnum("direction"),
    148                 subject = it.getString("subject"),
    149                 date = it.getTalerTimestamp("transaction_date"),
    150                 row_id = it.getLong("bank_transaction_id")
    151             )
    152         }
    153     }
    154 
    155     /** Pool [accountId] transactions history */
    156     suspend fun pollHistory(
    157         params: HistoryParams, 
    158         accountId: Long
    159     ): List<BankAccountTransactionInfo> {
    160         return db.poolHistory(params, accountId, db::listenBank,  """
    161             SELECT
    162                 bank_transaction_id
    163                 ,transaction_date
    164                 ,(amount).val AS amount_val
    165                 ,(amount).frac AS amount_frac
    166                 ,debtor_payto
    167                 ,debtor_name
    168                 ,creditor_payto
    169                 ,creditor_name
    170                 ,subject
    171                 ,direction
    172             FROM bank_account_transactions WHERE
    173         """) {
    174             BankAccountTransactionInfo(
    175                 row_id = it.getLong("bank_transaction_id"),
    176                 date = it.getTalerTimestamp("transaction_date"),
    177                 creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx),
    178                 debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
    179                 amount = it.getAmount("amount", db.bankCurrency),
    180                 subject = it.getString("subject"),
    181                 direction = it.getEnum("direction")
    182             )
    183         }
    184     }
    185 
    186     /** Query [accountId] history of incoming transactions to its account */
    187     suspend fun revenueHistory(
    188         params: HistoryParams, 
    189         accountId: Long
    190     ): List<RevenueIncomingBankTransaction> 
    191         = db.poolHistory(params, accountId, db::listenRevenue, """
    192             SELECT
    193                 bank_transaction_id
    194                 ,transaction_date
    195                 ,(amount).val AS amount_val
    196                 ,(amount).frac AS amount_frac
    197                 ,debtor_payto
    198                 ,debtor_name
    199                 ,subject
    200             FROM bank_account_transactions WHERE direction='credit' AND
    201         """) {
    202             RevenueIncomingBankTransaction(
    203                 row_id = it.getLong("bank_transaction_id"),
    204                 date = it.getTalerTimestamp("transaction_date"),
    205                 amount = it.getAmount("amount", db.bankCurrency),
    206                 debit_account = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
    207                 subject = it.getString("subject")
    208             )
    209         }
    210 }