libeufin

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

commit 4474752e53c0ffdec32150282bd85e88261eb020
parent 989111c2c13bed174ba1fb9a01fa62ef56348002
Author: ms <ms@taler.net>
Date:   Thu, 21 Oct 2021 09:13:51 +0200

Access API: GETting withdrawal information.

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 4++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 9+++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 8++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 68+++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mutil/src/main/kotlin/HTTP.kt | 8++++++++
Mutil/src/main/kotlin/amounts.kt | 7+++++++
Mutil/src/main/kotlin/strings.kt | 7-------
7 files changed, 83 insertions(+), 28 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -430,7 +430,7 @@ object TalerWithdrawalsTable : LongIdTable() { * gets completed _on the bank's side_. This does never guarantees that * the payment arrived at the exchange's bank yet. */ - val transferDone = bool("transferDone").default(false) + val confirmationDone = bool("confirmationDone").default(false) val reservePub = text("reservePub").nullable() val selectedExchangePayto = text("selectedExchangePayto").nullable() val walletBankAccount = reference("walletBankAccount", BankAccountsTable) @@ -439,7 +439,7 @@ class TalerWithdrawalEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<TalerWithdrawalEntity>(TalerWithdrawalsTable) var wopid by TalerWithdrawalsTable.wopid var selectionDone by TalerWithdrawalsTable.selectionDone - var transferDone by TalerWithdrawalsTable.transferDone + var confirmationDone by TalerWithdrawalsTable.confirmationDone var reservePub by TalerWithdrawalsTable.reservePub var selectedExchangePayto by TalerWithdrawalsTable.selectedExchangePayto var amount by TalerWithdrawalsTable.amount diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -151,6 +151,15 @@ fun wireTransfer( return transactionRef } +fun getWithdrawalOperation(opId: String): TalerWithdrawalEntity { + return transaction { + TalerWithdrawalEntity.find { + TalerWithdrawalsTable.wopid eq java.util.UUID.fromString(opId) + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, "Withdrawal operation $opId not found." + ) + } +} fun getBankAccountFromPayto(paytoUri: String): BankAccountEntity { val paytoParse = parsePayto(paytoUri) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -22,6 +22,14 @@ package tech.libeufin.sandbox import tech.libeufin.util.PaymentInfo import tech.libeufin.util.RawPayment +data class WithdrawalRequest( + /** + * Note: the currency is redundant, because at each point during + * the execution the Demobank should have a handle of the currency. + */ + val amount: String // $CURRENCY:X.Y +) + data class Demobank( val currency: String, val name: String, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -63,7 +63,6 @@ import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.util.* import io.ktor.util.date.* -import io.ktor.util.pipeline.* import kotlinx.coroutines.newSingleThreadContext import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.ExposedBlob @@ -72,6 +71,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.w3c.dom.Document +import parseAmount import startServer import tech.libeufin.util.* import validatePlainAmount @@ -960,7 +960,7 @@ val sandboxApp: Application.() -> Unit = { HttpStatusCode.NotFound, "Withdrawal operation $wopid not found." ) if (wo.selectionDone) { - if (wo.transferDone) { + if (wo.confirmationDone) { logger.info("Wallet performs again this operation that was paid out earlier: idempotent") return@newSuspendedTransaction } @@ -977,7 +977,7 @@ val sandboxApp: Application.() -> Unit = { wo.reservePub = body.reserve_pub wo.selectedExchangePayto = body.selected_exchange wo.selectionDone = true - wo.transferDone + wo.confirmationDone } call.respond(object { val transfer_done = transferDone @@ -997,7 +997,7 @@ val sandboxApp: Application.() -> Unit = { val demobank = ensureDemobank(call) val ret = TalerWithdrawalStatus( selection_done = wo.selectionDone, - transfer_done = wo.transferDone, + transfer_done = wo.confirmationDone, amount = wo.amount, suggested_exchange = demobank.suggestedExchange ) @@ -1007,13 +1007,31 @@ val sandboxApp: Application.() -> Unit = { } // Talk to Web UI. route("/access-api") { + // Information about one withdrawal. + get("/accounts/{account_name}/withdrawals/{withdrawal_id}") { + val op = getWithdrawalOperation(call.getUriComponent("withdrawal_id")) + val demobank = ensureDemobank(call) + if (!op.selectionDone && op.reservePub != null) throw internalServerError( + "Unselected withdrawal has a reserve public key", + LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE + ) + call.respond(object { + val amount = "${demobank.currency}:${op.amount}" + val aborted = op.aborted + val confirmation_done = op.confirmationDone + val selection_done = op.selectionDone + val selected_reserve_pub = op.reservePub + val selected_exchange_account = op.selectedExchangePayto + }) + return@get + } // Create a new withdrawal operation. post("/accounts/{account_name}/withdrawals") { - val username = call.request.basicAuth() - if (username == null) throw badRequest( - "Taler withdrawal tried with authentication disabled. " + - "That is impossible, because no bank account can get this operation debited." - ) + var username = call.request.basicAuth() + if (username == null && (!WITH_AUTH)) { + logger.info("Authentication is disabled to facilitate tests, defaulting to 'admin' username") + username = "admin" + } val demobank = ensureDemobank(call) /** * Check here if the user has the right over the claimed bank account. After @@ -1024,8 +1042,14 @@ val sandboxApp: Application.() -> Unit = { if (maybeOwnedAccount.owner != username) throw unauthorized( "Customer '$username' has no rights over bank account '${maybeOwnedAccount.label}'" ) + val req = call.receive<WithdrawalRequest>() + // Check for currency consistency + val amount = parseAmount(req.amount) + if (amount.currency != demobank.currency) throw badRequest( + "Currency ${amount.currency} differs from Demobank's: ${demobank.currency}" + ) val wo: TalerWithdrawalEntity = transaction { TalerWithdrawalEntity.new { - amount = "${demobank.currency}:5" + this.amount = amount.amount.toPlainString() walletBankAccount = maybeOwnedAccount } } val baseUrl = URL(call.request.getBaseUrl()) @@ -1043,15 +1067,11 @@ val sandboxApp: Application.() -> Unit = { }) return@post } - // Confirm a withdrawal. + // Confirm a withdrawal: no basic auth, because the ID should be unguessable. post("/accounts/{account_name}/withdrawals/{withdrawal_id}/confirm") { val withdrawalId = call.getUriComponent("withdrawal_id") transaction { - val wo = TalerWithdrawalEntity.find { - TalerWithdrawalsTable.wopid eq java.util.UUID.fromString(withdrawalId) - }.firstOrNull() ?: throw SandboxError( - HttpStatusCode.NotFound, "Withdrawal operation $withdrawalId not found." - ) + val wo = getWithdrawalOperation(withdrawalId) if (wo.aborted) throw SandboxError( HttpStatusCode.Conflict, "Cannot confirm an aborted withdrawal." @@ -1066,7 +1086,7 @@ val sandboxApp: Application.() -> Unit = { "Cannot withdraw without an exchange." ) ) - if (!wo.transferDone) { + if (!wo.confirmationDone) { // Need the exchange bank account! wireTransfer( debitAccount = wo.walletBankAccount, @@ -1075,15 +1095,25 @@ val sandboxApp: Application.() -> Unit = { subject = wo.reservePub ?: throw internalServerError( "Cannot transfer funds without reserve public key." ), + // provide the currency. demoBank = ensureDemobank(call) ) - wo.transferDone = true + wo.confirmationDone = true } - wo.transferDone + wo.confirmationDone } call.respond(object {}) return@post } + + post("/accounts/{account_name}/withdrawals/{withdrawal_id}/abort") { + val withdrawalId = call.getUriComponent("withdrawal_id") + val operation = getWithdrawalOperation(withdrawalId) + if (operation.confirmationDone) throw conflict("Cannot abort paid withdrawal.") + transaction { operation.aborted = true } + call.respond(object {}) + return@post + } // Bank account basic information. get("/accounts/{account_name}") { val username = call.request.basicAuth() diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt @@ -68,6 +68,14 @@ fun badRequest(msg: String): UtilError { ) } +fun conflict(msg: String): UtilError { + return UtilError( + HttpStatusCode.Conflict, + msg, + ec = LibeufinErrorCode.LIBEUFIN_EC_NONE + ) +} + /** * Get the base URL of a request; handles proxied case. */ diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt @@ -28,3 +28,10 @@ val re = Regex("^([0-9]+(\\.[0-9]+)?)$") fun validatePlainAmount(plainAmount: String): Boolean { return re.matches(plainAmount) } + +fun parseAmount(amount: String): AmountWithCurrency { + val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?: throw + EbicsProtocolError(HttpStatusCode.BadRequest, "invalid amount: $amount") + val (currency, number) = match.destructured + return AmountWithCurrency(currency, Amount(number)) +} diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt @@ -122,13 +122,6 @@ fun parseDecimal(decimalStr: String): BigDecimal { } } -fun parseAmount(amount: String): AmountWithCurrency { - val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?: throw - EbicsProtocolError(HttpStatusCode.BadRequest, "invalid amount: $amount") - val (currency, number) = match.destructured - return AmountWithCurrency(currency, Amount(number)) -} - fun getRandomString(length: Int): String { val allowedChars = ('A' .. 'Z') + ('0' .. '9') return (1 .. length)