commit 77ea0eb003a2157172350d737573ed587d556a6d
parent 1f67cce74dbb6cd297cde4f7ac8e7da86a42b541
Author: Antoine A <>
Date: Sun, 23 Jun 2024 19:10:34 +0200
bank: improve max_amount and add more tests
Diffstat:
5 files changed, 72 insertions(+), 7 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt
@@ -44,10 +44,14 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
get("/taler-integration/withdrawal-operation/{wopid}") {
val uuid = call.uuidPath("wopid")
val params = StatusParams.extract(call.request.queryParameters)
- val op = db.withdrawal.pollStatus(uuid, params, ctx.wireMethod)?.copy(
+ val op = db.withdrawal.pollStatus(
+ uuid,
+ params,
+ ctx.wireMethod,
+ ctx.maxAmount
+ )?.copy(
card_fees = ctx.wireTransferFees,
- min_amount = ctx.minAmount,
- max_amount = ctx.maxAmount
+ min_amount = ctx.minAmount
) ?: throw notFound(
"Withdrawal operation '$uuid' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -337,7 +337,12 @@ class WithdrawalDAO(private val db: Database) {
}
/** Pool public status of operation [uuid] */
- suspend fun pollStatus(uuid: UUID, params: StatusParams, wire: WireMethod): BankWithdrawalOperationStatus? =
+ suspend fun pollStatus(
+ uuid: UUID,
+ params: StatusParams,
+ wire: WireMethod,
+ maxAmount: TalerAmount
+ ): BankWithdrawalOperationStatus? =
poll(uuid, params, status = { it.status }) {
db.serializableRead(
"""
@@ -357,18 +362,24 @@ class WithdrawalDAO(private val db: Database) {
,confirmation_done
,internal_payto_uri
,reserve_pub
- ,selected_exchange_payto
+ ,selected_exchange_payto
+ ,(max_amount).val as max_amount_val
+ ,(max_amount).frac as max_amount_frac
FROM taler_withdrawal_operations
JOIN bank_accounts ON (wallet_bank_account=bank_account_id)
+ ,account_max_amount(bank_account_id, (?, ?)::taler_amount) AS max_amount
WHERE withdrawal_uuid=?
"""
) {
- setObject(1, uuid)
+ setLong(1, maxAmount.value)
+ setInt(2, maxAmount.frac)
+ setObject(3, uuid)
oneOrNull {
BankWithdrawalOperationStatus(
status = WithdrawalStatus.valueOf(it.getString("status")),
amount = it.getOptAmount("amount", db.bankCurrency),
suggested_amount = it.getOptAmount("suggested_amount", db.bankCurrency),
+ max_amount = it.getAmount("max_amount", db.bankCurrency),
selection_done = it.getBoolean("selection_done"),
transfer_done = it.getBoolean("confirmation_done"),
aborted = it.getBoolean("aborted"),
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -60,7 +60,7 @@ class BankIntegrationApiTest {
assert(!it.transfer_done)
assertEquals(it.card_fees, TalerAmount.zero("KUDOS"))
assertEquals(it.min_amount, TalerAmount.zero("KUDOS"))
- assertEquals(it.max_amount, TalerAmount.max("KUDOS"))
+ assertEquals(it.max_amount, TalerAmount("KUDOS:10"))
assertEquals(amount, it.amount)
assertEquals(suggested, it.suggested_amount)
assertEquals(listOf("iban"), it.wire_types)
@@ -201,6 +201,7 @@ class BankIntegrationApiTest {
client.get("/taler-integration/withdrawal-operation/$uuid")
.assertOkJson<BankWithdrawalOperationStatus> {
assertEquals(TalerAmount("KUDOS:1.1"), it.amount)
+ assertEquals(TalerAmount("KUDOS:10"), it.max_amount)
}
}
}
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -26,6 +26,7 @@ import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.bank.auth.*
import tech.libeufin.common.*
+import tech.libeufin.common.db.*
import java.time.Duration
import java.time.Instant
import java.util.*
@@ -1444,6 +1445,34 @@ class CoreBankWithdrawalApiTest {
}.assertNoContent()
}
}
+
+ @Test
+ fun confirmWithFee() = bankSetup(conf = "test_with_fees.conf") { db ->
+ suspend fun run(amount: TalerAmount): HttpResponse {
+ val uuid = UUID.randomUUID()
+ // Create a selected withdrawal directly in the database to bypass checks
+ db.serializableWrite("""
+ INSERT INTO taler_withdrawal_operations(withdrawal_uuid,amount,selected_exchange_payto,selection_done,wallet_bank_account,creation_date)
+ VALUES (?, (?, ?)::taler_amount, ?, true, 3, 0)
+ """) {
+ setObject(1, uuid)
+ setLong(2, amount.value)
+ setInt(3, amount.frac)
+ setString(4, exchangePayto.canonical)
+ execute()
+ }
+
+ return client.postA("/accounts/customer/withdrawals/$uuid/confirm")
+ }
+
+ // Check insufficient fund
+ for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) {
+ run(TalerAmount(amount)).assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
+ }
+
+ // Check OK
+ run(TalerAmount("KUDOS:9.9"))
+ }
}
class CoreBankCashoutApiTest {
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -163,6 +163,26 @@ END IF;
END $$;
COMMENT ON FUNCTION account_balance_is_sufficient IS 'Check if an account have enough fund to transfer an amount.';
+CREATE FUNCTION account_max_amount(
+ IN in_account_id INT8,
+ IN in_max_amount taler_amount,
+ OUT out_max_amount taler_amount
+)
+LANGUAGE plpgsql AS $$
+BEGIN
+-- add balance and max_debt
+WITH added AS (
+ SELECT amount_add(balance, max_debt) as sum FROM bank_accounts
+ WHERE bank_account_id=in_account_id
+) SELECT (sum).val, (sum).frac
+ INTO out_max_amount.val, out_max_amount.frac
+ FROM added;
+
+IF in_max_amount.val < out_max_amount.val
+ OR (in_max_amount.val = out_max_amount.val OR in_max_amount.frac < out_max_amount.frac) THEN
+ out_max_amount = in_max_amount;
+END IF;
+END $$;
CREATE FUNCTION bank_wire_transfer(
IN in_creditor_account_id INT8,