commit 336568dc7e9d70e14c12e69e5b3b448968f70065
parent dcac26dacf579751b96a145840815980b1d34a3d
Author: Antoine A <>
Date: Wed, 14 Aug 2024 12:43:50 +0200
bank: fix withdrawal max_amount computation
Diffstat:
3 files changed, 116 insertions(+), 57 deletions(-)
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
@@ -17,17 +17,18 @@
* <http://www.gnu.org/licenses/>
*/
+import io.ktor.http.*
import org.junit.Test
import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult
import tech.libeufin.bank.db.WithdrawalDAO.WithdrawalCreationResult
import tech.libeufin.common.*
-import tech.libeufin.common.db.one
+import tech.libeufin.common.db.*
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
class AmountTest {
- // Test amount computation in database
+ // Test amount computation in db
@Test
fun computationTest() = bankSetup { db -> db.conn { conn ->
conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 100000 WHERE internal_payto_uri = '${customerPayto.canonical}'")
@@ -38,50 +39,45 @@ class AmountTest {
,max_debt = (?, ?)::taler_amount
WHERE internal_payto_uri = '${merchantPayto.canonical}'
""")
- suspend fun routine(balance: TalerAmount, due: TalerAmount, hasBalanceDebt: Boolean, maxDebt: TalerAmount): Boolean {
+ suspend fun routine(
+ balance: TalerAmount,
+ hasDebt: Boolean,
+ maxDebt: TalerAmount,
+ amount: TalerAmount
+ ): Boolean {
stmt.setLong(1, balance.value)
stmt.setInt(2, balance.frac)
- stmt.setBoolean(3, hasBalanceDebt)
+ stmt.setBoolean(3, hasDebt)
stmt.setLong(4, maxDebt.value)
stmt.setInt(5, maxDebt.frac)
// Check bank transaction
stmt.executeUpdate()
- val txRes = db.transaction.create(
- creditAccountPayto = customerPayto,
- debitAccountUsername = "merchant",
- subject = "test",
- amount = due,
- timestamp = Instant.now(),
- is2fa = false,
- requestUid = null,
- wireTransferFees = TalerAmount.zero("KUDOS"),
- minAmount = TalerAmount.zero("KUDOS"),
- maxAmount = TalerAmount.max("KUDOS")
- )
- val txBool = when (txRes) {
- BankTransactionResult.BalanceInsufficient -> false
- is BankTransactionResult.Success -> true
+ val txRes = client.postA("/accounts/merchant/transactions") {
+ json {
+ "payto_uri" to "$customerPayto?message="
+ "amount" to amount
+ }
+ }
+ val txBool = when {
+ txRes.isStatus(HttpStatusCode.OK, null) -> true
+ txRes.isStatus(HttpStatusCode.Conflict, TalerErrorCode.BANK_UNALLOWED_DEBIT) -> false
else -> throw Exception("Unexpected error $txRes")
}
// Check whithdraw
stmt.executeUpdate()
- for ((amount, suggested) in listOf(Pair(due, null), Pair(null, due), Pair(due, due))) {
- val wRes = db.withdrawal.create(
- login = "merchant",
- uuid = UUID.randomUUID(),
- amount = due,
- suggested_amount = null,
- timestamp = Instant.now(),
- wireTransferFees = TalerAmount.zero("KUDOS"),
- minAmount = TalerAmount.zero("KUDOS"),
- maxAmount = TalerAmount.max("KUDOS")
- )
- val wBool = when (wRes) {
- WithdrawalCreationResult.BalanceInsufficient -> false
- WithdrawalCreationResult.Success -> true
- else -> throw Exception("Unexpected error $txRes")
+ for ((amount, suggested) in listOf(Pair(amount, null), Pair(null, amount), Pair(amount, amount))) {
+ val wRes = client.postA("/accounts/merchant/withdrawals") {
+ json {
+ "amount" to amount
+ "suggested_amount" to suggested
+ }
+ }
+ val wBool = when {
+ wRes.isStatus(HttpStatusCode.OK, null) -> true
+ wRes.isStatus(HttpStatusCode.Conflict, TalerErrorCode.BANK_UNALLOWED_DEBIT) -> false
+ else -> throw Exception("Unexpected error $wRes")
}
// Logic must be the same
assertEquals(wBool, txBool)
@@ -93,44 +89,95 @@ class AmountTest {
// Balance enough, assert for true
assert(routine(
balance = TalerAmount(10, 0, "KUDOS"),
- due = TalerAmount(8, 0, "KUDOS"),
- hasBalanceDebt = false,
- maxDebt = TalerAmount(100, 0, "KUDOS")
+ hasDebt = false,
+ maxDebt = TalerAmount(100, 0, "KUDOS"),
+ amount = TalerAmount(8, 0, "KUDOS"),
))
// Balance still sufficient, thanks for big enough debt permission. Assert true.
assert(routine(
balance = TalerAmount(10, 0, "KUDOS"),
- due = TalerAmount(80, 0, "KUDOS"),
- hasBalanceDebt = false,
- maxDebt = TalerAmount(100, 0, "KUDOS")
+ hasDebt = false,
+ maxDebt = TalerAmount(100, 0, "KUDOS"),
+ amount = TalerAmount(80, 0, "KUDOS"),
))
// Balance not enough, max debt cannot cover, asserting for false.
assert(!routine(
balance = TalerAmount(10, 0, "KUDOS"),
- due = TalerAmount(80, 0, "KUDOS"),
- hasBalanceDebt = true,
- maxDebt = TalerAmount(50, 0, "KUDOS")
+ hasDebt = true,
+ maxDebt = TalerAmount(50, 0, "KUDOS"),
+ amount = TalerAmount(80, 0, "KUDOS"),
))
// Balance becomes enough, due to a larger max debt, asserting for true.
assert(routine(
balance = TalerAmount(10, 0, "KUDOS"),
- due = TalerAmount(80, 0, "KUDOS"),
- hasBalanceDebt = false,
- maxDebt = TalerAmount(70, 0, "KUDOS")
+ hasDebt = false,
+ maxDebt = TalerAmount(70, 0, "KUDOS"),
+ amount = TalerAmount(80, 0, "KUDOS"),
))
// Max debt not enough for the smallest fraction, asserting for false
assert(!routine(
balance = TalerAmount(0, 0, "KUDOS"),
- due = TalerAmount(0, 2, "KUDOS"),
- hasBalanceDebt = false,
- maxDebt = TalerAmount(0, 1, "KUDOS")
+ hasDebt = false,
+ maxDebt = TalerAmount(0, 1, "KUDOS"),
+ amount = TalerAmount(0, 2, "KUDOS"),
))
// Same as above, but already in debt.
assert(!routine(
balance = TalerAmount(0, 1, "KUDOS"),
- due = TalerAmount(0, 1, "KUDOS"),
- hasBalanceDebt = true,
- maxDebt = TalerAmount(0, 1, "KUDOS")
+ hasDebt = true,
+ maxDebt = TalerAmount(0, 1, "KUDOS"),
+ amount = TalerAmount(0, 1, "KUDOS"),
+ ))
+ }}
+
+ // Max withdrawal amount computation in db
+ @Test
+ fun maxComputationTest() = bankSetup { db -> db.conn { conn ->
+ val update = conn.prepareStatement("""
+ UPDATE libeufin_bank.bank_accounts
+ SET balance = (?, ?)::taler_amount
+ ,has_debt = ?
+ ,max_debt = (?, ?)::taler_amount
+ WHERE bank_account_id = 1
+ """)
+ val select = conn.prepareStatement("""
+ SELECT
+ (max_amount).val as max_amount_val
+ ,(max_amount).frac as max_amount_frac
+ FROM account_max_amount(1, (?, ?)::taler_amount) AS max_amount
+ """)
+ select.apply {
+ val max = TalerAmount.max("KUDOS")
+ setLong(1, max.value)
+ setInt(2, max.frac)
+ }
+ suspend fun routine(
+ balance: TalerAmount,
+ hasDebt: Boolean,
+ maxDebt: TalerAmount
+ ): TalerAmount {
+ update.apply {
+ setLong(1, balance.value)
+ setInt(2, balance.frac)
+ setBoolean(3, hasDebt)
+ setLong(4, maxDebt.value)
+ setInt(5, maxDebt.frac)
+ executeUpdate()
+ }
+ return select.one { it.getAmount("max_amount", "KUDOS") }
+ }
+
+ // Without debt
+ assertEquals(TalerAmount(110, 3, "KUDOS"), routine(
+ balance = TalerAmount(10, 1, "KUDOS"),
+ hasDebt = false,
+ maxDebt = TalerAmount(100, 2, "KUDOS"),
+ ))
+ // With debt
+ assertEquals(TalerAmount(90, 1, "KUDOS"), routine(
+ balance = TalerAmount(10, 1, "KUDOS"),
+ hasDebt = true,
+ maxDebt = TalerAmount(100, 2, "KUDOS"),
))
}}
diff --git a/common/src/main/kotlin/Client.kt b/common/src/main/kotlin/Client.kt
@@ -78,6 +78,15 @@ suspend inline fun <reified B> HttpResponse.assertAcceptedJson(lambda: (B) -> Un
/* ----- Assert ----- */
+suspend fun HttpResponse.isStatus(status: HttpStatusCode, err: TalerErrorCode?): Boolean {
+ if (status != this.status) return false
+ if (err != null) {
+ val body = json<TalerError>()
+ return err.code == body.code
+ }
+ return true
+}
+
suspend fun HttpResponse.assertStatus(status: HttpStatusCode, err: TalerErrorCode?): HttpResponse {
assertEquals(status, this.status, if (err != null) "$err" else err)
if (err != null) {
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -171,12 +171,15 @@ CREATE FUNCTION account_max_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
+WITH computed AS (
+ SELECT CASE has_debt
+ WHEN false THEN amount_add(balance, max_debt)
+ ELSE (SELECT diff FROM amount_left_minus_right(max_debt, balance))
+ END AS amount
+ FROM bank_accounts WHERE bank_account_id=in_account_id
+) SELECT (amount).val, (amount).frac
INTO out_max_amount.val, out_max_amount.frac
- FROM added;
+ FROM computed;
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