libeufin

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

commit 70157b68c559073179410f62469eb8b504e4b35b
parent 61a3b2c89d4d6786469992f344ba379a1ab8db10
Author: Antoine A <>
Date:   Mon,  6 Nov 2023 19:17:24 +0000

Improve cashout endpoints

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 6+++++-
Mbank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt | 3+++
Mbank/src/test/kotlin/CoreBankApiTest.kt | 42++++++++++++++++++++++++++++++++++++++----
Mdatabase-versioning/libeufin-bank-procedures.sql | 20+++++++++++++++-----
4 files changed, 61 insertions(+), 10 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -587,12 +587,16 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { ) CashoutConfirmationResult.NO_CASHOUT_PAYTO -> throw conflict( "Missing cashout payto uri", - TalerErrorCode.BANK_MISSING_TAN_INFO + TalerErrorCode.BANK_CONFIRM_INCOMPLETE ) CashoutConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict( "Insufficient funds", TalerErrorCode.BANK_UNALLOWED_DEBIT ) + CashoutConfirmationResult.BAD_CONVERSION -> throw conflict( + "Wrong currency conversion", + TalerErrorCode.BANK_BAD_CONVERSION + ) CashoutConfirmationResult.SUCCESS -> call.respond(HttpStatusCode.NoContent) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -39,6 +39,7 @@ enum class CashoutCreationResult { /** Result status of cashout operation confirmation */ enum class CashoutConfirmationResult { SUCCESS, + BAD_CONVERSION, OP_NOT_FOUND, BAD_TAN_CODE, BALANCE_INSUFFICIENT, @@ -158,6 +159,7 @@ class CashoutDAO(private val db: Database) { val stmt = conn.prepareStatement(""" SELECT out_no_op, + out_bad_conversion, out_bad_code, out_balance_insufficient, out_aborted, @@ -178,6 +180,7 @@ class CashoutDAO(private val db: Database) { it.getBoolean("out_aborted") -> CashoutConfirmationResult.ABORTED it.getBoolean("out_no_retry") -> CashoutConfirmationResult.NO_RETRY it.getBoolean("out_no_cashout_payto") -> CashoutConfirmationResult.NO_CASHOUT_PAYTO + it.getBoolean("out_bad_conversion") -> CashoutConfirmationResult.BAD_CONVERSION else -> CashoutConfirmationResult.SUCCESS } } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -936,7 +936,7 @@ class CoreBankWithdrawalApiTest { // POST /withdrawals/withdrawal_id/confirm @Test - fun confirm() = bankSetup { _ -> + fun confirm() = bankSetup { db -> // Check confirm created client.post("/accounts/merchant/withdrawals") { basicAuth("merchant", "merchant-password") @@ -1233,7 +1233,7 @@ class CoreBankCashoutApiTest { // POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm @Test - fun confirm() = bankSetup { _ -> + fun confirm() = bankSetup { db -> // TODO auth routine client.patch("/accounts/customer") { basicAuth("customer", "customer-password") @@ -1261,7 +1261,7 @@ class CoreBankCashoutApiTest { client.post("/accounts/customer/cashouts/$uuid/confirm") { basicAuth("customer", "customer-password") jsonBody { "tan" to "code" } - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) + }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) client.patch("/accounts/customer") { basicAuth("customer", "customer-password") jsonBody(json { @@ -1292,12 +1292,46 @@ class CoreBankCashoutApiTest { }.assertNoContent() } - // Check balance insufficient + // Check bad conversion client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") jsonBody(json(req) { "request_uid" to randShortHashCode() }) }.assertOk().run { val uuid = json<CashoutPending>().cashout_id + + db.conversion.updateConfig(ConversionInfo( + buy_ratio = DecimalNumber("1"), + buy_fee = DecimalNumber("1"), + buy_tiny_amount = TalerAmount("KUDOS:0.0001"), + buy_rounding_mode = RoundingMode.nearest, + buy_min_amount = TalerAmount("FIAT:0.0001"), + sell_ratio = DecimalNumber("1"), + sell_fee = DecimalNumber("1"), + sell_tiny_amount = TalerAmount("FIAT:0.0001"), + sell_rounding_mode = RoundingMode.nearest, + sell_min_amount = TalerAmount("KUDOS:0.0001"), + )) + + client.post("/accounts/customer/cashouts/$uuid/confirm"){ + basicAuth("customer", "customer-password") + jsonBody { "tan" to smsCode("+99") } + }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) + + // Check can abort because not confirmed + client.post("/accounts/customer/cashouts/$uuid/abort") { + basicAuth("customer", "customer-password") + }.assertNoContent() + } + + // Check balance insufficient + client.post("/accounts/customer/cashouts") { + basicAuth("customer", "customer-password") + jsonBody(json(req) { + "request_uid" to randShortHashCode() + "amount_credit" to convert("KUDOS:1") + }) + }.assertOk().run { + val uuid = json<CashoutPending>().cashout_id // Send too much money client.post("/accounts/customer/transactions") { basicAuth("customer", "customer-password") diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -1101,6 +1101,7 @@ CREATE OR REPLACE FUNCTION cashout_confirm( IN in_tan_code TEXT, IN in_now_date BIGINT, OUT out_no_op BOOLEAN, + OUT out_bad_conversion BOOLEAN, OUT out_bad_code BOOLEAN, OUT out_balance_insufficient BOOLEAN, OUT out_aborted BOOLEAN, @@ -1113,7 +1114,8 @@ DECLARE admin_account_id BIGINT; already_confirmed BOOLEAN; subject_local TEXT; - amount_local taler_amount; + amount_debit_local taler_amount; + amount_credit_local taler_amount; challenge_id BIGINT; tx_id BIGINT; BEGIN @@ -1123,14 +1125,16 @@ SELECT aborted, subject, bank_account, challenge, (amount_debit).val, (amount_debit).frac, + (amount_credit).val, (amount_credit).frac, cashout_payto IS NULL INTO already_confirmed, out_aborted, subject_local, wallet_account_id, challenge_id, - amount_local.val, amount_local.frac, + amount_debit_local.val, amount_debit_local.frac, + amount_credit_local.val, amount_credit_local.frac, out_no_cashout_payto - FROM cashout_operations + FROM cashout_operations JOIN bank_accounts ON bank_account_id=bank_account JOIN customers ON customer_id=owning_customer_id WHERE cashout_uuid=in_cashout_uuid; @@ -1141,7 +1145,13 @@ ELSIF already_confirmed OR out_aborted OR out_no_cashout_payto THEN RETURN; END IF; --- Check challenge +-- check conversion +SELECT too_small OR amount_credit_local!=to_amount INTO out_bad_conversion FROM conversion_to(amount_debit_local, 'sell'::text); +IF out_bad_conversion THEN + RETURN; +END IF; + +-- check challenge SELECT NOT ok, no_retry INTO out_bad_code, out_no_retry FROM challenge_try(challenge_id, in_tan_code, in_now_date); @@ -1164,7 +1174,7 @@ FROM bank_wire_transfer( admin_account_id, wallet_account_id, subject_local, - amount_local, + amount_debit_local, in_now_date, 'not-used', 'not-used',