gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (d7064c85 -> 640cc009)


From: gnunet
Subject: [libeufin] branch master updated (d7064c85 -> 640cc009)
Date: Fri, 13 Oct 2023 15:25:41 +0200

This is an automated email from the git hooks/post-receive script.

antoine pushed a change to branch master
in repository libeufin.

    from d7064c85 Improve and fix config logic
     new 15269b46 Move all transaction metadata logic in a single file
     new 640cc009 Improve wire gateway API

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../tech/libeufin/bank/CorebankApiHandlers.kt      |   9 +-
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 270 +++++++++++++++------
 .../src/main/kotlin/tech/libeufin/bank/Metadata.kt |  54 +++++
 .../main/kotlin/tech/libeufin/bank/TalerMessage.kt |  40 ---
 .../tech/libeufin/bank/WireGatewayApiHandlers.kt   | 115 +++++----
 bank/src/test/kotlin/TalerApiTest.kt               |  97 +++++---
 database-versioning/procedures.sql                 | 232 +++++++++++++-----
 7 files changed, 558 insertions(+), 259 deletions(-)
 create mode 100644 bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index ceff0cf6..7b457cf7 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -246,7 +246,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
             when (db.bankTransactionCreate(adminPaysBonus)) {
                 BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Bonus impossible: creditor not found, despite its recent 
creation.")
                 BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
-                BankTransactionResult.CONFLICT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
+                BankTransactionResult.BALANCE_INSUFFICIENT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
+                BankTransactionResult.SAME_ACCOUNT -> throw 
internalServerError("Bonus impossible: admin should not be creditor.")
                 BankTransactionResult.SUCCESS -> {/* continue the execution */
                 }
             }
@@ -546,10 +547,14 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         val res = db.bankTransactionCreate(dbInstructions)
         when (res) {
-            BankTransactionResult.CONFLICT -> throw conflict(
+            BankTransactionResult.BALANCE_INSUFFICIENT -> throw conflict(
                 "Insufficient funds",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
+            BankTransactionResult.SAME_ACCOUNT -> throw conflict(
+                "Wire transfer attempted with credit and debit party being the 
same bank account",
+                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+            )
             BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Creditor not found despite previous checks.")
             BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Debtor not found despite the request was authenticated.")
             BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 14cc1666..43483627 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -733,12 +733,13 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         conn.transaction {
             val stmt = conn.prepareStatement("""
                 SELECT 
-                    out_nx_creditor
-                    ,out_nx_debtor
-                    ,out_balance_insufficient
+                    out_same_account 
+                    ,out_debtor_not_found 
+                    ,out_creditor_not_found 
+                    ,out_balance_insufficient 
                     ,out_credit_row_id
                     ,out_debit_row_id
-                    ,out_creditor_is_exchange
+                    ,out_creditor_is_exchange 
                     ,out_debtor_is_exchange
                 FROM 
bank_wire_transfer(?,?,TEXT(?),(?,?)::taler_amount,?,TEXT(?),TEXT(?),TEXT(?))
             """
@@ -755,62 +756,53 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
             stmt.executeQuery().use {
                 when {
                     !it.next() -> throw internalServerError("Bank transaction 
didn't properly return")
-                    it.getBoolean("out_nx_debtor") -> {
+                    it.getBoolean("out_debtor_not_found") -> {
                         logger.error("No debtor account found")
                         BankTransactionResult.NO_DEBTOR
                     }
-                    it.getBoolean("out_nx_creditor") -> {
+                    it.getBoolean("out_creditor_not_found") -> {
                         logger.error("No creditor account found")
                         BankTransactionResult.NO_CREDITOR
                     }
+                    it.getBoolean("out_same_account") ->  
BankTransactionResult.SAME_ACCOUNT
                     it.getBoolean("out_balance_insufficient") -> {
                         logger.error("Balance insufficient")
-                        BankTransactionResult.CONFLICT
+                        BankTransactionResult.BALANCE_INSUFFICIENT
                     }
                     else -> {
+                        val metadata = TxMetadata.parse(tx.subject)
                         if (it.getBoolean("out_creditor_is_exchange")) {
-                            // Parse subject
-                            val reservePub = try {
-                                EddsaPublicKey(tx.subject)
-                            } catch (e: Exception) {
-                                null
-                            }
-                            if (reservePub != null) {
-                                val rowId = it.getLong("out_credit_row_id")
+                            val rowId = it.getLong("out_credit_row_id")
+                            if (metadata is IncomingTxMetadata) {
                                 val stmt = conn.prepareStatement("""
                                     INSERT INTO taler_exchange_incoming 
                                         (reserve_pub, bank_transaction) 
                                     VALUES (?, ?)
                                 """)
-                                stmt.setBytes(1, reservePub.raw)
+                                stmt.setBytes(1, metadata.reservePub.raw)
                                 stmt.setLong(2, rowId)
                                 stmt.executeUpdate()
                                 conn.execSQLUpdate("NOTIFY incoming_tx, 
'${"${tx.creditorAccountId} $rowId"}'")
                             } else {
                                 // TODO bounce
+                                logger.warn("exchange account 
${tx.creditorAccountId} received a transaction $rowId with malformed metadata, 
will bounce in future version")
                             }
-                        } else if (it.getBoolean("out_debtor_is_exchange")) {
-                            // Parse subject
-                            val metadata = try {
-                                val split = tx.subject.split(" ", limit=2) ;
-                                Pair(ShortHashCode(split[0]), split[2])
-                            } catch (e: Exception) {
-                                null
-                            }
-                            if (metadata != null) {
-                                val rowId = it.getLong("out_debit_row_id")
+                        }
+                        if (it.getBoolean("out_debtor_is_exchange")) {
+                            val rowId = it.getLong("out_debit_row_id")
+                            if (metadata is OutgoingTxMetadata) {
                                 val stmt = conn.prepareStatement("""
                                     INSERT INTO taler_exchange_outgoing 
                                         (wtid, exchange_base_url, 
bank_transaction) 
                                     VALUES (?, ?, ?)
                                 """)
-                                stmt.setBytes(1, metadata.first.raw)
-                                stmt.setString(2, metadata.second)
+                                stmt.setBytes(1, metadata.wtid.raw)
+                                stmt.setString(2, metadata.exchangeBaseUrl)
                                 stmt.setLong(3, rowId)
                                 stmt.executeUpdate()
                                 conn.execSQLUpdate("NOTIFY outgoing_tx, 
'${"${tx.debtorAccountId} $rowId"}'")
                             } else {
-                                // TODO log ?
+                                logger.warn("exchange account 
${tx.debtorAccountId} sent a transaction $rowId with malformed metadata")
                             }
                         }
                         BankTransactionResult.SUCCESS
@@ -893,7 +885,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
     private suspend fun <T> poolHistory(
         params: HistoryParams, 
         bankAccountId: Long,
-        listen: suspend NotificationWatcher.(Long, suspend 
(Flow<Notification>) -> Unit) -> Unit,
+        listen: suspend NotificationWatcher.(Long, suspend (Flow<Long>) -> 
Unit) -> Unit,
         query: String,
         map: (ResultSet) -> T
     ): List<T> {
@@ -949,7 +941,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
                 if (missing > 0) {
                     withTimeoutOrNull(params.poll_ms) {
                         buffered
-                            .filter { it.rowId > min } // Skip transactions 
already checked
+                            .filter { it > min } // Skip transactions already 
checked
                             .take(missing).count() // Wait for missing 
transactions
                     }
 
@@ -1244,9 +1236,9 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
     ): WithdrawalConfirmationResult = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
-              out_nx_op,
-              out_nx_exchange,
-              out_insufficient_funds,
+              out_no_op,
+              out_exchange_not_found,
+              out_balance_insufficient,
               out_already_confirmed_conflict
             FROM confirm_taler_withdrawal(?, ?, ?, ?, ?);
         """
@@ -1260,9 +1252,9 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
             when {
                 !it.next() ->
                     throw internalServerError("No result from DB procedure 
confirm_taler_withdrawal")
-                it.getBoolean("out_nx_op") -> 
WithdrawalConfirmationResult.OP_NOT_FOUND
-                it.getBoolean("out_nx_exchange") -> 
WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND
-                it.getBoolean("out_insufficient_funds") -> 
WithdrawalConfirmationResult.BALANCE_INSUFFICIENT
+                it.getBoolean("out_no_op") -> 
WithdrawalConfirmationResult.OP_NOT_FOUND
+                it.getBoolean("out_exchange_not_found") -> 
WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND
+                it.getBoolean("out_balance_insufficient") -> 
WithdrawalConfirmationResult.BALANCE_INSUFFICIENT
                 it.getBoolean("out_already_confirmed_conflict") -> 
WithdrawalConfirmationResult.CONFLICT
                 else -> WithdrawalConfirmationResult.SUCCESS
             }
@@ -1479,7 +1471,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         val txRowId: Long? = null,
         val timestamp: TalerProtocolTimestamp? = null
     )
-    /**
+    /** TODO better doc
      * This function calls the SQL function that (1) inserts the TWG
      * requests details into the database and (2) performs the actual
      * bank transaction to pay the merchant according to the 'req' parameter.
@@ -1498,29 +1490,23 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         pmtInfId: String = "not used",
         endToEndId: String = "not used",
         ): TalerTransferCreationResult = conn { conn ->
-        val subject = "${req.wtid.encoded()} ${req.exchange_base_url}"
+        val subject = OutgoingTxMetadata(req.wtid, 
req.exchange_base_url).toString()
         val stmt = conn.prepareStatement("""
             SELECT
-              out_exchange_balance_insufficient
-              ,out_nx_debitor
-              ,out_nx_exchange
-              ,out_nx_creditor
-              ,out_tx_row_id
-              ,out_request_uid_reuse
-              ,out_timestamp
+                out_debtor_not_found
+                ,out_debtor_not_exchange
+                ,out_creditor_not_found
+                ,out_same_account
+                ,out_both_exchanges
+                ,out_request_uid_reuse
+                ,out_exchange_balance_insufficient
+                ,out_tx_row_id
+                ,out_timestamp
               FROM
               taler_transfer (
-                  ?,
-                  ?,
-                  ?,
+                  ?, ?, ?,
                   (?,?)::taler_amount,
-                  ?,
-                  ?,
-                  ?,
-                  ?,
-                  ?,
-                  ?,
-                  ?
+                  ?, ?, ?, ?, ?, ?, ?
                 );
         """)
 
@@ -1541,16 +1527,20 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
             when {
                 !it.next() ->
                     throw internalServerError("SQL function taler_transfer did 
not return anything.")
-                it.getBoolean("out_nx_debitor") ->
+                it.getBoolean("out_debtor_not_found") ->
                     TalerTransferCreationResult(TalerTransferResult.NO_DEBITOR)
-                it.getBoolean("out_nx_exchange") ->
+                it.getBoolean("out_debtor_not_exchange") ->
                     
TalerTransferCreationResult(TalerTransferResult.NOT_EXCHANGE)
-                it.getBoolean("out_request_uid_reuse") ->
-                    
TalerTransferCreationResult(TalerTransferResult.REQUEST_UID_REUSE)
+                it.getBoolean("out_creditor_not_found") ->
+                    
TalerTransferCreationResult(TalerTransferResult.NO_CREDITOR)
+                it.getBoolean("out_same_account") ->
+                    
TalerTransferCreationResult(TalerTransferResult.SAME_ACCOUNT)
+                it.getBoolean("out_both_exchanges") ->
+                    
TalerTransferCreationResult(TalerTransferResult.BOTH_EXCHANGE)
                 it.getBoolean("out_exchange_balance_insufficient") ->
                     
TalerTransferCreationResult(TalerTransferResult.BALANCE_INSUFFICIENT)
-                it.getBoolean("out_nx_creditor") ->
-                    
TalerTransferCreationResult(TalerTransferResult.NO_CREDITOR)
+                it.getBoolean("out_request_uid_reuse") ->
+                    
TalerTransferCreationResult(TalerTransferResult.REQUEST_UID_REUSE)
                 else -> {
                     TalerTransferCreationResult(
                         txResult = TalerTransferResult.SUCCESS,
@@ -1563,21 +1553,153 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
             }
         }
     }
+
+    data class TalerAddIncomingCreationResult(
+        val txResult: TalerAddIncomingResult,
+        val txRowId: Long? = null
+    )
+
+     /** TODO better doc
+     * This function calls the SQL function that (1) inserts the TWG
+     * requests details into the database and (2) performs the actual
+     * bank transaction to pay the merchant according to the 'req' parameter.
+     *
+     * 'req' contains the same data that was POSTed by the exchange
+     * to the TWG /transfer endpoint.  The exchangeBankAccountId parameter
+     * is the row ID of the exchange's bank account.  The return type
+     * is the same returned by "bank_wire_transfer()" where however
+     * the NO_DEBTOR error will hardly take place.
+     */
+    suspend fun talerAddIncomingCreate(
+        req: AddIncomingRequest,
+        username: String,
+        timestamp: Instant,
+        acctSvcrRef: String = "not used",
+        pmtInfId: String = "not used",
+        endToEndId: String = "not used",
+        ): TalerAddIncomingCreationResult = conn { conn ->
+        val subject = IncomingTxMetadata(req.reserve_pub).toString()
+        val stmt = conn.prepareStatement("""
+            SELECT
+                out_creditor_not_found
+                ,out_creditor_not_exchange
+                ,out_debtor_not_found
+                ,out_same_account
+                ,out_both_exchanges
+                ,out_reserve_pub_reuse
+                ,out_debitor_balance_insufficient
+                ,out_tx_row_id
+              FROM
+              taler_add_incoming (
+                  ?, ?,
+                  (?,?)::taler_amount,
+                  ?, ?, ?, ?, ?, ?
+                );
+        """)
+
+        stmt.setBytes(1, req.reserve_pub.raw)
+        stmt.setString(2, subject)
+        stmt.setLong(3, req.amount.value)
+        stmt.setInt(4, req.amount.frac)
+        stmt.setString(5, stripIbanPayto(req.debit_account) ?: throw 
badRequest("debit_account payto URI is invalid"))
+        stmt.setString(6, username)
+        stmt.setLong(7, timestamp.toDbMicros() ?: throw 
faultyTimestampByBank())
+        stmt.setString(8, acctSvcrRef)
+        stmt.setString(9, pmtInfId)
+        stmt.setString(10, endToEndId)
+
+        stmt.executeQuery().use {
+            when {
+                !it.next() ->
+                    throw internalServerError("SQL function taler_add_incoming 
did not return anything.")
+                it.getBoolean("out_creditor_not_found") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.NO_CREDITOR)
+                it.getBoolean("out_creditor_not_exchange") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.NOT_EXCHANGE)
+                it.getBoolean("out_debtor_not_found") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.NO_DEBITOR)
+                it.getBoolean("out_same_account") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.SAME_ACCOUNT)
+                it.getBoolean("out_both_exchanges") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.BOTH_EXCHANGE)
+                it.getBoolean("out_debitor_balance_insufficient") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.BALANCE_INSUFFICIENT)
+                it.getBoolean("out_reserve_pub_reuse") ->
+                    
TalerAddIncomingCreationResult(TalerAddIncomingResult.RESERVE_PUB_REUSE)
+                else -> {
+                    TalerAddIncomingCreationResult(
+                        txResult = TalerAddIncomingResult.SUCCESS,
+                        txRowId = it.getLong("out_tx_row_id")
+                    )
+                }
+            }
+        }
+    }
+}
+
+/** Result status of customer account deletion */
+enum class CustomerDeletionResult {
+    SUCCESS,
+    CUSTOMER_NOT_FOUND,
+    BALANCE_NOT_ZERO
+}
+
+/** Result status of bank transaction creation .*/
+enum class BankTransactionResult {
+    NO_CREDITOR,
+    NO_DEBTOR,
+    SAME_ACCOUNT,
+    BALANCE_INSUFFICIENT,
+    SUCCESS,
 }
 
+/** Result status of taler transfer transaction */
 enum class TalerTransferResult {
     NO_DEBITOR,
     NOT_EXCHANGE,
     NO_CREDITOR,
+    SAME_ACCOUNT,
+    BOTH_EXCHANGE,
     REQUEST_UID_REUSE,
     BALANCE_INSUFFICIENT,
     SUCCESS
 }
 
-private data class Notification(val rowId: Long)
+/** Result status of taler add incoming transaction */
+enum class TalerAddIncomingResult {
+    NO_DEBITOR,
+    NOT_EXCHANGE,
+    NO_CREDITOR,
+    SAME_ACCOUNT,
+    BOTH_EXCHANGE,
+    RESERVE_PUB_REUSE,
+    BALANCE_INSUFFICIENT,
+    SUCCESS
+}
+
+/**
+ * This type communicates the result of a database operation
+ * to confirm one withdrawal operation.
+ */
+enum class WithdrawalConfirmationResult {
+    SUCCESS,
+    OP_NOT_FOUND,
+    EXCHANGE_NOT_FOUND,
+    BALANCE_INSUFFICIENT,
+
+    /**
+     * This state indicates that the withdrawal was already
+     * confirmed BUT Kotlin did not detect it and still invoked
+     * the SQL procedure to confirm the withdrawal.  This is
+     * conflictual because only Kotlin is responsible to check
+     * for idempotency, and this state witnesses a failure in
+     * this regard.
+     */
+    CONFLICT
+}
 
 private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
-    private class CountedSharedFlow(val flow: MutableSharedFlow<Notification>, 
var count: Int)
+    private class CountedSharedFlow(val flow: MutableSharedFlow<Long>, var 
count: Int)
 
     private val bankTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>()
     private val outgoingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>()
@@ -1604,12 +1726,12 @@ private class NotificationWatcher(private val pgSource: 
PGSimpleDataSource) {
                                     val creditRow = info[3];
                                     
                                     bankTxFlows[debtorAccount]?.run {
-                                        flow.emit(Notification(debitRow))
-                                        flow.emit(Notification(creditRow))
+                                        flow.emit(debitRow)
+                                        flow.emit(creditRow)
                                     }
                                     bankTxFlows[creditorAccount]?.run {
-                                        flow.emit(Notification(debitRow))
-                                        flow.emit(Notification(creditRow))
+                                        flow.emit(debitRow)
+                                        flow.emit(creditRow)
                                     }
                                 } else {
                                     val info = it.parameter.split(' ', limit = 
2).map { it.toLong() }
@@ -1617,11 +1739,11 @@ private class NotificationWatcher(private val pgSource: 
PGSimpleDataSource) {
                                     val row = info[1];
                                     if (it.name == "outgoing_tx") {
                                         outgoingTxFlows[account]?.run {
-                                            flow.emit(Notification(row))
+                                            flow.emit(row)
                                         }
                                     } else {
                                         incomingTxFlows[account]?.run {
-                                            flow.emit(Notification(row))
+                                            flow.emit(row)
                                         }
                                     }
                                 }
@@ -1635,7 +1757,7 @@ private class NotificationWatcher(private val pgSource: 
PGSimpleDataSource) {
         }
     }
 
-    private suspend fun listen(map: ConcurrentHashMap<Long, 
CountedSharedFlow>, account: Long, lambda: suspend (Flow<Notification>) -> 
Unit) {
+    private suspend fun listen(map: ConcurrentHashMap<Long, 
CountedSharedFlow>, account: Long, lambda: suspend (Flow<Long>) -> Unit) {
         // Register listener
         val flow = map.compute(account) { _, v ->
             val tmp = v ?: CountedSharedFlow(MutableSharedFlow(), 0);
@@ -1655,15 +1777,15 @@ private class NotificationWatcher(private val pgSource: 
PGSimpleDataSource) {
         }
     } 
 
-    suspend fun listenBank(account: Long, lambda: suspend (Flow<Notification>) 
-> Unit) {
+    suspend fun listenBank(account: Long, lambda: suspend (Flow<Long>) -> 
Unit) {
         listen(bankTxFlows, account, lambda)
     }
 
-    suspend fun listenOutgoing(account: Long, lambda: suspend 
(Flow<Notification>) -> Unit) {
+    suspend fun listenOutgoing(account: Long, lambda: suspend (Flow<Long>) -> 
Unit) {
         listen(outgoingTxFlows, account, lambda)
     }
 
-    suspend fun listenIncoming(account: Long, lambda: suspend 
(Flow<Notification>) -> Unit) {
+    suspend fun listenIncoming(account: Long, lambda: suspend (Flow<Long>) -> 
Unit) {
         listen(incomingTxFlows, account, lambda)
     }
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt
new file mode 100644
index 00000000..119de048
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt
@@ -0,0 +1,54 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * 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
+
+sealed interface TxMetadata {
+    // TODO versioning ?
+    companion object {
+        fun parse(subject: String): TxMetadata? {
+            // IncomingTxMetadata
+            try {
+                return IncomingTxMetadata(EddsaPublicKey(subject))
+            } catch (e: Exception) { }
+
+            // OutgoingTxMetadata
+            try {
+                val (wtid, exchangeBaseUrl) = subject.split(" ", limit=2) ; 
+                return OutgoingTxMetadata(ShortHashCode(wtid), exchangeBaseUrl)
+            } catch (e: Exception) { }
+
+            // No well formed metadata
+            return null
+        }
+
+        fun encode(metadata: TxMetadata): String {
+            return when (metadata) {
+                is IncomingTxMetadata -> "${metadata.reservePub}"
+                is OutgoingTxMetadata -> "${metadata.wtid} 
${metadata.exchangeBaseUrl}"
+            }
+        }
+    }
+}
+
+data class IncomingTxMetadata(val reservePub: EddsaPublicKey): TxMetadata {
+    override fun toString(): String = TxMetadata.encode(this)
+}
+data class OutgoingTxMetadata(val wtid: ShortHashCode, val exchangeBaseUrl: 
String): TxMetadata {
+    override fun toString(): String = TxMetadata.encode(this)
+}
\ 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
index 6f77c49a..b70e5242 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -426,46 +426,6 @@ fun ResourceName.canI(c: Customer, withAdmin: Boolean = 
true): Boolean {
 fun ApplicationCall.getResourceName(param: String): ResourceName =
     this.expectUriComponent(param)
 
-/**
- * This type communicates the result of deleting an account
- * from the database.
- */
-enum class CustomerDeletionResult {
-    SUCCESS,
-    CUSTOMER_NOT_FOUND,
-    BALANCE_NOT_ZERO
-}
-
-/**
- * This type communicates the result of a database operation
- * to confirm one withdrawal operation.
- */
-enum class WithdrawalConfirmationResult {
-    SUCCESS,
-    OP_NOT_FOUND,
-    EXCHANGE_NOT_FOUND,
-    BALANCE_INSUFFICIENT,
-
-    /**
-     * This state indicates that the withdrawal was already
-     * confirmed BUT Kotlin did not detect it and still invoked
-     * the SQL procedure to confirm the withdrawal.  This is
-     * conflictual because only Kotlin is responsible to check
-     * for idempotency, and this state witnesses a failure in
-     * this regard.
-     */
-    CONFLICT
-}
-
-/**
- * Communicates the result of creating a bank transaction in the database.
- */
-enum class BankTransactionResult {
-    NO_CREDITOR,
-    NO_DEBTOR,
-    SUCCESS,
-    CONFLICT // balance insufficient
-}
 
 // GET /config response from the Taler Integration API.
 @Serializable
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 7dfbe1c5..653ef732 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -39,9 +39,9 @@ private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.nexus")
 fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext) {
     /** Authenticate and check access rights */
     suspend fun ApplicationCall.authCheck(scope: TokenScope, withAdmin: 
Boolean): String {
-        val authCustomer = authenticateBankRequest(db, scope) ?: throw 
unauthorized()
+        val authCustomer = authenticateBankRequest(db, scope) ?: throw 
unauthorized("Bad login")
         val username = getResourceName("USERNAME")
-        if (!username.canI(authCustomer, withAdmin)) throw forbidden()
+        if (!username.canI(authCustomer, withAdmin)) throw unauthorized("No 
right on $username account")
         return username
     }
 
@@ -73,20 +73,29 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
             timestamp = Instant.now()
         )
         when (dbRes.txResult) {
-            TalerTransferResult.NO_DEBITOR -> 
-                throw notFound(
-                    hint = "Customer $username not found",
-                    talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC.
-                )
-            TalerTransferResult.NOT_EXCHANGE -> 
-                throw forbidden("$username is not an exchange account.")
+            TalerTransferResult.NO_DEBITOR -> throw notFound(
+                "Customer $username not found",
+                TalerErrorCode.TALER_EC_END // FIXME: need EC.
+            )
+            TalerTransferResult.NOT_EXCHANGE -> throw conflict(
+                "$username is not an exchange account.",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
             TalerTransferResult.NO_CREDITOR -> throw notFound(
                 "Creditor account was not found",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
+            TalerTransferResult.SAME_ACCOUNT -> throw conflict(
+                "Wire transfer attempted with credit and debit party being the 
same bank account",
+                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+            )
+            TalerTransferResult.BOTH_EXCHANGE -> throw conflict(
+                "Wire transfer attempted with credit and debit party being 
both exchange account",
+                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+            )
             TalerTransferResult.REQUEST_UID_REUSE -> throw conflict(
-                hint = "request_uid used already",
-                talerEc = 
TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
+                "request_uid used already",
+                TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
             )
             TalerTransferResult.BALANCE_INSUFFICIENT -> throw conflict(
                 "Insufficient balance for exchange",
@@ -106,10 +115,15 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
         reduce: (List<T>, String) -> Any, 
         dbLambda: suspend Database.(HistoryParams, Long) -> List<T>
     ) {
-        call.authCheck(TokenScope.readonly, true)
+        val username = call.authCheck(TokenScope.readonly, true)
         val params = getHistoryParams(call.request.queryParameters)
         val bankAccount = call.bankAccount()
-        if (!bankAccount.isTalerExchange) throw forbidden("History is not 
related to a Taler exchange.")
+        
+        if (!bankAccount.isTalerExchange)
+            throw conflict(
+                "$username is not an exchange account.",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
 
         val items = db.dbLambda(params, bankAccount.id);
     
@@ -129,55 +143,54 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
     }
 
     post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
-        call.authCheck(TokenScope.readwrite, false);
+        val username = call.authCheck(TokenScope.readwrite, false)
         val req = call.receive<AddIncomingRequest>()
         if (req.amount.currency != ctx.currency)
             throw badRequest(
                 "Currency mismatch",
                 TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
             )
-        
-        // TODO check conflict in transaction
-        if (db.bankTransactionCheckExists(req.reserve_pub.encoded()) != null)
-            throw conflict(
-                "Reserve pub. already used",
-                TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+        val timestamp = Instant.now()
+        val dbRes = db.talerAddIncomingCreate(
+            req = req,
+            username = username,
+            timestamp = timestamp
+        )
+        when (dbRes.txResult) {
+            TalerAddIncomingResult.NO_CREDITOR -> throw notFound(
+                "Customer $username not found",
+                TalerErrorCode.TALER_EC_END // FIXME: need EC.
             )
-        val strippedIbanPayto: String = stripIbanPayto(req.debit_account) ?: 
throw badRequest("Invalid debit_account payto URI")
-        val walletAccount = 
db.bankAccountGetFromInternalPayto(strippedIbanPayto)
-            ?: throw notFound(
-                "debit_account not found",
+            TalerAddIncomingResult.NOT_EXCHANGE -> throw conflict(
+                "$username is not an exchange account.",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
-        val exchangeAccount = call.bankAccount()
-        if (!exchangeAccount.isTalerExchange) throw forbidden("Expected taler 
exchange bank account.")
-
-        val txTimestamp = Instant.now()
-        val op = BankInternalTransaction(
-            debtorAccountId = walletAccount.expectRowId(),
-            amount = req.amount,
-            creditorAccountId = exchangeAccount.id,
-            transactionDate = txTimestamp,
-            subject = req.reserve_pub.encoded()
-        )
-        val res = db.bankTransactionCreate(op)
-        /**
-         * Other possible errors are highly unlikely, because of the
-         * previous checks on the existence of the involved bank accounts.
-         */
-        if (res == BankTransactionResult.CONFLICT)
-            throw conflict(
-                "Insufficient balance",
+            TalerAddIncomingResult.NO_DEBITOR -> throw notFound(
+                "Debitor account was not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
+            TalerAddIncomingResult.SAME_ACCOUNT -> throw conflict(
+                "Wire transfer attempted with credit and debit party being the 
same bank account",
+                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+            )
+            TalerAddIncomingResult.BOTH_EXCHANGE -> throw conflict(
+                "Wire transfer attempted with credit and debit party being 
both exchange account",
+                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+            )
+            TalerAddIncomingResult.RESERVE_PUB_REUSE -> throw conflict(
+                "reserve_pub used already",
+                TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+            )
+            TalerAddIncomingResult.BALANCE_INSUFFICIENT -> throw conflict(
+                "Insufficient balance for debitor",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
-        val rowId = db.bankTransactionCheckExists(req.reserve_pub.encoded())
-            ?: throw internalServerError("Could not find the just inserted 
bank transaction")
-        call.respond(
-            AddIncomingResponse(
-                row_id = rowId,
-                timestamp = TalerProtocolTimestamp(txTimestamp)
+            TalerAddIncomingResult.SUCCESS -> call.respond(
+                AddIncomingResponse(
+                    timestamp = TalerProtocolTimestamp(timestamp),
+                    row_id = dbRes.txRowId!!
+                )
             )
-        )
-        return@post
+        }
     }
 }
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index 0cdd450a..0684f3c0 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,8 +4,7 @@ import io.ktor.client.statement.*
 import io.ktor.client.HttpClient
 import io.ktor.http.*
 import io.ktor.server.testing.*
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.*
 import kotlinx.coroutines.*
 import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
@@ -74,7 +73,7 @@ class TalerApiTest {
             BankInternalTransaction(
                 creditorAccountId = from,
                 debtorAccountId = to,
-                subject = randShortHashCode().encoded(),
+                subject = IncomingTxMetadata(randShortHashCode()).toString(),
                 amount = TalerAmount( 10, 0, "KUDOS"),
                 accountServicerReference = "acct-svcr-ref",
                 endToEndId = "end-to-end-id",
@@ -96,8 +95,8 @@ class TalerApiTest {
     }
 
     // Test endpoint is correctly authenticated 
-    suspend fun authRoutine(client: HttpClient, path: String, method: 
HttpMethod = HttpMethod.Post) {
-        // No body because authentication must happen before parsing the body
+    suspend fun authRoutine(client: HttpClient, path: String, body: 
JsonObject? = null, method: HttpMethod = HttpMethod.Post) {
+        // No body when authentication must happen before parsing the body
         
         // Unknown account
         client.request(path) {
@@ -115,7 +114,14 @@ class TalerApiTest {
         client.request(path) {
             this.method = method
             basicAuth("bar", "secret")
-        }.assertStatus(HttpStatusCode.Forbidden)
+        }.assertStatus(HttpStatusCode.Unauthorized)
+
+        // Not exchange account
+        client.request(path) {
+            this.method = method
+            if (body != null) jsonBody(body)
+            basicAuth("foo", "pw")
+        }.assertStatus(HttpStatusCode.Conflict)
     }
 
     // Testing the POST /transfer call from the TWG API.
@@ -127,23 +133,15 @@ class TalerApiTest {
                 corebankWebApp(db, ctx)
             }
 
-            // TODO what to do when creditor and debtor are both exchanges
-
-            authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer")
-
             val valid_req = json {
                 "request_uid" to randHashCode()
                 "amount" to "KUDOS:55"
                 "exchange_base_url" to "http://exchange.example.com/";
                 "wtid" to randShortHashCode()
-                "credit_account" to 
"${stripIbanPayto(bankAccountFoo.internalPaytoUri)}"
+                "credit_account" to 
stripIbanPayto(bankAccountFoo.internalPaytoUri)
             };
 
-            // Checking exchange account constraint.
-            client.post("/accounts/foo/taler-wire-gateway/transfer") {
-                basicAuth("foo", "pw")
-                jsonBody(valid_req)
-            }.assertStatus(HttpStatusCode.Forbidden)
+            authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer", 
valid_req)
 
             // Checking exchange debt constraint.
             client.post("/accounts/bar/taler-wire-gateway/transfer") {
@@ -178,19 +176,17 @@ class TalerApiTest {
                 )
             }.assertStatus(HttpStatusCode.Conflict)
 
-            // Triggering currency mismatch
+            // Currency mismatch
             client.post("/accounts/bar/taler-wire-gateway/transfer") {
                 basicAuth("bar", "secret")
                 jsonBody(
-                    json(valid_req) { 
-                        "request_uid" to randHashCode()
-                        "wtid" to randShortHashCode()
+                    json(valid_req) {
                         "amount" to "EUR:33"
                     }
                 )
             }.assertBadRequest()
 
-            // Unknown account currency mismatch
+            // Unknown account
             client.post("/accounts/bar/taler-wire-gateway/transfer") {
                 basicAuth("bar", "secret")
                 jsonBody(
@@ -286,7 +282,7 @@ class TalerApiTest {
                 corebankWebApp(db, ctx)
             }
 
-            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/history/incoming", HttpMethod.Get)
+            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/history/incoming?delta=7", method = 
HttpMethod.Get)
 
             // Check error when no transactions
             
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") {
@@ -425,7 +421,7 @@ class TalerApiTest {
                 corebankWebApp(db, ctx)
             }
 
-            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/history/outgoing", HttpMethod.Get)
+            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/history/outgoing?delta=7", method = 
HttpMethod.Get)
 
             // Check error when no transactions
             
client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") {
@@ -524,34 +520,63 @@ class TalerApiTest {
     // Testing the /admin/add-incoming call from the TWG API.
     @Test
     fun addIncoming() = commonSetup { db, ctx -> 
-        // Give Bar reasonable debt allowance:
-        assert(db.bankAccountSetMaxDebt(
-            2L,
-            TalerAmount(1000, 0, "KUDOS")
-        ))
         testApplication {
             application {
                 corebankWebApp(db, ctx)
             }
 
-            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/admin/add-incoming")
-
             val valid_req = json {
                 "amount" to "KUDOS:44"
                 "reserve_pub" to randEddsaPublicKey()
-                "debit_account" to "${"payto://iban/BAR-IBAN-ABC"}"
+                "debit_account" to bankAccountFoo.internalPaytoUri
             };
-            
-            client.post("/accounts/foo/taler-wire-gateway/admin/add-incoming") 
{
-                basicAuth("foo", "pw")
-                jsonBody(valid_req, deflate = true)
-            }.assertStatus(HttpStatusCode.Forbidden)
+
+            authRoutine(client, 
"/accounts/foo/taler-wire-gateway/admin/add-incoming", valid_req)
+
+            // Checking exchange debt constraint.
+            client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") 
{
+                basicAuth("bar", "secret")
+                jsonBody(valid_req)
+            }.assertStatus(HttpStatusCode.Conflict)
+
+            // Giving debt allowance and checking the OK case.
+            assert(db.bankAccountSetMaxDebt(
+                1L,
+                TalerAmount(1000, 0, "KUDOS")
+            ))
 
             client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") 
{
                 basicAuth("bar", "secret")
                 jsonBody(valid_req, deflate = true)
             }.assertOk()
 
+            // Trigger conflict due to reused reserve_pub
+             
client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
+                basicAuth("bar", "secret")
+                jsonBody(valid_req)
+            }.assertStatus(HttpStatusCode.Conflict)
+
+            // Currency mismatch
+            client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") 
{
+                basicAuth("bar", "secret")
+                jsonBody(
+                    json(valid_req) {
+                        "amount" to "EUR:33"
+                    }
+                )
+            }.assertBadRequest()
+
+            // Unknown account
+            client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") 
{
+                basicAuth("bar", "secret")
+                jsonBody(
+                    json(valid_req) { 
+                        "reserve_pub" to randEddsaPublicKey()
+                        "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+                    }
+                )
+            }.assertStatus(HttpStatusCode.NotFound)
+
             // Bad BASE32 reserve_pub
             client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") 
{
                 basicAuth("bar", "secret")
diff --git a/database-versioning/procedures.sql 
b/database-versioning/procedures.sql
index afcdbdec..4c41bb16 100644
--- a/database-versioning/procedures.sql
+++ b/database-versioning/procedures.sql
@@ -206,11 +206,15 @@ CREATE OR REPLACE FUNCTION taler_transfer(
   IN in_account_servicer_reference TEXT,
   IN in_payment_information_id TEXT,
   IN in_end_to_end_id TEXT,
+  -- Error status
+  OUT out_debtor_not_found BOOLEAN,
+  OUT out_debtor_not_exchange BOOLEAN,
+  OUT out_creditor_not_found BOOLEAN,
+  OUT out_same_account BOOLEAN,
+  OUT out_both_exchanges BOOLEAN,
   OUT out_request_uid_reuse BOOLEAN,
   OUT out_exchange_balance_insufficient BOOLEAN,
-  OUT out_nx_debitor BOOLEAN,
-  OUT out_nx_exchange BOOLEAN,
-  OUT out_nx_creditor BOOLEAN,
+  -- Success return
   OUT out_tx_row_id BIGINT,
   OUT out_timestamp BIGINT
 )
@@ -237,37 +241,38 @@ END IF;
 -- Find exchange bank account id
 SELECT
   bank_account_id, NOT is_taler_exchange
-  INTO exchange_bank_account_id, out_nx_exchange
+  INTO exchange_bank_account_id, out_debtor_not_exchange
   FROM bank_accounts 
       JOIN customers 
         ON customer_id=owning_customer_id
   WHERE login = in_username;
 IF NOT FOUND THEN
-  out_nx_debitor=TRUE;
+  out_debtor_not_found=TRUE;
   RETURN;
-ELSIF out_nx_exchange THEN
+ELSIF out_debtor_not_exchange THEN
   RETURN;
 END IF;
--- Find receiver bank account id 
--- TODO handle bounce when receiver is exchange ?
--- TODO handle transfer to self ?
+-- Find receiver bank account id
 SELECT
-  bank_account_id
-  INTO receiver_bank_account_id
+  bank_account_id, is_taler_exchange
+  INTO receiver_bank_account_id, out_both_exchanges
   FROM bank_accounts
   WHERE internal_payto_uri = in_credit_account_payto;
-IF NOT FOUND
-THEN
-  out_nx_creditor=TRUE;
+IF NOT FOUND THEN
+  out_creditor_not_found=TRUE;
+  RETURN;
+ELSIF out_both_exchanges THEN
   RETURN;
 END IF;
 -- Perform bank transfer
 SELECT
   out_balance_insufficient,
-  out_debit_row_id
+  out_debit_row_id,
+  transfer.out_same_account
   INTO
     out_exchange_balance_insufficient,
-    out_tx_row_id
+    out_tx_row_id,
+    out_same_account
   FROM bank_wire_transfer(
     receiver_bank_account_id,
     exchange_bank_account_id,
@@ -277,7 +282,7 @@ SELECT
     in_account_servicer_reference,
     in_payment_information_id,
     in_end_to_end_id
-  );
+  ) as transfer;
 IF out_exchange_balance_insufficient THEN
   RETURN;
 END IF;
@@ -310,10 +315,124 @@ COMMENT ON FUNCTION taler_transfer(
   text,
   text,
   text
-  )
+  )-- TODO new comment
+  IS 'function that (1) inserts the TWG requests'
+     'details into the database and (2) performs '
+     'the actual bank transaction to pay the merchant';
+
+
+CREATE OR REPLACE FUNCTION taler_add_incoming(
+  IN in_reserve_pub BYTEA,
+  IN in_subject TEXT,
+  IN in_amount taler_amount,
+  IN in_debit_account_payto TEXT,
+  IN in_username TEXT,
+  IN in_timestamp BIGINT,
+  IN in_account_servicer_reference TEXT,
+  IN in_payment_information_id TEXT,
+  IN in_end_to_end_id TEXT,
+  -- Error status
+  OUT out_creditor_not_found BOOLEAN,
+  OUT out_creditor_not_exchange BOOLEAN,
+  OUT out_debtor_not_found BOOLEAN,
+  OUT out_same_account BOOLEAN,
+  OUT out_both_exchanges BOOLEAN,
+  OUT out_reserve_pub_reuse BOOLEAN,
+  OUT out_debitor_balance_insufficient BOOLEAN,
+  -- Success return
+  OUT out_tx_row_id BIGINT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+exchange_bank_account_id BIGINT;
+sender_bank_account_id BIGINT;
+BEGIN
+-- Check for idempotence and conflict
+SELECT true
+  FROM taler_exchange_incoming
+      JOIN bank_account_transactions AS txs
+        ON bank_transaction=txs.bank_transaction_id 
+  WHERE reserve_pub = in_reserve_pub
+  INTO out_reserve_pub_reuse;
+IF out_reserve_pub_reuse THEN
+  RETURN;
+END IF;
+-- Find exchange bank account id
+SELECT
+  bank_account_id, NOT is_taler_exchange
+  INTO exchange_bank_account_id, out_creditor_not_exchange
+  FROM bank_accounts 
+      JOIN customers 
+        ON customer_id=owning_customer_id
+  WHERE login = in_username;
+IF NOT FOUND THEN
+  out_creditor_not_found=TRUE;
+  RETURN;
+ELSIF out_creditor_not_exchange THEN
+  RETURN;
+END IF;
+-- Find sender bank account id
+SELECT
+  bank_account_id, is_taler_exchange
+  INTO sender_bank_account_id, out_both_exchanges
+  FROM bank_accounts
+  WHERE internal_payto_uri = in_debit_account_payto;
+IF NOT FOUND THEN
+  out_debtor_not_found=TRUE;
+  RETURN;
+ELSIF out_both_exchanges THEN
+  RETURN;
+END IF;
+-- Perform bank transfer
+SELECT
+  out_balance_insufficient,
+  out_debit_row_id,
+  transfer.out_same_account
+  INTO
+    out_debitor_balance_insufficient,
+    out_tx_row_id,
+    out_same_account
+  FROM bank_wire_transfer(
+    exchange_bank_account_id,
+    sender_bank_account_id,
+    in_subject,
+    in_amount,
+    in_timestamp,
+    in_account_servicer_reference,
+    in_payment_information_id,
+    in_end_to_end_id
+  ) as transfer;
+IF out_debitor_balance_insufficient THEN
+  RETURN;
+END IF;
+-- Register incoming transaction
+INSERT
+  INTO taler_exchange_incoming (
+    reserve_pub,
+    bank_transaction
+) VALUES (
+  in_reserve_pub,
+  out_tx_row_id
+);
+-- notify new transaction
+PERFORM pg_notify('incoming_tx', exchange_bank_account_id || ' ' || 
out_tx_row_id);
+END $$;
+COMMENT ON FUNCTION taler_add_incoming(
+  bytea,
+  text,
+  taler_amount,
+  text,
+  text,
+  bigint,
+  text,
+  text,
+  text
+  ) -- TODO new comment
   IS 'function that (1) inserts the TWG requests'
      'details into the database and (2) performs '
      'the actual bank transaction to pay the merchant';
+  
 
 CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
   IN in_withdrawal_uuid uuid,
@@ -321,12 +440,9 @@ CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
   IN in_acct_svcr_ref TEXT,
   IN in_pmt_inf_id TEXT,
   IN in_end_to_end_id TEXT,
-  OUT out_nx_op BOOLEAN,
-  -- can't use out_balance_insufficient, because
-  -- it conflicts with the return column of the called
-  -- function that moves the funds.  FIXME?
-  OUT out_insufficient_funds BOOLEAN,
-  OUT out_nx_exchange BOOLEAN,
+  OUT out_no_op BOOLEAN,
+  OUT out_balance_insufficient BOOLEAN,
+  OUT out_exchange_not_found BOOLEAN,
   OUT out_already_confirmed_conflict BOOLEAN
 )
 LANGUAGE plpgsql
@@ -356,10 +472,10 @@ SELECT -- Really no-star policy and instead DECLARE 
almost one var per column?
   WHERE withdrawal_uuid=in_withdrawal_uuid;
 IF NOT FOUND
 THEN
-  out_nx_op=TRUE;
+  out_no_op=TRUE;
   RETURN;
 END IF;
-out_nx_op=FALSE;
+out_no_op=FALSE;
 IF (confirmation_done_local)
 THEN
   out_already_confirmed_conflict=TRUE
@@ -378,14 +494,14 @@ SELECT
   WHERE internal_payto_uri = selected_exchange_payto_local;
 IF NOT FOUND
 THEN
-  out_nx_exchange=TRUE;
+  out_exchange_not_found=TRUE;
   RETURN;
 END IF;
-out_nx_exchange=FALSE;
+out_exchange_not_found=FALSE;
 SELECT -- not checking for accounts existence, as it was done above.
-  out_balance_insufficient
+  transfer.out_balance_insufficient
   INTO
-    maybe_balance_insufficient
+    out_balance_insufficient
 FROM bank_wire_transfer(
   exchange_bank_account_id,
   wallet_bank_account_local,
@@ -395,12 +511,12 @@ FROM bank_wire_transfer(
   in_acct_svcr_ref,
   in_pmt_inf_id,
   in_end_to_end_id
-);
+) as transfer;
 IF (maybe_balance_insufficient)
 THEN
-  out_insufficient_funds=TRUE;
+  out_balance_insufficient=TRUE;
 END IF;
-out_insufficient_funds=FALSE;
+out_balance_insufficient=FALSE;
 END $$;
 COMMENT ON FUNCTION confirm_taler_withdrawal(uuid, bigint, text, text, text)
   IS 'Set a withdrawal operation as confirmed and wire the funds to the 
exchange.';
@@ -414,9 +530,12 @@ CREATE OR REPLACE FUNCTION bank_wire_transfer(
   IN in_account_servicer_reference TEXT,
   IN in_payment_information_id TEXT,
   IN in_end_to_end_id TEXT,
-  OUT out_nx_creditor BOOLEAN,
-  OUT out_nx_debtor BOOLEAN,
+  -- Error status
+  OUT out_same_account BOOLEAN,
+  OUT out_debtor_not_found BOOLEAN,
+  OUT out_creditor_not_found BOOLEAN,
   OUT out_balance_insufficient BOOLEAN,
+  -- Success return
   OUT out_credit_row_id BIGINT,
   OUT out_debit_row_id BIGINT,
   OUT out_creditor_is_exchange BOOLEAN,
@@ -446,6 +565,13 @@ potential_balance_ok BOOLEAN;
 new_debit_row_id BIGINT;
 new_credit_row_id BIGINT;
 BEGIN
+
+IF in_creditor_account_id=in_debtor_account_id THEN
+  out_same_account=TRUE;
+  RETURN;
+END IF;
+out_same_account=FALSE;
+
 -- check debtor exists.
 SELECT
   has_debt,
@@ -462,12 +588,11 @@ SELECT
   FROM bank_accounts
   JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
   WHERE bank_account_id=in_debtor_account_id;
-IF NOT FOUND
-THEN
-  out_nx_debtor=TRUE;
+IF NOT FOUND THEN
+  out_debtor_not_found=TRUE;
   RETURN;
 END IF;
-out_nx_debtor=FALSE;
+out_debtor_not_found=FALSE;
 -- check creditor exists.  Future versions may skip this
 -- due to creditors being hosted at other banks.
 SELECT
@@ -483,16 +608,16 @@ SELECT
   FROM bank_accounts
   JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
   WHERE bank_account_id=in_creditor_account_id;
-IF NOT FOUND
-THEN
-  out_nx_creditor=TRUE;
+IF NOT FOUND THEN
+  out_creditor_not_found=TRUE;
   RETURN;
 END IF;
-out_nx_creditor=FALSE;
+out_creditor_not_found=FALSE;
+
 -- DEBTOR SIDE
 -- check debtor has enough funds.
-IF (debtor_has_debt)
-THEN -- debt case: simply checking against the max debt allowed.
+IF debtor_has_debt THEN 
+  -- debt case: simply checking against the max debt allowed.
   CALL amount_add(debtor_balance,
                  in_amount,
                  potential_balance);
@@ -500,8 +625,7 @@ THEN -- debt case: simply checking against the max debt 
allowed.
     INTO potential_balance_check
     FROM amount_left_minus_right(debtor_max_debt,
                                  potential_balance);
-  IF (NOT potential_balance_check)
-  THEN
+  IF NOT potential_balance_check THEN
     out_balance_insufficient=TRUE;
     RETURN;
   END IF;
@@ -517,8 +641,7 @@ ELSE -- not a debt account
       potential_balance.frac
     FROM amount_left_minus_right(debtor_balance,
                                  in_amount);
-  IF (potential_balance_ok) -- debtor has enough funds in the (positive) 
balance.
-  THEN
+  IF potential_balance_ok THEN -- debtor has enough funds in the (positive) 
balance.
     new_debtor_balance=potential_balance;
     will_debtor_have_debt=FALSE;
   ELSE -- debtor will switch to debt: determine their new negative balance.
@@ -533,8 +656,7 @@ ELSE -- not a debt account
       INTO potential_balance_check
       FROM amount_left_minus_right(debtor_max_debt,
                                    new_debtor_balance);
-    IF (NOT potential_balance_check)
-    THEN
+    IF NOT potential_balance_check THEN
       out_balance_insufficient=TRUE;
       RETURN;
     END IF;
@@ -545,8 +667,7 @@ END IF;
 -- Here we figure out whether the creditor would switch
 -- from debit to a credit situation, and adjust the balance
 -- accordingly.
-IF (NOT creditor_has_debt) -- easy case.
-THEN
+IF NOT creditor_has_debt THEN -- easy case.
   CALL amount_add(creditor_balance, in_amount, new_creditor_balance);
   will_creditor_have_debt=FALSE;
 ELSE -- creditor had debit but MIGHT switch to credit.
@@ -558,9 +679,8 @@ ELSE -- creditor had debit but MIGHT switch to credit.
       amount_at_least_debit
     FROM amount_left_minus_right(in_amount,
                                  creditor_balance);
-  IF (amount_at_least_debit)
-  -- the amount is at least as big as the debit, can switch to credit then.
-  THEN
+  IF amount_at_least_debit THEN
+    -- the amount is at least as big as the debit, can switch to credit then.
     will_creditor_have_debt=FALSE;
     -- compute new balance.
   ELSE

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]