gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (df45bda1 -> 6d6fa5fc)


From: gnunet
Subject: [libeufin] branch master updated (df45bda1 -> 6d6fa5fc)
Date: Tue, 24 Oct 2023 18:45:18 +0200

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

antoine pushed a change to branch master
in repository libeufin.

    from df45bda1 DD50 compliance.
     new aecf4112 Fix some status codes
     new df3a7c52 Prepare core bank cashout API
     new 6d6fa5fc Fix and improve withdrawal API

The 3 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/BankIntegrationApi.kt       |  18 +--
 .../main/kotlin/tech/libeufin/bank/CoreBankApi.kt  | 128 ++++++++++++---------
 .../src/main/kotlin/tech/libeufin/bank/Database.kt |  39 ++++---
 .../main/kotlin/tech/libeufin/bank/TalerMessage.kt |  54 ++++++++-
 .../kotlin/tech/libeufin/bank/WireGatewayApi.kt    |   4 +-
 bank/src/main/kotlin/tech/libeufin/bank/helpers.kt |  36 +++---
 bank/src/test/kotlin/CoreBankApiTest.kt            |  81 ++++++++++---
 bank/src/test/kotlin/WireGatewayApiTest.kt         |   4 +-
 database-versioning/libeufin-bank-procedures.sql   |  29 ++---
 9 files changed, 260 insertions(+), 133 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
index aebb8782..7a006535 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -38,16 +38,15 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
 
     // Note: wopid acts as an authentication token.
     get("/taler-integration/withdrawal-operation/{wopid}") {
-        val wopid = call.expectUriComponent("wopid")
         // TODO long poll
-        val op = getWithdrawal(db, wopid) // throws 404 if not found.
+        val op = call.getWithdrawal(db, "wopid") // throws 404 if not found.
         val relatedBankAccount = 
db.bankAccountGetFromOwnerId(op.walletBankAccount)
             ?: throw internalServerError("Bank has a withdrawal not related to 
any bank account.")
         val suggestedExchange = ctx.suggestedWithdrawalExchange
         val confirmUrl = if (ctx.spaCaptchaURL == null) null else
             getWithdrawalConfirmUrl(
                 baseUrl = ctx.spaCaptchaURL,
-                wopId = wopid
+                wopId = op.withdrawalUuid
             )
         call.respond(
             BankWithdrawalOperationStatus(
@@ -62,20 +61,15 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
         )
     }
     post("/taler-integration/withdrawal-operation/{wopid}") {
-        val wopid = call.expectUriComponent("wopid")
-        val uuid = try {
-            UUID.fromString(wopid)
-        } catch (e: Exception) {
-            throw badRequest("withdrawal_id query parameter was malformed")
-        }
+        val opId = call.uuidUriComponent("wopid")
         val req = call.receive<BankWithdrawalOperationPostRequest>()
 
         val (result, confirmationDone) = db.talerWithdrawalSetDetails(
-            uuid, req.selected_exchange, req.reserve_pub
+            opId, req.selected_exchange, req.reserve_pub
         )
         when (result) {
             WithdrawalSelectionResult.OP_NOT_FOUND -> throw notFound(
-                "Withdrawal operation $uuid not found", 
+                "Withdrawal operation $opId not found", 
                 TalerErrorCode.TALER_EC_END
             )
             WithdrawalSelectionResult.ALREADY_SELECTED -> throw conflict(
@@ -98,7 +92,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
                 val confirmUrl: String? = if (ctx.spaCaptchaURL !== null && 
!confirmationDone) {
                     getWithdrawalConfirmUrl(
                         baseUrl = ctx.spaCaptchaURL,
-                        wopId = wopid
+                        wopId = opId
                     )
                 } else null
                 call.respond(BankWithdrawalOperationPostResponse(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
index 5804a951..d47088d0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -31,6 +31,7 @@ fun Routing.coreBankApi(db: Database, ctx: 
BankApplicationContext) {
     coreBankAccountsMgmtApi(db, ctx)
     coreBankTransactionsApi(db, ctx)
     coreBankWithdrawalApi(db, ctx)
+    coreBankCashoutApi(db, ctx)
 }
 
 private fun Routing.coreBankTokenApi(db: Database) {
@@ -109,7 +110,6 @@ private fun Routing.coreBankTokenApi(db: Database) {
     }
 }
 
-
 private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
     post("/accounts") { 
         // check if only admin is allowed to create new accounts
@@ -385,7 +385,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, 
ctx: BankApplicationCo
 
         val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer 
lacks subject")
         val amount = tx.payto_uri.amount ?: tx.amount ?: throw 
badRequest("Wire transfer lacks amount")
-        checkInternalCurrency(ctx, amount)
+        ctx.checkInternalCurrency(amount)
         val result = db.bankTransaction(
             creditAccountPayto = tx.payto_uri,
             debitAccountUsername = login,
@@ -410,7 +410,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, 
ctx: BankApplicationCo
                 "Creditor account was not found",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
-            BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
+            BankTransactionResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
         }
     }
 }
@@ -420,7 +420,7 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: 
BankApplicationContext) {
         val (login, _) = call.authCheck(db, TokenScope.readwrite)
         val req = call.receive<BankAccountCreateWithdrawalRequest>() // 
Checking that the user has enough funds.
         
-        checkInternalCurrency(ctx, req.amount)
+        ctx.checkInternalCurrency(req.amount)
 
         val opId = UUID.randomUUID()
         when (db.talerWithdrawalCreate(login, opId, req.amount)) {
@@ -430,7 +430,7 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: 
BankApplicationContext) {
             )
             WithdrawalCreationResult.ACCOUNT_IS_EXCHANGE -> throw conflict(
                 "Exchange account cannot perform withdrawal operation",
-                TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
             WithdrawalCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
                 "Insufficient funds to withdraw with Taler",
@@ -447,7 +447,7 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: 
BankApplicationContext) {
         }       
     }
     get("/withdrawals/{withdrawal_id}") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
+        val op = call.getWithdrawal(db, "withdrawal_id")
         call.respond(
             BankAccountGetWithdrawalResponse(
                 amount = op.amount,
@@ -460,58 +460,80 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: 
BankApplicationContext) {
         )
     }
     post("/withdrawals/{withdrawal_id}/abort") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Idempotency:
-        if (op.aborted) {
-            call.respondText("{}", ContentType.Application.Json)
-            return@post
-        } // Op is found, it'll now fail only if previously confirmed (DB 
checks).
-        if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
-            hint = "Cannot abort confirmed withdrawal", talerEc = 
TalerErrorCode.TALER_EC_END
-        )
-        call.respondText("{}", ContentType.Application.Json)
+        val opId = call.uuidUriComponent("withdrawal_id")
+        when (db.talerWithdrawalAbort(opId)) {
+            WithdrawalAbortResult.NOT_FOUND -> throw notFound(
+                "Withdrawal operation $opId not found", 
+                TalerErrorCode.TALER_EC_END
+            )
+            WithdrawalAbortResult.CONFIRMED -> throw conflict(
+                "Cannot abort confirmed withdrawal", 
+                TalerErrorCode.TALER_EC_END
+            )
+            WithdrawalAbortResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
+        }
+        
     }
     post("/withdrawals/{withdrawal_id}/confirm") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Checking idempotency:
-        if (op.confirmationDone) {
-            call.respondText("{}", ContentType.Application.Json)
-            return@post
-        }
-        if (op.aborted) throw conflict(
-            hint = "Cannot confirm an aborted withdrawal", talerEc = 
TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
-        ) // Checking that reserve GOT indeed selected.
-        if (!op.selectionDone) throw LibeufinBankException(
-            httpStatus = HttpStatusCode.UnprocessableEntity, talerError = 
TalerError(
-                hint = "Cannot confirm an unselected withdrawal", code = 
TalerErrorCode.TALER_EC_END.code
+        val opId = call.uuidUriComponent("withdrawal_id")
+        when (db.talerWithdrawalConfirm(opId, Instant.now())) {
+            WithdrawalConfirmationResult.OP_NOT_FOUND -> throw notFound(
+                "Withdrawal operation $opId not found", 
+                TalerErrorCode.TALER_EC_END
             )
-        ) // Confirmation conditions are all met, now put the operation
-        // to the selected state _and_ wire the funds to the exchange.
-        // Note: 'when' helps not to omit more result codes, should more
-        // be added.
-        when (db.talerWithdrawalConfirm(op.withdrawalUuid, Instant.now())) {
-            WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw 
conflict(
-                    "Insufficient funds",
-                    TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
-                )
-            WithdrawalConfirmationResult.OP_NOT_FOUND ->
-                /**
-                 * Despite previous checks, the database _still_ did not
-                 * find the withdrawal operation, that's on the bank.
-                 */
-                throw internalServerError("Withdrawal operation 
(${op.withdrawalUuid}) not found")
-            WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
-                /**
-                 * That can happen because the bank did not check the exchange
-                 * exists when POST /withdrawals happened, or because the 
exchange
-                 * bank account got removed before this confirmation.
-                 */
-                throw conflict(
-                    hint = "Exchange to withdraw from not found",
-                    talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            WithdrawalConfirmationResult.ABORTED -> throw conflict(
+                "Cannot confirm an aborted withdrawal",
+                TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+            )
+            WithdrawalConfirmationResult.NOT_SELECTED -> throw 
LibeufinBankException(
+                httpStatus = HttpStatusCode.UnprocessableEntity, talerError = 
TalerError(
+                    hint = "Cannot confirm an unselected withdrawal", code = 
TalerErrorCode.TALER_EC_END.code
                 )
-            WithdrawalConfirmationResult.CONFLICT -> throw 
internalServerError("Bank didn't check for idempotency")
-            WithdrawalConfirmationResult.SUCCESS -> call.respondText(
-                "{}", ContentType.Application.Json
             )
+            WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw 
conflict(
+                "Insufficient funds",
+                TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+            )
+            WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND -> throw conflict(
+                "Exchange to withdraw from not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
+            WithdrawalConfirmationResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
         }
     }
+}
+
+fun Routing.coreBankCashoutApi(db: Database, ctx: BankApplicationContext) {
+    post("/accounts/{USERNAME}/cashouts") {
+        val (login, _) = call.authCheck(db, TokenScope.readwrite)
+        val req = call.receive<CashoutRequest>() // Checking that the user has 
enough funds.
+        
+        ctx.checkInternalCurrency(req.amount_debit)
+        ctx.checkCashoutCurrency(req.amount_credit)
+
+        // TODO    
+    }
+    post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/abort") {
+        val (login, _) = call.authCheck(db, TokenScope.readwrite)
+        // TODO    
+    }
+    post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm") {
+        val (login, _) = call.authCheck(db, TokenScope.readwrite)
+        // TODO
+    }
+    get("/accounts/{USERNAME}/cashouts") {
+        val (login, _) = call.authCheck(db, TokenScope.readonly)
+        // TODO
+    }
+    get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") {
+        val (login, _) = call.authCheck(db, TokenScope.readonly)
+        // TODO
+    }
+    get("/cashouts") {
+        call.authAdmin(db, TokenScope.readonly)
+        // TODO
+    }
+    get("/cashout-rate") {
+        // TODO
+    }
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index a28d7b05..8e4be686 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -1022,16 +1022,20 @@ class Database(dbConfig: String, private val 
bankCurrency: String, private val f
      * Aborts one Taler withdrawal, only if it wasn't previously
      * confirmed.  It returns false if the UPDATE didn't succeed.
      */
-    suspend fun talerWithdrawalAbort(opUUID: UUID): Boolean = conn { conn ->
+    suspend fun talerWithdrawalAbort(opUUID: UUID): WithdrawalAbortResult = 
conn { conn ->
         val stmt = conn.prepareStatement("""
             UPDATE taler_withdrawal_operations
-            SET aborted = true
-            WHERE withdrawal_uuid=? AND confirmation_done = false
-            RETURNING taler_withdrawal_id
+            SET aborted = NOT confirmation_done
+            WHERE withdrawal_uuid=?
+            RETURNING confirmation_done
         """
         )
         stmt.setObject(1, opUUID)
-        stmt.executeQueryCheck()
+        when (stmt.oneOrNull { it.getBoolean(1) }) {
+            null -> WithdrawalAbortResult.NOT_FOUND
+            true -> WithdrawalAbortResult.CONFIRMED
+            false -> WithdrawalAbortResult.SUCCESS
+        }
     }
 
     /**
@@ -1093,7 +1097,8 @@ class Database(dbConfig: String, private val 
bankCurrency: String, private val f
               out_no_op,
               out_exchange_not_found,
               out_balance_insufficient,
-              out_already_confirmed_conflict
+              out_not_selected,
+              out_aborted
             FROM confirm_taler_withdrawal(?, ?, ?, ?, ?);
         """
         )
@@ -1109,7 +1114,8 @@ class Database(dbConfig: String, private val 
bankCurrency: String, private val f
                 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
+                it.getBoolean("out_not_selected") -> 
WithdrawalConfirmationResult.NOT_SELECTED
+                it.getBoolean("out_aborted") -> 
WithdrawalConfirmationResult.ABORTED
                 else -> WithdrawalConfirmationResult.SUCCESS
             }
         }
@@ -1586,6 +1592,13 @@ enum class WithdrawalSelectionResult {
     ACCOUNT_IS_NOT_EXCHANGE
 }
 
+/** Result status of withdrawal operation abortion */
+enum class WithdrawalAbortResult {
+    SUCCESS,
+    NOT_FOUND,
+    CONFIRMED
+}
+
 /**
  * This type communicates the result of a database operation
  * to confirm one withdrawal operation.
@@ -1595,16 +1608,8 @@ enum class WithdrawalConfirmationResult {
     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
+    NOT_SELECTED,
+    ABORTED
 }
 
 private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index a91e7b38..3d17e0ae 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -40,13 +40,17 @@ enum class FracDigits {
     TWO, EIGHT
 }
 
-
 // Allowed values for bank transactions directions.
 enum class TransactionDirection {
     credit,
     debit
 }
 
+enum class CashoutStatus {
+    pending,
+    confirmed
+}
+
 /**
  * HTTP response type of successful token refresh.
  * access_token is the Crockford encoding of the 32 byte
@@ -506,6 +510,54 @@ data class BankWithdrawalOperationPostResponse(
     val confirm_transfer_url: String? = null
 )
 
+@Serializable
+data class CashoutRequest(
+    val subject: String?,
+    val amount_debit: TalerAmount,
+    val amount_credit: TalerAmount,
+    val tan_channel: TanChannel?
+)
+
+@Serializable
+data class Cashouts(
+    val cashouts: List<CashoutInfo>,
+)
+
+@Serializable
+data class CashoutInfo(
+    val cashout_id: String,
+    val status: CashoutStatus,
+)
+
+
+@Serializable
+data class GlobalCashouts(
+    val cashouts: List<GlobalCashoutInfo>,
+)
+
+@Serializable
+data class GlobalCashoutInfo(
+    val cashout_id: String,
+    val username: String,
+    val status: CashoutStatus,
+)
+
+@Serializable
+data class CashoutStatusResponse(
+    val status: CashoutStatus,
+    val amount_debit: TalerAmount,
+    val amount_credit: TalerAmount,
+    val subject: String,
+    val credit_payto_uri: IbanPayTo,
+    val creation_time: TalerProtocolTimestamp,
+    val confirmation_time: TalerProtocolTimestamp?,
+)
+
+@Serializable
+data class CashoutConfirm(
+    val tan: String
+)
+
 /**
  * Request to an /admin/add-incoming request from
  * the Taler Wire Gateway API.
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
index 68b9c9f4..b734a603 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -45,7 +45,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
     post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
         val (login, _) = call.authCheck(db, TokenScope.readwrite)
         val req = call.receive<TransferRequest>()
-        checkInternalCurrency(ctx, req.amount)
+        ctx.checkInternalCurrency(req.amount)
         val dbRes = db.talerTransferCreate(
             req = req,
             username = login,
@@ -124,7 +124,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
     post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
         val (login, _) = call.authCheck(db, TokenScope.readwrite) // TODO 
authAdmin ?
         val req = call.receive<AddIncomingRequest>()
-        checkInternalCurrency(ctx, req.amount)
+        ctx.checkInternalCurrency(req.amount)
         val timestamp = Instant.now()
         val dbRes = db.talerAddIncomingCreate(
             req = req,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index e5f2335f..21d3b4c4 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -116,9 +116,16 @@ fun badRequest(
     )
 )
 
-fun checkInternalCurrency(ctx: BankApplicationContext, amount: TalerAmount) {
-    if (amount.currency != ctx.currency) throw badRequest(
-        "Wrong currency: expected internal currency ${ctx.currency} got 
${amount.currency}",
+fun BankApplicationContext.checkInternalCurrency(amount: TalerAmount) {
+    if (amount.currency != currency) throw badRequest(
+        "Wrong currency: expected internal currency $currency got 
${amount.currency}",
+        talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+    )
+}
+
+fun BankApplicationContext.checkCashoutCurrency(amount: TalerAmount) {
+    if (amount.currency != cashoutCurrency) throw badRequest(
+        "Wrong currency: expected cashout currency $cashoutCurrency got 
${amount.currency}",
         talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
     )
 }
@@ -157,11 +164,19 @@ fun getTalerWithdrawUri(baseUrl: String, woId: String) = 
url {
 
 // Builds a withdrawal confirm URL.
 fun getWithdrawalConfirmUrl(
-    baseUrl: String, wopId: String
+    baseUrl: String, wopId: UUID
 ): String {
-    return baseUrl.replace("{woid}", wopId)
+    return baseUrl.replace("{woid}", wopId.toString())
 }
 
+fun ApplicationCall.uuidUriComponent(name: String): UUID {
+    try {
+        return UUID.fromString(expectUriComponent(name))
+    } catch (e: Exception) {
+        logger.error(e.message)
+        throw badRequest("UUID uri component malformed")
+    }
+}
 
 /**
  * This handler factors out the checking of the query param
@@ -170,15 +185,10 @@ fun getWithdrawalConfirmUrl(
  * if the query param doesn't parse into a UUID.  Currently
  * used by the Taler Web/SPA and Integration API handlers.
  */
-suspend fun getWithdrawal(db: Database, opIdParam: String): 
TalerWithdrawalOperation {
-    val opId = try {
-        UUID.fromString(opIdParam)
-    } catch (e: Exception) {
-        logger.error(e.message)
-        throw badRequest("withdrawal_id query parameter was malformed")
-    }
+suspend fun ApplicationCall.getWithdrawal(db: Database, name: String): 
TalerWithdrawalOperation {
+    val opId = uuidUriComponent(name)
     val op = db.talerWithdrawalGet(opId) ?: throw notFound(
-        hint = "Withdrawal operation $opIdParam not found", talerEc = 
TalerErrorCode.TALER_EC_END
+        hint = "Withdrawal operation $opId not found", talerEc = 
TalerErrorCode.TALER_EC_END
     )
     return op
 }
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt 
b/bank/src/test/kotlin/CoreBankApiTest.kt
index 56fbb002..a180e8bb 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -263,7 +263,7 @@ class CoreBankAccountsMgmtApiTest {
             jsonBody(json {
                 "payto_uri" to 
"payto://iban/MERCHANT-IBAN-XYZ?message=payout&amount=KUDOS:1"
             })
-        }.assertOk()
+        }.assertNoContent()
         client.delete("/accounts/merchant") {
             basicAuth("admin", "admin-password")
         }.assertConflict()
@@ -272,7 +272,7 @@ class CoreBankAccountsMgmtApiTest {
             jsonBody(json {
                 "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout&amount=KUDOS:1"
             })
-        }.assertOk()
+        }.assertNoContent()
         client.delete("/accounts/merchant") {
             basicAuth("admin", "admin-password")
         }.assertNoContent()
@@ -496,7 +496,7 @@ class CoreBankTransactionsApiTest {
                 jsonBody(json {
                     "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout$it&amount=KUDOS:0.$it"
                 })
-            }.assertOk()
+            }.assertNoContent()
         }
         // Gen two transactions from exchange to merchant
         repeat(2) {
@@ -505,7 +505,7 @@ class CoreBankTransactionsApiTest {
                 jsonBody(json {
                     "payto_uri" to 
"payto://iban/MERCHANT-IBAN-XYZ?message=payout$it&amount=KUDOS:0.$it"
                 })
-            }.assertOk()
+            }.assertNoContent()
         }
 
         // Check no useless polling
@@ -543,7 +543,7 @@ class CoreBankTransactionsApiTest {
                 jsonBody(json {
                     "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout_poll&amount=KUDOS:4.2"
                 })
-            }.assertOk()
+            }.assertNoContent()
         }
 
         // Testing ranges. 
@@ -553,7 +553,7 @@ class CoreBankTransactionsApiTest {
                 jsonBody(json {
                     "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout_range&amount=KUDOS:0.001"
                 })
-            }.assertOk()
+            }.assertNoContent()
         }
 
         // forward range:
@@ -579,7 +579,7 @@ class CoreBankTransactionsApiTest {
                 "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
                 "amount" to "KUDOS:0.3"
             })
-        }.assertOk()
+        }.assertNoContent()
         // Check OK
         client.get("/accounts/merchant/transactions/1") {
             basicAuth("merchant", "merchant-password")
@@ -612,7 +612,7 @@ class CoreBankTransactionsApiTest {
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody(valid_req)
-        }.assertOk()
+        }.assertNoContent()
         client.get("/accounts/merchant/transactions/1") {
             basicAuth("merchant", "merchant-password")
         }.assertOk().run {
@@ -626,7 +626,7 @@ class CoreBankTransactionsApiTest {
             jsonBody(json {
                 "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout2&amount=KUDOS:1.05"
             })
-        }.assertOk()
+        }.assertNoContent()
         client.get("/accounts/merchant/transactions/3") {
             basicAuth("merchant", "merchant-password")
         }.assertOk().run {
@@ -641,7 +641,7 @@ class CoreBankTransactionsApiTest {
                 "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout3&amount=KUDOS:1.05"
                 "amount" to "KUDOS:10.003"
             })
-        }.assertOk()
+        }.assertNoContent()
         client.get("/accounts/merchant/transactions/5") {
             basicAuth("merchant", "merchant-password")
         }.assertOk().run {
@@ -695,10 +695,24 @@ class CoreBankWithdrawalApiTest {
     // POST /accounts/USERNAME/withdrawals
     @Test
     fun create() = bankSetup { _ ->
+        // Check OK
         client.post("/accounts/merchant/withdrawals") {
             basicAuth("merchant", "merchant-password")
             jsonBody(json { "amount" to "KUDOS:9.0" }) 
         }.assertOk()
+
+        // Check exchange account
+        client.post("/accounts/exchange/withdrawals") {
+            basicAuth("exchange", "exchange-password")
+            jsonBody(json { "amount" to "KUDOS:9.0" }) 
+        
}.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT)
+
+        // Check insufficient fund
+        client.post("/accounts/merchant/withdrawals") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(json { "amount" to "KUDOS:90" }) 
+        
}.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT)
+
     }
 
     // GET /withdrawals/withdrawal_id
@@ -734,9 +748,9 @@ class CoreBankWithdrawalApiTest {
             val uuid = resp.taler_withdraw_uri.split("/").last()
 
             // Check OK
-            client.post("/withdrawals/$uuid/abort").assertOk()
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
             // Check idempotence
-            client.post("/withdrawals/$uuid/abort").assertOk()
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
         }
 
         // Check abort selected
@@ -754,9 +768,9 @@ class CoreBankWithdrawalApiTest {
             }.assertOk()
 
             // Check OK
-            client.post("/withdrawals/$uuid/abort").assertOk()
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
             // Check idempotence
-            client.post("/withdrawals/$uuid/abort").assertOk()
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
         }
 
         // Check abort confirmed
@@ -772,7 +786,7 @@ class CoreBankWithdrawalApiTest {
                     "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
                 })
             }.assertOk()
-            client.post("/withdrawals/$uuid/confirm").assertOk()
+            client.post("/withdrawals/$uuid/confirm").assertNoContent()
 
             // Check error
             client.post("/withdrawals/$uuid/abort").assertConflict()
@@ -787,7 +801,7 @@ class CoreBankWithdrawalApiTest {
 
     // POST /withdrawals/withdrawal_id/confirm
     @Test
-    fun confirm() = bankSetup { db -> 
+    fun confirm() = bankSetup { _ -> 
         // Check confirm created
         client.post("/accounts/merchant/withdrawals") {
             basicAuth("merchant", "merchant-password")
@@ -815,9 +829,9 @@ class CoreBankWithdrawalApiTest {
             }.assertOk()
 
             // Check OK
-            client.post("/withdrawals/$uuid/confirm").assertOk()
+            client.post("/withdrawals/$uuid/confirm").assertNoContent()
             // Check idempotence
-            client.post("/withdrawals/$uuid/confirm").assertOk()
+            client.post("/withdrawals/$uuid/confirm").assertNoContent()
         }
 
         // Check confirm aborted
@@ -833,13 +847,42 @@ class CoreBankWithdrawalApiTest {
                     "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
                 })
             }.assertOk()
-            client.post("/withdrawals/$uuid/abort").assertOk()
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
 
             // Check error
             client.post("/withdrawals/$uuid/confirm").assertConflict()
                 .assertErr(TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT)
         }
 
+        // Check balance insufficient
+        client.post("/accounts/merchant/withdrawals") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(json { "amount" to "KUDOS:5" }) 
+        }.assertOk().run {
+            val resp = 
Json.decodeFromString<BankAccountCreateWithdrawalResponse>(bodyAsText())
+            val uuid = resp.taler_withdraw_uri.split("/").last()
+            client.post("/taler-integration/withdrawal-operation/$uuid") {
+                jsonBody(json {
+                    "reserve_pub" to randEddsaPublicKey()
+                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                })
+            }.assertOk()
+
+            // Send too much money
+            client.post("/accounts/merchant/transactions") {
+                basicAuth("merchant", "merchant-password")
+                jsonBody(json {
+                    "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout&amount=KUDOS:5"
+                })
+            }.assertNoContent()
+
+            client.post("/withdrawals/$uuid/confirm").assertConflict()
+                .assertErr(TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT)
+
+            // Check can abort because not confirmed
+            client.post("/withdrawals/$uuid/abort").assertNoContent()
+        }
+
         // Check bad UUID
         client.post("/withdrawals/chocolate/confirm").assertBadRequest()
 
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt 
b/bank/src/test/kotlin/WireGatewayApiTest.kt
index 1fa33dba..3164c28a 100644
--- a/bank/src/test/kotlin/WireGatewayApiTest.kt
+++ b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -271,7 +271,7 @@ class WireGatewayApiTest {
             }.assertOk()
             client.post("/withdrawals/${uuid}/confirm") {
                 basicAuth("merchant", "merchant-password")
-            }.assertOk()
+            }.assertNoContent()
         }
 
         // Check ignore bogus subject
@@ -356,7 +356,7 @@ class WireGatewayApiTest {
                 }.assertOk()
                 client.post("/withdrawals/${uuid}/confirm") {
                     basicAuth("merchant", "merchant-password")
-                }.assertOk()
+                }.assertNoContent()
             }
         }
 
diff --git a/database-versioning/libeufin-bank-procedures.sql 
b/database-versioning/libeufin-bank-procedures.sql
index 7ad6cb4a..6da31ed8 100644
--- a/database-versioning/libeufin-bank-procedures.sql
+++ b/database-versioning/libeufin-bank-procedures.sql
@@ -687,11 +687,12 @@ CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
   OUT out_balance_insufficient BOOLEAN,
   OUT out_creditor_not_found BOOLEAN,
   OUT out_exchange_not_found BOOLEAN,
-  OUT out_already_confirmed_conflict BOOLEAN
+  OUT out_not_selected BOOLEAN,
+  OUT out_aborted BOOLEAN
 )
 LANGUAGE plpgsql AS $$
 DECLARE
-  confirmation_done_local BOOLEAN;
+  already_confirmed BOOLEAN;
   subject_local TEXT;
   reserve_pub_local BYTEA;
   selected_exchange_payto_local TEXT;
@@ -702,12 +703,14 @@ DECLARE
 BEGIN
 SELECT -- Really no-star policy and instead DECLARE almost one var per column?
   confirmation_done,
+  aborted, NOT selection_done,
   reserve_pub, subject,
   selected_exchange_payto,
   wallet_bank_account,
   (amount).val, (amount).frac
   INTO
-    confirmation_done_local,
+    already_confirmed,
+    out_aborted, out_not_selected,
     reserve_pub_local, subject_local,
     selected_exchange_payto_local,
     wallet_bank_account_local,
@@ -717,17 +720,10 @@ SELECT -- Really no-star policy and instead DECLARE 
almost one var per column?
 IF NOT FOUND THEN
   out_no_op=TRUE;
   RETURN;
+ELSIF already_confirmed OR out_aborted OR out_not_selected THEN
+  RETURN;
 END IF;
-out_no_op=FALSE;
-IF confirmation_done_local THEN
-  out_already_confirmed_conflict=TRUE
-  RETURN; -- Kotlin should have checked for idempotency before reaching here!
-END IF;
-out_already_confirmed_conflict=FALSE;
--- exists and wasn't confirmed, do it.
-UPDATE taler_withdrawal_operations
-  SET confirmation_done = true
-  WHERE withdrawal_uuid=in_withdrawal_uuid;
+
 -- sending the funds to the exchange, but needs first its bank account row ID
 SELECT
   bank_account_id
@@ -738,7 +734,7 @@ IF NOT FOUND THEN
   out_exchange_not_found=TRUE;
   RETURN;
 END IF;
-out_exchange_not_found=FALSE;
+
 SELECT -- not checking for accounts existence, as it was done above.
   transfer.out_balance_insufficient,
   out_credit_row_id
@@ -757,6 +753,11 @@ IF out_balance_insufficient THEN
   RETURN;
 END IF;
 
+-- Confirm operation
+UPDATE taler_withdrawal_operations
+  SET confirmation_done = true
+  WHERE withdrawal_uuid=in_withdrawal_uuid;
+
 -- Register incoming transaction
 CALL register_incoming(reserve_pub_local, tx_row_id, exchange_bank_account_id);
 END $$;

-- 
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]