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 }