libeufin

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

commit bd7c4f46f76ea2414b965d163f0e4910c0e27113
parent 059c62f86ac618ee2231141926d8e920b9de0e5f
Author: Antoine A <>
Date:   Tue, 17 Oct 2023 16:17:02 +0000

Move all amount computation logic into the database

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Authentication.kt | 2--
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 41++++++++++++++++++++++-------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 40+++++++++++++++++++++++++++++-----------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt | 28----------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 45---------------------------------------------
Mbank/src/test/kotlin/AmountTest.kt | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mbank/src/test/kotlin/BankIntegrationApiTest.kt | 19+++++++++----------
Mbank/src/test/kotlin/DatabaseTest.kt | 11++++-------
Mdatabase-versioning/procedures.sql | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
9 files changed, 219 insertions(+), 175 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt @@ -9,7 +9,6 @@ import java.time.Instant /** Authenticate admin */ suspend fun ApplicationCall.authAdmin(db: Database, scope: TokenScope) { - // TODO when all endpoints use this function we can use an optimized database request that only query the customer login val login = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login") if (login != "admin") { throw unauthorized("Only administrator allowed") @@ -19,7 +18,6 @@ suspend fun ApplicationCall.authAdmin(db: Database, scope: TokenScope) { /** Authenticate and check access rights */ suspend fun ApplicationCall.authCheck(db: Database, scope: TokenScope, withAdmin: Boolean = false, requireAdmin: Boolean = false): Pair<String, Boolean> { - // TODO when all endpoints use this function we can use an optimized database request that only query the customer login val authLogin = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login") val login = accountLogin() if (requireAdmin && authLogin != "admin") { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -101,32 +101,35 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { } // WITHDRAWAL ENDPOINTS post("/accounts/{USERNAME}/withdrawals") { - call.authCheck(db, TokenScope.readwrite) + val (login, _) = call.authCheck(db, TokenScope.readwrite) val req = call.receive<BankAccountCreateWithdrawalRequest>() // Checking that the user has enough funds. + if (req.amount.currency != ctx.currency) throw badRequest("Wrong currency: ${req.amount.currency}") - val b = call.bankAccount(db) - // TODO balance check only in database - if (!isBalanceEnough( - balance = b.expectBalance(), due = req.amount, maxDebt = b.maxDebt, hasBalanceDebt = b.hasDebt - ) - ) throw forbidden( - hint = "Insufficient funds to withdraw with Taler", - talerErrorCode = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT - ) // Auth and funds passed, create the operation now! val opId = UUID.randomUUID() - if (!db.talerWithdrawalCreate( - opId, b.expectRowId(), req.amount + when (db.talerWithdrawalCreate(login, opId, req.amount)) { + WithdrawalCreationResult.ACCOUNT_NOT_FOUND -> throw notFound( + "Customer $login not found", + TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) - ) throw internalServerError("Bank failed at creating the withdraw operation.") - - val bankBaseUrl = call.request.getBaseUrl() ?: throw internalServerError("Bank could not find its own base URL") - call.respond( - BankAccountCreateWithdrawalResponse( - withdrawal_id = opId.toString(), taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl, opId.toString()) + WithdrawalCreationResult.ACCOUNT_IS_EXCHANGE -> throw conflict( + "Exchange account cannot perform withdrawal operation", + TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT ) - ) + WithdrawalCreationResult.BALANCE_INSUFFICIENT -> throw conflict( + "Insufficient funds to withdraw with Taler", + TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT + ) + WithdrawalCreationResult.SUCCESS -> { + val bankBaseUrl = call.request.getBaseUrl() ?: throw internalServerError("Bank could not find its own base URL") + call.respond( + BankAccountCreateWithdrawalResponse( + withdrawal_id = opId.toString(), taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl, opId.toString()) + ) + ) + } + } } get("/withdrawals/{withdrawal_id}") { val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -192,7 +192,7 @@ private fun PreparedStatement.executeUpdateViolation(): Boolean { } class Database(dbConfig: String, private val bankCurrency: String): java.io.Closeable { - private val dbPool: HikariDataSource + val dbPool: HikariDataSource private val notifWatcher: NotificationWatcher init { @@ -1091,21 +1091,31 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos // WITHDRAWALS suspend fun talerWithdrawalCreate( + walletAccountUsername: String, opUUID: UUID, - walletBankAccount: Long, amount: TalerAmount - ): Boolean = conn { conn -> + ): WithdrawalCreationResult = conn { conn -> val stmt = conn.prepareStatement(""" - INSERT INTO - taler_withdrawal_operations - (withdrawal_uuid, wallet_bank_account, amount) - VALUES (?,?,(?,?)::taler_amount) - """) // Take all defaults from the SQL. - stmt.setObject(1, opUUID) - stmt.setLong(2, walletBankAccount) + SELECT + out_account_not_found, + out_account_is_exchange, + out_balance_insufficient + FROM create_taler_withdrawal(?, ?, (?,?)::taler_amount); + """) + stmt.setString(1, walletAccountUsername) + stmt.setObject(2, opUUID) stmt.setLong(3, amount.value) stmt.setInt(4, amount.frac) - stmt.executeUpdateViolation() + stmt.executeQuery().use { + when { + !it.next() -> + throw internalServerError("No result from DB procedure create_taler_withdrawal") + it.getBoolean("out_account_not_found") -> WithdrawalCreationResult.ACCOUNT_NOT_FOUND + it.getBoolean("out_account_is_exchange") -> WithdrawalCreationResult.ACCOUNT_IS_EXCHANGE + it.getBoolean("out_balance_insufficient") -> WithdrawalCreationResult.BALANCE_INSUFFICIENT + else -> WithdrawalCreationResult.SUCCESS + } + } } suspend fun talerWithdrawalGet(opUUID: UUID): TalerWithdrawalOperation? = conn { conn -> val stmt = conn.prepareStatement(""" @@ -1616,6 +1626,14 @@ enum class TalerAddIncomingResult { SUCCESS } +/** Result status of withdrawal operation creation */ +enum class WithdrawalCreationResult { + SUCCESS, + ACCOUNT_NOT_FOUND, + ACCOUNT_IS_EXCHANGE, + BALANCE_INSUFFICIENT +} + /** * This type communicates the result of a database operation * to confirm one withdrawal operation. diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt @@ -253,34 +253,6 @@ class TalerAmount { } } - fun normalize(): TalerAmount { - if (frac > FRACTION_BASE) { - val overflow = frac / FRACTION_BASE - val normalFrac = frac % FRACTION_BASE - val normalValue = value + overflow - if (normalValue < overflow || normalValue > MAX_SAFE_INTEGER) - throw badRequest("Amount value overflowed") - return TalerAmount( - value = normalValue, frac = normalFrac, currency = currency - ) - } - return this - } - - operator fun plus(other: TalerAmount): TalerAmount { - if (currency != other.currency) throw badRequest( - "Currency mismatch, balance '$currency', price '${other.currency}'", - TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH - ) - val valueAdd = value + other.value - if (valueAdd < value || valueAdd > MAX_SAFE_INTEGER) throw badRequest("Amount value overflowed") - val fracAdd = frac + other.frac - if (fracAdd < frac) throw badRequest("Amount fraction overflowed") - return TalerAmount( - value = valueAdd, frac = fracAdd, currency = currency - ).normalize() - } - override fun equals(other: Any?): Boolean { return other is TalerAmount && other.value == this.value && diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -44,15 +44,6 @@ fun ApplicationCall.expectUriComponent(componentName: String) = typealias ResourceName = String /** - * Checks if the input Customer has the rights over ResourceName. - */ -fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean { - if (c.login == this) return true - if (c.login == "admin" && withAdmin) return true - return false -} - -/** * Factors out the retrieval of the resource name from * the URI. The resource looked for defaults to "USERNAME" * as this is frequently mentioned resource along the endpoints. @@ -127,42 +118,6 @@ fun badRequest( fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}" /** - * Checks whether the balance could cover the due amount. Returns true - * when it does, false otherwise. Note: this function is only a checker, - * meaning that no actual business state should change after it runs. - * The place where business states change is in the SQL that's loaded in - * the database. - */ -fun isBalanceEnough( - balance: TalerAmount, due: TalerAmount, maxDebt: TalerAmount, hasBalanceDebt: Boolean -): Boolean { - val normalMaxDebt = maxDebt.normalize() // Very unlikely to be needed. - if (hasBalanceDebt) { - val chargedBalance = balance + due - if (chargedBalance.value > normalMaxDebt.value) return false // max debt surpassed - if ((chargedBalance.value == normalMaxDebt.value) && (chargedBalance.frac > maxDebt.frac)) return false - return true - } - /** - * Balance doesn't have debt, but it MIGHT get one. The following - * block calculates how much debt the balance would get, should a - * subtraction of 'due' occur. - */ - if (balance.currency != due.currency) throw badRequest( - "Currency mismatch, balance '${balance.currency}', due '${due.currency}'", - TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH - ) - val valueDiff = if (balance.value < due.value) due.value - balance.value else 0L - val fracDiff = if (balance.frac < due.frac) due.frac - balance.frac else 0 - // Getting the normalized version of such diff. - val normalDiff = TalerAmount(valueDiff, fracDiff, balance.currency).normalize() - // Failing if the normalized diff surpasses the max debt. - if (normalDiff.value > normalMaxDebt.value) return false - if ((normalDiff.value == normalMaxDebt.value) && (normalDiff.frac > normalMaxDebt.frac)) return false - return true -} - -/** * Builds the taler://withdraw-URI. Such URI will serve the requests * from wallets, when they need to manage the operation. For example, * a URI like taler://withdraw/$BANK_URL/taler-integration/$WO_ID needs diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -19,49 +19,103 @@ import org.junit.Test +import org.postgresql.jdbc.PgConnection import tech.libeufin.bank.* -import kotlin.test.assertEquals +import kotlin.test.* +import java.time.Instant +import java.util.* class AmountTest { + + // Test amount computation in database @Test - fun amountAdditionTest() { + fun computationTest() = bankSetup { db -> + val conn = db.dbPool.getConnection().unwrap(PgConnection::class.java) + conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 100000 WHERE internal_payto_uri = '${IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ").canonical}'") + val stmt = conn.prepareStatement(""" + UPDATE libeufin_bank.bank_accounts + SET balance = (?, ?)::taler_amount + ,has_debt = ? + ,max_debt = (?, ?)::taler_amount + WHERE internal_payto_uri = '${IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ").canonical}' + """) + suspend fun routine(balance: TalerAmount, due: TalerAmount, hasBalanceDebt: Boolean, maxDebt: TalerAmount): Boolean { + stmt.setLong(1, balance.value) + stmt.setInt(2, balance.frac) + stmt.setBoolean(3, hasBalanceDebt) + stmt.setLong(4, maxDebt.value) + stmt.setInt(5, maxDebt.frac) + + // Check bank transaction + stmt.executeUpdate() + val txRes = db.bankTransaction( + creditAccountPayto = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"), + debitAccountUsername = "merchant", + subject = "test", + amount = due, + timestamp = Instant.now(), + ) + val txBool = when (txRes) { + BankTransactionResult.BALANCE_INSUFFICIENT -> false + BankTransactionResult.SUCCESS -> true + else -> throw Exception("Unexpected error $txRes") + } + + // Check whithdraw + stmt.executeUpdate() + val wRes = db.talerWithdrawalCreate( + walletAccountUsername = "merchant", + opUUID = UUID.randomUUID(), + amount = due, + ) + val wBool = when (wRes) { + WithdrawalCreationResult.BALANCE_INSUFFICIENT -> false + WithdrawalCreationResult.SUCCESS -> true + else -> throw Exception("Unexpected error $txRes") + } + + // Logic must be the same + assertEquals(wBool, txBool) + return txBool + } + // Balance enough, assert for true - assert(isBalanceEnough( + assert(routine( balance = TalerAmount(10, 0, "KUDOS"), due = TalerAmount(8, 0, "KUDOS"), hasBalanceDebt = false, maxDebt = TalerAmount(100, 0, "KUDOS") )) // Balance still sufficient, thanks for big enough debt permission. Assert true. - assert(isBalanceEnough( + assert(routine( balance = TalerAmount(10, 0, "KUDOS"), due = TalerAmount(80, 0, "KUDOS"), hasBalanceDebt = false, maxDebt = TalerAmount(100, 0, "KUDOS") )) // Balance not enough, max debt cannot cover, asserting for false. - assert(!isBalanceEnough( + assert(!routine( balance = TalerAmount(10, 0, "KUDOS"), due = TalerAmount(80, 0, "KUDOS"), hasBalanceDebt = true, maxDebt = TalerAmount(50, 0, "KUDOS") )) // Balance becomes enough, due to a larger max debt, asserting for true. - assert(isBalanceEnough( + assert(routine( balance = TalerAmount(10, 0, "KUDOS"), due = TalerAmount(80, 0, "KUDOS"), hasBalanceDebt = false, maxDebt = TalerAmount(70, 0, "KUDOS") )) // Max debt not enough for the smallest fraction, asserting for false - assert(!isBalanceEnough( + assert(!routine( balance = TalerAmount(0, 0, "KUDOS"), due = TalerAmount(0, 2, "KUDOS"), hasBalanceDebt = false, maxDebt = TalerAmount(0, 1, "KUDOS") )) // Same as above, but already in debt. - assert(!isBalanceEnough( + assert(!routine( balance = TalerAmount(0, 1, "KUDOS"), due = TalerAmount(0, 1, "KUDOS"), hasBalanceDebt = true, @@ -97,21 +151,4 @@ class AmountTest { assertEquals(amount, TalerAmount(amount).toString()) } } - - @Test - fun normalize() { - assertEquals(TalerAmount("EUR:6"), TalerAmount(4L, 2 * TalerAmount.FRACTION_BASE, "EUR").normalize()) - assertEquals(TalerAmount("EUR:6.00000001"), TalerAmount(4L, 2 * TalerAmount.FRACTION_BASE + 1, "EUR").normalize()) - assertException("Amount value overflowed") { TalerAmount(Long.MAX_VALUE, 2 * TalerAmount.FRACTION_BASE + 1, "EUR").normalize() } - assertException("Amount value overflowed") { TalerAmount(MAX_SAFE_INTEGER, 2 * TalerAmount.FRACTION_BASE + 1, "EUR").normalize() } - } - - @Test - fun add() { - assertEquals(TalerAmount("EUR:6.41") + TalerAmount("EUR:4.69"), TalerAmount("EUR:11.1")) - assertException("Amount value overflowed") { TalerAmount(MAX_SAFE_INTEGER - 5, 0, "EUR") + TalerAmount(6, 0, "EUR") } - assertException("Amount value overflowed") { TalerAmount(Long.MAX_VALUE, 0, "EUR") + TalerAmount(1, 0, "EUR") } - assertException("Amount value overflowed") { TalerAmount(MAX_SAFE_INTEGER - 5, TalerAmount.FRACTION_BASE - 1, "EUR") + TalerAmount(5, 2, "EUR") } - assertException("Amount fraction overflowed") { TalerAmount(0, Int.MAX_VALUE, "EUR") + TalerAmount(0, 1, "EUR") } - } } \ No newline at end of file diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -13,8 +13,7 @@ import tech.libeufin.util.CryptoUtil import tech.libeufin.util.stripIbanPayto import java.util.* import java.time.Instant -import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.* import randHashCode class BankIntegrationApiTest { @@ -23,9 +22,9 @@ class BankIntegrationApiTest { fun intSelect() = bankSetup { db -> val uuid = UUID.randomUUID() // insert new. - assert(db.talerWithdrawalCreate( + assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( opUUID = uuid, - walletBankAccount = 1L, + walletAccountUsername = "merchant", amount = TalerAmount(1, 0, "KUDOS") )) @@ -43,9 +42,9 @@ class BankIntegrationApiTest { fun intGet() = bankSetup { db -> val uuid = UUID.randomUUID() // insert new. - assert(db.talerWithdrawalCreate( + assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( opUUID = uuid, - walletBankAccount = 1L, + walletAccountUsername = "merchant", amount = TalerAmount(1, 0, "KUDOS") )) @@ -58,9 +57,9 @@ class BankIntegrationApiTest { fun withdrawalAbort() = bankSetup { db -> val uuid = UUID.randomUUID() // insert new. - assert(db.talerWithdrawalCreate( + assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( opUUID = uuid, - walletBankAccount = 1L, + walletAccountUsername = "merchant", amount = TalerAmount(1, 0, "KUDOS") )) val op = db.talerWithdrawalGet(uuid) @@ -95,9 +94,9 @@ class BankIntegrationApiTest { fun withdrawalConfirmation() = bankSetup { db -> // Artificially making a withdrawal operation for merchant. val uuid = UUID.randomUUID() - assert(db.talerWithdrawalCreate( + assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( opUUID = uuid, - walletBankAccount = 1L, + walletAccountUsername = "merchant", amount = TalerAmount(1, 0, "KUDOS") )) // Specifying the exchange via its Payto URI. diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -26,10 +26,7 @@ import java.time.Instant import java.util.Random import java.util.UUID import kotlin.experimental.inv -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* // Foo pays Bar with custom subject. fun genTx( @@ -283,14 +280,14 @@ class DatabaseTest { assert(db.customerCreate(customerBar) != null) // plays the exchange. assert(db.bankAccountCreate(bankAccountBar) != null) // insert new. - assert(db.talerWithdrawalCreate( + assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( + "bar", uuid, - 1L, TalerAmount(1, 0, currency) )) // get it. val op = db.talerWithdrawalGet(uuid) - assert(op?.walletBankAccount == 1L && op.withdrawalUuid == uuid) + assert(op?.walletBankAccount == 2L && op.withdrawalUuid == uuid) // Setting the details. assert(db.talerWithdrawalSetDetails( opUuid = uuid, diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql @@ -508,6 +508,80 @@ IF out_balance_insufficient THEN END IF; END $$; +CREATE OR REPLACE FUNCTION create_taler_withdrawal( + IN in_account_username TEXT, + IN in_withdrawal_uuid UUID, + IN in_amount taler_amount, + -- Error status + OUT out_account_not_found BOOLEAN, + OUT out_account_is_exchange BOOLEAN, + OUT out_balance_insufficient BOOLEAN +) +LANGUAGE plpgsql AS $$ +DECLARE +account_id BIGINT; +account_has_debt BOOLEAN; +account_balance taler_amount; +account_max_debt taler_amount; +BEGIN + +-- check account exists +SELECT + has_debt, bank_account_id, + (balance).val, (balance).frac, + (max_debt).val, (max_debt).frac, + is_taler_exchange + INTO + account_has_debt, account_id, + account_balance.val, account_balance.frac, + account_max_debt.val, account_max_debt.frac, + out_account_is_exchange + FROM bank_accounts + JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id + WHERE login=in_account_username; +IF NOT FOUND THEN + out_account_not_found=TRUE; + RETURN; +ELSIF out_account_is_exchange THEN + RETURN; +END IF; + +-- check enough funds +IF account_has_debt THEN + -- debt case: simply checking against the max debt allowed. + CALL amount_add(account_balance, in_amount, account_balance); + SELECT NOT ok + INTO out_balance_insufficient + FROM amount_left_minus_right(account_max_debt, account_balance); + IF out_balance_insufficient THEN + RETURN; + END IF; +ELSE -- not a debt account + SELECT NOT ok + INTO out_balance_insufficient + FROM amount_left_minus_right(account_balance, in_amount); + IF out_balance_insufficient THEN + -- debtor will switch to debt: determine their new negative balance. + SELECT + (diff).val, (diff).frac + INTO + account_balance.val, account_balance.frac + FROM amount_left_minus_right(in_amount, account_balance); + SELECT NOT ok + INTO out_balance_insufficient + FROM amount_left_minus_right(account_max_debt, account_balance); + IF out_balance_insufficient THEN + RETURN; + END IF; + END IF; +END IF; + +-- Create withdrawal operation +INSERT INTO taler_withdrawal_operations + (withdrawal_uuid, wallet_bank_account, amount) + VALUES (in_withdrawal_uuid, account_id, in_amount); +END $$; + CREATE OR REPLACE FUNCTION confirm_taler_withdrawal( IN in_withdrawal_uuid uuid, IN in_confirmation_date BIGINT, @@ -620,22 +694,19 @@ AS $$ DECLARE debtor_has_debt BOOLEAN; debtor_balance taler_amount; +debtor_max_debt taler_amount; debtor_payto_uri TEXT; debtor_name TEXT; -creditor_payto_uri TEXT; -creditor_name TEXT; -debtor_max_debt taler_amount; creditor_has_debt BOOLEAN; creditor_balance taler_amount; +creditor_payto_uri TEXT; +creditor_name TEXT; potential_balance taler_amount; -potential_balance_check BOOLEAN; new_debtor_balance taler_amount; new_debtor_balance_ok BOOLEAN; new_creditor_balance taler_amount; will_debtor_have_debt BOOLEAN; will_creditor_have_debt BOOLEAN; -amount_at_least_debit BOOLEAN; -potential_balance_ok BOOLEAN; new_debit_row_id BIGINT; new_credit_row_id BIGINT; BEGIN @@ -695,27 +766,26 @@ IF debtor_has_debt THEN CALL amount_add(debtor_balance, in_amount, potential_balance); - SELECT ok - INTO potential_balance_check + SELECT NOT ok + INTO out_balance_insufficient FROM amount_left_minus_right(debtor_max_debt, potential_balance); - IF NOT potential_balance_check THEN - out_balance_insufficient=TRUE; + IF out_balance_insufficient THEN RETURN; END IF; new_debtor_balance=potential_balance; will_debtor_have_debt=TRUE; ELSE -- not a debt account SELECT - ok, + NOT ok, (diff).val, (diff).frac INTO - potential_balance_ok, + out_balance_insufficient, potential_balance.val, potential_balance.frac FROM amount_left_minus_right(debtor_balance, in_amount); - IF potential_balance_ok THEN -- debtor has enough funds in the (positive) balance. + IF NOT out_balance_insufficient THEN -- debtor has enough funds in the (positive) balance. new_debtor_balance=potential_balance; will_debtor_have_debt=FALSE; ELSE -- debtor will switch to debt: determine their new negative balance. @@ -726,16 +796,16 @@ ELSE -- not a debt account FROM amount_left_minus_right(in_amount, debtor_balance); will_debtor_have_debt=TRUE; - SELECT ok - INTO potential_balance_check + SELECT NOT ok + INTO out_balance_insufficient FROM amount_left_minus_right(debtor_max_debt, new_debtor_balance); - IF NOT potential_balance_check THEN - out_balance_insufficient=TRUE; + IF out_balance_insufficient THEN RETURN; END IF; END IF; END IF; +out_balance_insufficient=FALSE; -- CREDITOR SIDE. -- Here we figure out whether the creditor would switch @@ -747,28 +817,23 @@ IF NOT creditor_has_debt THEN -- easy case. ELSE -- creditor had debit but MIGHT switch to credit. SELECT (diff).val, (diff).frac, - ok + NOT ok INTO new_creditor_balance.val, new_creditor_balance.frac, - amount_at_least_debit + will_creditor_have_debt FROM amount_left_minus_right(in_amount, creditor_balance); - IF amount_at_least_debit THEN - -- the amount is at least as big as the debit, can switch to credit then. - will_creditor_have_debt=FALSE; - -- compute new balance. - ELSE - -- the amount is not enough to bring the receiver - -- to a credit state, switch operators to calculate the new balance. + IF will_creditor_have_debt THEN + -- the amount is not enough to bring the receiver + -- to a credit state, switch operators to calculate the new balance. SELECT (diff).val, (diff).frac INTO new_creditor_balance.val, new_creditor_balance.frac FROM amount_left_minus_right(creditor_balance, in_amount); - will_creditor_have_debt=TRUE; END IF; END IF; -out_balance_insufficient=FALSE; + -- now actually create the bank transaction. -- debtor side: INSERT INTO bank_account_transactions (