commit ead9f9be0ad8cd52f94d20f2ac7185285d7afb38 parent 50bf46ceedaae54a18709f22d9eae27aff59d85d Author: Antoine A <> Date: Tue, 14 Nov 2023 19:39:38 +0000 Add revenue API Diffstat:
14 files changed, 319 insertions(+), 134 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -217,6 +217,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) { coreBankApi(db, ctx) bankIntegrationApi(db, ctx) wireGatewayApi(db, ctx) + revenueApi(db) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt @@ -0,0 +1,56 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ +package tech.libeufin.bank + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* +import java.util.concurrent.TimeUnit +import java.io.File +import kotlin.random.Random +import net.taler.common.errorcodes.TalerErrorCode +import net.taler.wallet.crypto.Base32Crockford +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import tech.libeufin.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.future.await + +fun Routing.revenueApi(db: Database) { + auth(db, TokenScope.readonly) { + get("/accounts/{USERNAME}/taler-revenue/history") { + val params = HistoryParams.extract(context.request.queryParameters) + val bankAccount = call.bankAccount(db) + val items = db.exchange.revenueHistory(params, bankAccount.bankAccountId); + + if (items.isEmpty()) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(MerchantIncomingHistory(items, bankAccount.internalPaytoUri)) + } + } + } +} +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -161,7 +161,7 @@ data class MonitorWithConversion( * from/to the database. */ data class BankAccount( - val internalPaytoUri: IbanPayTo, + val internalPaytoUri: String, val bankAccountId: Long, val isTalerExchange: Boolean, ) @@ -249,10 +249,10 @@ data class ListBankAccountsResponse( data class AccountData( val name: String, val balance: Balance, - val payto_uri: IbanPayTo, + val payto_uri: String, val debit_threshold: TalerAmount, val contact_data: ChallengeContactData? = null, - val cashout_payto_uri: IbanPayTo? = null, + val cashout_payto_uri: String? = null, ) @Serializable @@ -306,7 +306,7 @@ data class BankAccountGetWithdrawalResponse( val confirmation_done: Boolean, val selection_done: Boolean, val selected_reserve_pub: EddsaPublicKey? = null, - val selected_exchange_account: IbanPayTo? = null + val selected_exchange_account: String? = null ) // GET /config response from the Taler Integration API. @@ -512,9 +512,25 @@ data class OutgoingTransaction( val row_id: Long, // DB row ID of the payment. val date: TalerProtocolTimestamp, val amount: TalerAmount, - val credit_account: IbanPayTo, // Payto of the receiver. + val credit_account: String, // Payto of the receiver. val wtid: ShortHashCode, - val exchange_base_url: ExchangeUrl, + val exchange_base_url: String, +) + +@Serializable +data class MerchantIncomingHistory( + val incoming_transactions : List<MerchantIncomingBankTransaction>, + val credit_account: String +) + +@Serializable +data class MerchantIncomingBankTransaction( + val row_id: Long, + val date: TalerProtocolTimestamp, + val amount: TalerAmount, + val debit_account: String, + val exchange_url: String, + val wtid: ShortHashCode ) /** diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt @@ -103,7 +103,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { if (items.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { - call.respond(reduce(items, bankAccount.internalPaytoUri.canonical)) + call.respond(reduce(items, bankAccount.internalPaytoUri)) } } get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -383,8 +383,8 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val email = it.getString("email"), phone = it.getString("phone") ), - cashout_payto_uri = it.getString("cashout_payto")?.run(::IbanPayTo), - payto_uri = IbanPayTo(it.getString("internal_payto_uri")), + cashout_payto_uri = it.getString("cashout_payto"), + payto_uri = it.getString("internal_payto_uri"), balance = Balance( amount = TalerAmount( it.getLong("balance_val"), @@ -600,7 +600,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.oneOrNull { BankAccount( - internalPaytoUri = IbanPayTo(it.getString("internal_payto_uri")), + internalPaytoUri = it.getString("internal_payto_uri"), isTalerExchange = it.getBoolean("is_taler_exchange"), bankAccountId = it.getLong("bank_account_id") ) @@ -622,7 +622,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.oneOrNull { BankAccount( - internalPaytoUri = IbanPayTo(it.getString("internal_payto_uri")), + internalPaytoUri = it.getString("internal_payto_uri"), isTalerExchange = it.getBoolean("is_taler_exchange"), bankAccountId = it.getLong("bank_account_id") ) @@ -631,40 +631,6 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val // BANK ACCOUNT TRANSACTIONS - private fun handleExchangeTx( - conn: PgConnection, - subject: String, - creditorAccountId: Long, - debtorAccountId: Long, - it: ResultSet - ) { - val metadata = TxMetadata.parse(subject) - if (it.getBoolean("out_creditor_is_exchange")) { - val rowId = it.getLong("out_credit_row_id") - if (metadata is IncomingTxMetadata) { - val stmt = conn.prepareStatement("CALL register_incoming(?, ?)") - stmt.setBytes(1, metadata.reservePub.raw) - stmt.setLong(2, rowId) - stmt.executeUpdate() - } else { - // TODO bounce - logger.warn("exchange account $creditorAccountId received a transaction $rowId with malformed metadata, will bounce in future version") - } - } - if (it.getBoolean("out_debtor_is_exchange")) { - val rowId = it.getLong("out_debit_row_id") - if (metadata is OutgoingTxMetadata) { - val stmt = conn.prepareStatement("CALL register_outgoing(NULL, ?, ?, ?)") - stmt.setBytes(1, metadata.wtid.raw) - stmt.setString(2, metadata.exchangeBaseUrl.url) - stmt.setLong(3, rowId) - stmt.executeUpdate() - } else { - logger.warn("exchange account $debtorAccountId sent a transaction $rowId with malformed metadata") - } - } - } - suspend fun bankTransaction( creditAccountPayto: IbanPayTo, debitAccountUsername: String, @@ -703,8 +669,39 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val it.getBoolean("out_same_account") -> BankTransactionResult.SAME_ACCOUNT it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BALANCE_INSUFFICIENT else -> { - handleExchangeTx(conn, subject, it.getLong("out_credit_bank_account_id"), it.getLong("out_debit_bank_account_id"), it) - rowId = it.getLong("out_debit_row_id"); + val creditAccountId = it.getLong("out_credit_bank_account_id") + val creditRowId = it.getLong("out_credit_row_id") + val debitAccountId = it.getLong("out_debit_bank_account_id") + val debitRowId = it.getLong("out_debit_row_id") + val metadata = TxMetadata.parse(subject) + if (it.getBoolean("out_creditor_is_exchange")) { + if (metadata is IncomingTxMetadata) { + conn.prepareStatement("CALL register_incoming(?, ?)").run { + setBytes(1, metadata.reservePub.raw) + setLong(2, creditRowId) + executeUpdate() + } + } else { + // TODO bounce + logger.warn("exchange account $creditAccountId received a transaction $creditRowId with malformed metadata, will bounce in future version") + } + } + if (it.getBoolean("out_debtor_is_exchange")) { + if (metadata is OutgoingTxMetadata) { + conn.prepareStatement("CALL register_outgoing(NULL, ?, ?, ?, ?, ?, ?)").run { + setBytes(1, metadata.wtid.raw) + setString(2, metadata.exchangeBaseUrl.url) + setLong(3, debitAccountId) + setLong(4, creditAccountId) + setLong(5, debitRowId) + setLong(6, creditRowId) + executeUpdate() + } + } else { + logger.warn("exchange account $debitAccountId sent a transaction $debitRowId with malformed metadata") + } + } + rowId = debitRowId; BankTransactionResult.SUCCESS } } @@ -785,13 +782,14 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val bankAccountId: Long, listen: suspend NotificationWatcher.(Long, suspend (Flow<Long>) -> Unit) -> Unit, query: String, + accountColumn: String = "bank_account_id", map: (ResultSet) -> T ): List<T> { suspend fun load(): List<T> = page( params.page, "bank_transaction_id", - "$query WHERE bank_account_id=? AND", + "$query WHERE $accountColumn=? AND", { setLong(1, bankAccountId) 1 diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -106,9 +106,42 @@ class ExchangeDAO(private val db: Database) { it.getInt("amount_frac"), db.bankCurrency ), - credit_account = IbanPayTo(it.getString("creditor_payto_uri")), + credit_account = it.getString("creditor_payto_uri"), wtid = ShortHashCode(it.getBytes("wtid")), - exchange_base_url = ExchangeUrl(it.getString("exchange_base_url")) + exchange_base_url = it.getString("exchange_base_url") + ) + } + + suspend fun revenueHistory( + params: HistoryParams, + bankAccountId: Long + ): List<MerchantIncomingBankTransaction> + = db.poolHistory(params, bankAccountId, NotificationWatcher::listenRevenue, """ + SELECT + bank_transaction_id + ,transaction_date + ,(amount).val AS amount_val + ,(amount).frac AS amount_frac + ,debtor_payto_uri + ,wtid + ,exchange_base_url + FROM taler_exchange_outgoing AS tfr + JOIN bank_account_transactions AS txs + ON bank_transaction=txs.bank_transaction_id + """, "creditor_account_id") { + MerchantIncomingBankTransaction( + row_id = it.getLong("bank_transaction_id"), + date = TalerProtocolTimestamp( + it.getLong("transaction_date").microsToJavaInstant() ?: throw faultyTimestampByBank() + ), + amount = TalerAmount( + it.getLong("amount_val"), + it.getInt("amount_frac"), + db.bankCurrency + ), + debit_account = it.getString("debtor_payto_uri"), + wtid = ShortHashCode(it.getBytes("wtid")), + exchange_url = it.getString("exchange_base_url") ) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt @@ -29,6 +29,7 @@ internal class NotificationWatcher(private val pgSource: PGSimpleDataSource) { private class CountedSharedFlow(val flow: MutableSharedFlow<Long>, var count: Int) private val bankTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>() + private val revenueTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>() private val outgoingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>() private val incomingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>() @@ -46,32 +47,25 @@ internal class NotificationWatcher(private val pgSource: PGSimpleDataSource) { conn.getNotifications(0) // Block until we receive at least one notification .forEach { if (it.name == "bank_tx") { - val info = it.parameter.split(' ', limit = 4).map { it.toLong() } - val debtorAccount = info[0]; - val creditorAccount = info[1]; - val debitRow = info[2]; - val creditRow = info[3]; - - bankTxFlows[debtorAccount]?.run { + val (debtor, creditor, debitRow, creditRow) = it.parameter.split(' ', limit = 4).map { it.toLong() } + bankTxFlows[debtor]?.run { flow.emit(debitRow) + } + bankTxFlows[creditor]?.run { flow.emit(creditRow) } - bankTxFlows[creditorAccount]?.run { + } else if (it.name == "outgoing_tx") { + val (account, merchant, debitRow, creditRow) = it.parameter.split(' ', limit = 4).map { it.toLong() } + outgoingTxFlows[account]?.run { flow.emit(debitRow) + } + revenueTxFlows[merchant]?.run { flow.emit(creditRow) } } else { - val info = it.parameter.split(' ', limit = 2).map { it.toLong() } - val account = info[0]; - val row = info[1]; - if (it.name == "outgoing_tx") { - outgoingTxFlows[account]?.run { - flow.emit(row) - } - } else { - incomingTxFlows[account]?.run { - flow.emit(row) - } + val (account, row) = it.parameter.split(' ', limit = 2).map { it.toLong() } + incomingTxFlows[account]?.run { + flow.emit(row) } } } @@ -115,4 +109,8 @@ internal class NotificationWatcher(private val pgSource: PGSimpleDataSource) { suspend fun listenIncoming(account: Long, lambda: suspend (Flow<Long>) -> Unit) { listen(incomingTxFlows, account, lambda) } + + suspend fun listenRevenue(account: Long, lambda: suspend (Flow<Long>) -> Unit) { + listen(revenueTxFlows, account, lambda) + } } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -106,7 +106,7 @@ class WithdrawalDAO(private val db: Database) { selection_done = it.getBoolean("selection_done"), confirmation_done = it.getBoolean("confirmation_done"), aborted = it.getBoolean("aborted"), - selected_exchange_account = it.getString("selected_exchange_payto")?.run(::IbanPayTo), + selected_exchange_account = it.getString("selected_exchange_payto"), selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey), ) } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -386,7 +386,7 @@ class CoreBankAccountsApiTest { }.assertOk().run { val obj: AccountData = json() assertEquals("Another Foo", obj.name) - assertEquals(cashout.canonical, obj.cashout_payto_uri?.canonical) + assertEquals(cashout.canonical, obj.cashout_payto_uri) assertEquals("+99", obj.contact_data?.phone) assertEquals("foo@example.com", obj.contact_data?.email) assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold) @@ -1119,15 +1119,7 @@ class CoreBankCashoutApiTest { @Test fun abort() = bankSetup { _ -> // TODO auth routine - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") val req = json { "request_uid" to randShortHashCode() @@ -1214,15 +1206,7 @@ class CoreBankCashoutApiTest { basicAuth("customer", "customer-password") jsonBody { "tan" to "code" } }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") // Check bad TAN code client.post("/accounts/customer/cashouts/$id/confirm") { @@ -1314,16 +1298,7 @@ class CoreBankCashoutApiTest { @Test fun get() = bankSetup { _ -> // TODO auth routine - - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") val amountDebit = TalerAmount("KUDOS:1.5") val amountCredit = convert("KUDOS:1.5") @@ -1396,15 +1371,7 @@ class CoreBankCashoutApiTest { fun history() = bankSetup { _ -> // TODO auth routine - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") suspend fun HttpResponse.assertHistory(size: Int) { assertHistoryIds<Cashouts>(size) { @@ -1443,15 +1410,7 @@ class CoreBankCashoutApiTest { fun globalHistory() = bankSetup { _ -> // TODO admin auth routine - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") suspend fun HttpResponse.assertHistory(size: Int) { assertHistoryIds<GlobalCashouts>(size) { diff --git a/bank/src/test/kotlin/RevenueApiTest.kt b/bank/src/test/kotlin/RevenueApiTest.kt @@ -0,0 +1,109 @@ +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.client.HttpClient +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlinx.serialization.json.* +import kotlinx.coroutines.* +import org.junit.Test +import tech.libeufin.bank.* +import tech.libeufin.util.CryptoUtil +import net.taler.common.errorcodes.TalerErrorCode +import java.util.* +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import randHashCode + +class RevenueApiTest { + // GET /accounts/{USERNAME}/taler-revenue/history + @Test + fun history() = bankSetup { + setMaxDebt("exchange", TalerAmount("KUDOS:1000000")) + + suspend fun HttpResponse.assertHistory(size: Int) { + assertHistoryIds<MerchantIncomingHistory>(size) { + it.incoming_transactions.map { it.row_id } + } + } + + // TODO auth routine + + // Check error when no transactions + client.get("/accounts/merchant/taler-revenue/history?delta=7") { + basicAuth("merchant", "merchant-password") + }.assertNoContent() + + // Gen three transactions using clean transfer logic + repeat(3) { + transfer("KUDOS:10") + } + // Should not show up in the revenue API history + tx("exchange", "KUDOS:10", "merchant", "bogus") + // Merchant pays exchange once, but that should not appear in the result + tx("merchant", "KUDOS:10", "exchange", "ignored") + // Gen two transactions using raw bank transaction logic + repeat(2) { + tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) + } + + // Check ignore bogus subject + client.get("/accounts/merchant/taler-revenue/history?delta=7") { + basicAuth("merchant", "merchant-password") + }.assertHistory(5) + + // Check skip bogus subject + client.get("/accounts/merchant/taler-revenue/history?delta=5") { + basicAuth("merchant", "merchant-password") + }.assertHistory(5) + + // Check no useless polling + assertTime(0, 200) { + client.get("/accounts/merchant/taler-revenue/history?delta=-6&start=14&long_poll_ms=1000") { + basicAuth("merchant", "merchant-password") + }.assertHistory(5) + } + + // Check no polling when find transaction + assertTime(0, 200) { + client.get("/accounts/merchant/taler-revenue/history?delta=6&long_poll_ms=1000") { + basicAuth("merchant", "merchant-password") + }.assertHistory(5) + } + + coroutineScope { + launch { // Check polling succeed forward + assertTime(200, 300) { + client.get("/accounts/merchant/taler-revenue/history?delta=2&start=13&long_poll_ms=1000") { + basicAuth("merchant", "merchant-password") + }.assertHistory(1) + } + } + launch { // Check polling timeout forward + assertTime(200, 400) { + client.get("/accounts/merchant/taler-revenue/history?delta=1&start=15&long_poll_ms=300") { + basicAuth("merchant", "merchant-password") + }.assertNoContent() + } + } + delay(200) + transfer("KUDOS:10") + } + + // Testing ranges. + repeat(20) { + transfer("KUDOS:10") + } + + // forward range: + client.get("/accounts/merchant/taler-revenue/history?delta=10&start=20") { + basicAuth("merchant", "merchant-password") + }.assertHistory(10) + + // backward range: + client.get("/accounts/merchant/taler-revenue/history?delta=-10&start=25") { + basicAuth("merchant", "merchant-password") + }.assertHistory(10) + } +} +\ No newline at end of file diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt @@ -39,15 +39,7 @@ class StatsTest { setMaxDebt("merchant", TalerAmount("KUDOS:1000")) setMaxDebt("exchange", TalerAmount("KUDOS:1000")) setMaxDebt("customer", TalerAmount("KUDOS:1000")) - client.patch("/accounts/customer") { - basicAuth("customer", "customer-password") - jsonBody(json { - "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) - "challenge_contact_data" to json { - "phone" to "+99" - } - }) - }.assertNoContent() + fillCashoutInfo("customer") suspend fun cashin(amount: String) { db.conn { conn -> diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -165,6 +165,18 @@ suspend fun ApplicationTestBuilder.cashout(amount: String) { } } +suspend fun ApplicationTestBuilder.fillCashoutInfo(account: String) { + client.patch("/accounts/$account") { + basicAuth("$account", "$account-password") + jsonBody(json { + "cashout_payto_uri" to unknownPayto + "challenge_contact_data" to json { + "phone" to "+99" + } + }) + }.assertNoContent() +} + suspend fun ApplicationTestBuilder.convert(amount: String): TalerAmount { client.get("/cashout-rate?amount_debit=$amount").assertOk().run { return json<ConversionResponse>().amount_credit diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -220,6 +220,9 @@ CREATE TABLE IF NOT EXISTS taler_exchange_outgoing REFERENCES bank_account_transactions(bank_transaction_id) ON DELETE RESTRICT ON UPDATE RESTRICT + ,creditor_account_id BIGINT NOT NULL + REFERENCES bank_accounts(bank_account_id) + ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE TABLE IF NOT EXISTS taler_exchange_incoming diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -258,7 +258,10 @@ CREATE OR REPLACE PROCEDURE register_outgoing( IN in_request_uid BYTEA, IN in_wtid BYTEA, IN in_exchange_base_url TEXT, - IN in_tx_row_id BIGINT + IN in_debtor_account_id BIGINT, + IN in_creditor_account_id BIGINT, + IN in_debit_row_id BIGINT, + IN in_credit_row_id BIGINT ) LANGUAGE plpgsql AS $$ DECLARE @@ -271,21 +274,23 @@ INSERT request_uid, wtid, exchange_base_url, - bank_transaction + bank_transaction, + creditor_account_id ) VALUES ( in_request_uid, in_wtid, in_exchange_base_url, - in_tx_row_id + in_debit_row_id, + in_creditor_account_id ); -- TODO check if not drain -- update stats SELECT (amount).val, (amount).frac, bank_account_id INTO local_amount.val, local_amount.frac, local_bank_account_id -FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id; +FROM bank_account_transactions WHERE bank_transaction_id=in_debit_row_id; CALL stats_register_payment('taler_out', now()::TIMESTAMP, local_amount, null); -- notify new transaction -PERFORM pg_notify('outgoing_tx', local_bank_account_id || ' ' || in_tx_row_id); +PERFORM pg_notify('outgoing_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || in_debit_row_id || ' ' || in_credit_row_id); END $$; COMMENT ON PROCEDURE register_outgoing IS 'Register a bank transaction as a taler outgoing transaction'; @@ -344,6 +349,7 @@ LANGUAGE plpgsql AS $$ DECLARE exchange_bank_account_id BIGINT; receiver_bank_account_id BIGINT; +credit_row_id BIGINT; BEGIN -- Check for idempotence and conflict SELECT (amount != in_amount @@ -388,10 +394,10 @@ END IF; -- Perform bank transfer SELECT out_balance_insufficient, - out_debit_row_id + out_debit_row_id, out_credit_row_id INTO out_exchange_balance_insufficient, - out_tx_row_id + out_tx_row_id, credit_row_id FROM bank_wire_transfer( receiver_bank_account_id, exchange_bank_account_id, @@ -407,7 +413,7 @@ IF out_exchange_balance_insufficient THEN END IF; out_timestamp=in_timestamp; -- Register outgoing transaction -CALL register_outgoing(in_request_uid, in_wtid, in_exchange_base_url, out_tx_row_id); +CALL register_outgoing(in_request_uid, in_wtid, in_exchange_base_url, exchange_bank_account_id, receiver_bank_account_id, out_tx_row_id, credit_row_id); END $$; -- TODO new comment COMMENT ON FUNCTION taler_transfer IS 'function that (1) inserts the TWG requests'