From 5b56ad9ed803b3a1105e6333716dcfca7593a3f1 Mon Sep 17 00:00:00 2001 From: MS Date: Tue, 18 Jul 2023 13:49:55 +0200 Subject: Getting the balance in constant time. --- build.gradle | 2 +- database-versioning/sandbox-0001.sql | 3 + nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 5 +- nexus/src/test/kotlin/SandboxAccessApiTest.kt | 7 ++- nexus/src/test/kotlin/SandboxBankAccountTest.kt | 9 +-- nexus/src/test/kotlin/SandboxCircuitApiTest.kt | 7 ++- .../kotlin/tech/libeufin/sandbox/CircuitApi.kt | 9 +-- .../src/main/kotlin/tech/libeufin/sandbox/DB.kt | 5 +- .../tech/libeufin/sandbox/EbicsProtocolBackend.kt | 69 ---------------------- .../src/main/kotlin/tech/libeufin/sandbox/Main.kt | 19 ++---- .../kotlin/tech/libeufin/sandbox/bankAccount.kt | 39 +++++++----- sandbox/src/test/kotlin/BalanceTest.kt | 23 +++++++- util/src/main/kotlin/amounts.kt | 9 ++- util/src/test/kotlin/AmountTest.kt | 1 - 14 files changed, 87 insertions(+), 120 deletions(-) diff --git a/build.gradle b/build.gradle index bdcf07d0..6f944e01 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ if (!JavaVersion.current().isJava11Compatible()){ allprojects { ext.set("ktor_version", "2.2.1") ext.set("ktor_auth_version", "1.6.8") - ext.set("exposed_version", "0.32.1") + ext.set("exposed_version", "0.41.1") repositories { mavenCentral() diff --git a/database-versioning/sandbox-0001.sql b/database-versioning/sandbox-0001.sql index b6ed53ef..7fa9bbd7 100644 --- a/database-versioning/sandbox-0001.sql +++ b/database-versioning/sandbox-0001.sql @@ -25,6 +25,9 @@ CREATE TABLE IF NOT EXISTS bankaccounts ALTER TABLE bankaccounts ADD CONSTRAINT accountLabelIndex UNIQUE ("label"); +ALTER TABLE + bankaccounts ADD COLUMN balance TEXT DEFAULT ('0'); + CREATE TABLE IF NOT EXISTS bankaccounttransactions (id BIGSERIAL PRIMARY KEY ,"creditorIban" TEXT NOT NULL diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt index 5a59401a..cebb250d 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -638,7 +638,10 @@ private suspend fun addIncoming(call: ApplicationCall) { * the ingested ones. */ val lastIncomingPayment = transaction { - val lastRecord = TalerIncomingPaymentEntity.all().last() + val allIncomingPayments = TalerIncomingPaymentEntity.all() + if (allIncomingPayments.empty()) + throw internalServerError("Incoming payment(s) not found AFTER /add-incoming") + val lastRecord = allIncomingPayments.last() return@transaction Pair(lastRecord.id.value, lastRecord.timestampMs) } call.respond(object { diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt b/nexus/src/test/kotlin/SandboxAccessApiTest.kt index 175e479a..4ac26ab6 100644 --- a/nexus/src/test/kotlin/SandboxAccessApiTest.kt +++ b/nexus/src/test/kotlin/SandboxAccessApiTest.kt @@ -120,14 +120,17 @@ class SandboxAccessApiTest { } val mapper = ObjectMapper() var j = mapper.readTree(R.readBytes()) - assert(j.get("balance").get("amount").asText() == "TESTKUDOS:20") + val expectDebitOf20 = j.get("balance").get("amount").asText() + println("Expect debit of 20: $expectDebitOf20") + val testkudos20regex = "^TESTKUDOS:20(.00)?$".toRegex() + assert(testkudos20regex.matches(expectDebitOf20)) assert(j.get("balance").get("credit_debit_indicator").asText().lowercase() == "debit") // Bar checks its balance: 20 R = client.get("/demobanks/default/access-api/accounts/bar") { basicAuth("bar", "bar") } j = mapper.readTree(R.readBytes()) - assert(j.get("balance").get("amount").asText() == "TESTKUDOS:20") + assert(testkudos20regex.matches(j.get("balance").get("amount").asText())) assert(j.get("balance").get("credit_debit_indicator").asText().lowercase() == "credit") // Foo tries with an invalid amount R = client.post("/demobanks/default/access-api/accounts/foo/transactions") { diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt b/nexus/src/test/kotlin/SandboxBankAccountTest.kt index 799f3435..d2e3197a 100644 --- a/nexus/src/test/kotlin/SandboxBankAccountTest.kt +++ b/nexus/src/test/kotlin/SandboxBankAccountTest.kt @@ -6,6 +6,7 @@ import tech.libeufin.sandbox.sandboxApp import tech.libeufin.sandbox.wireTransfer import tech.libeufin.util.buildBasicAuthLine import tech.libeufin.util.parseDecimal +import tech.libeufin.util.roundToTwoDigits class SandboxBankAccountTest { // Check if the balance shows debit. @@ -26,7 +27,7 @@ class SandboxBankAccountTest { * transactions must be included in the calculation. */ var bankBalance = getBalance("admin") - assert(bankBalance == parseDecimal("-1")) + assert(bankBalance.roundToTwoDigits() == parseDecimal("-1").roundToTwoDigits()) wireTransfer( "foo", "admin", @@ -35,7 +36,7 @@ class SandboxBankAccountTest { "TESTKUDOS:5" ) bankBalance = getBalance("admin") - assert(bankBalance == parseDecimal("4")) + assert(bankBalance.roundToTwoDigits() == parseDecimal("4").roundToTwoDigits()) // Trigger Insufficient funds case for users. try { wireTransfer( @@ -64,9 +65,9 @@ class SandboxBankAccountTest { } // Check balance didn't change for both parties. bankBalance = getBalance("admin") - assert(bankBalance == parseDecimal("4")) + assert(bankBalance.roundToTwoDigits() == parseDecimal("4").roundToTwoDigits()) val fooBalance = getBalance("foo") - assert(fooBalance == parseDecimal("-4")) + assert(fooBalance.roundToTwoDigits() == parseDecimal("-4").roundToTwoDigits()) } } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt index 72a942e3..bbfffd1b 100644 --- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt +++ b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt @@ -13,6 +13,7 @@ import org.junit.Test import tech.libeufin.sandbox.* import tech.libeufin.util.getIban import tech.libeufin.util.parseAmount +import tech.libeufin.util.roundToTwoDigits import java.io.File import java.math.BigDecimal import java.util.* @@ -617,7 +618,7 @@ class SandboxCircuitApiTest { amount = "TESTKUDOS:100" ) val fooBalance = getBalance("foo") - assert(fooBalance == BigDecimal("100")) + assert(fooBalance.roundToTwoDigits() == BigDecimal("100").roundToTwoDigits()) // Foo pays 3 to bar. wireTransfer( "foo", @@ -626,7 +627,7 @@ class SandboxCircuitApiTest { amount = "TESTKUDOS:3" ) val barBalance = getBalance("bar") - assert(barBalance == BigDecimal("3")) + assert(barBalance.roundToTwoDigits() == BigDecimal("3").roundToTwoDigits()) // Deleting foo from the system. transaction { val uBankAccount = getBankAccountFromLabel("foo") @@ -635,7 +636,7 @@ class SandboxCircuitApiTest { uCustomerProfile.delete() } val barBalanceUpdate = getBalance("bar") - assert(barBalanceUpdate == BigDecimal("3")) + assert(barBalanceUpdate.roundToTwoDigits() == BigDecimal("3").roundToTwoDigits()) } } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt index 956ccba7..ebd72d3c 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -146,12 +146,6 @@ fun generateCashoutSubject( " to ${amountCredit.currency}:${amountCredit.amount}" } -fun BigDecimal.roundToTwoDigits(): BigDecimal { - // val twoDigitsRounding = MathContext(2) - // return this.round(twoDigitsRounding) - return this.setScale(2, RoundingMode.HALF_UP) -} - /** * By default, it takes the amount in the regional currency * and applies ratio and fees to convert it to fiat. If the @@ -522,8 +516,7 @@ fun circuitApi(circuitRoute: Route) { // check that the balance is sufficient val balance = getBalance( user, - demobank.name, - withPending = true + demobank.name ) val balanceCheck = balance - amountDebitAsNumber if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > BigDecimal(demobank.config.usersDebtLimit)) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt index c71d1ee7..87db263e 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -34,6 +34,7 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transactionManager +import tech.libeufin.sandbox.CashoutSubmissionsTable.nullable import tech.libeufin.util.* import java.sql.Connection import kotlin.reflect.* @@ -498,6 +499,7 @@ class BankAccountTransactionEntity(id: EntityID) : LongEntity(id) { * own multiple bank accounts. */ object BankAccountsTable : IntIdTable() { + val balance = text("balance").default("0") val iban = text("iban") val bic = text("bic").default("SANDBOXX") val label = text("label").uniqueIndex("accountLabelIndex") @@ -538,6 +540,7 @@ object BankAccountsTable : IntIdTable() { class BankAccountEntity(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(BankAccountsTable) + var balance by BankAccountsTable.balance var iban by BankAccountsTable.iban var bic by BankAccountsTable.bic var label by BankAccountsTable.label @@ -555,7 +558,7 @@ object BankAccountStatementsTable : IntIdTable() { val xmlMessage = text("xmlMessage") val bankAccount = reference("bankAccount", BankAccountsTable) // Signed BigDecimal representing a Camt.053 CLBD field. - val balanceClbd = text("balanceClbd") + val balanceClbd = text("balanceClbd").nullable() } class BankAccountStatementEntity(id: EntityID) : IntEntity(id) { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt index fc240963..658d6373 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -282,8 +282,6 @@ fun buildCamtString( type: Int, subscriberIban: String, history: MutableList, - balancePrcd: BigDecimal, // Balance up to freshHistory (excluded). - balanceClbd: BigDecimal, currency: String ): SandboxCamt { /** @@ -359,45 +357,6 @@ fun buildCamtString( } } } - element("Bal") { - element("Tp/CdOrPrtry/Cd") { - /* Balance type, in a coded format. PRCD stands - for "Previously closed booked" and shows the - balance at the time _before_ all the entries - reported in this document were posted to the - involved bank account. */ - text("PRCD") - } - element("Amt") { - attribute("Ccy", currency) - text(balancePrcd.abs().toPlainString()) - } - element("CdtDbtInd") { - text(getCreditDebitInd(balancePrcd)) - } - element("Dt/Dt") { - // date of this balance - text(dashedDate) - } - } - element("Bal") { - element("Tp/CdOrPrtry/Cd") { - /* CLBD stands for "Closing booked balance", and it - is calculated by summing the PRCD with all the - entries reported in this document */ - text("CLBD") - } - element("Amt") { - attribute("Ccy", currency) - text(balanceClbd.abs().toPlainString()) - } - element("CdtDbtInd") { - text(getCreditDebitInd(balanceClbd)) - } - element("Dt/Dt") { - text(dashedDate) - } - } history.forEach { this.element("Ntry") { element("Amt") { @@ -532,38 +491,10 @@ private fun constructCamtResponse( } } if (history.size == 0) throw EbicsNoDownloadDataAvailable() - - /** - * PRCD balance: balance mentioned in the last statement. This - * will be normally zero, because statements need to be explicitly created. - * - * CLBD balance: PRCD + transactions accounted in the current C52. - * Alternatively, that could be changed into: PRCD + all the pending - * transactions. This way, the CLBD balance would closer reflect the - * latest (pending) activities. - */ - val prcdBalance = getBalance(bankAccount, withPending = false) - val clbdBalance = run { - var base = prcdBalance - history.forEach { tx -> - when (tx.direction) { - XLibeufinBankDirection.DEBIT -> base -= parseDecimal(tx.amount) - XLibeufinBankDirection.CREDIT -> base += parseDecimal(tx.amount) - else -> { - logger.error("Transaction with subject '${tx.subject}' is " + - "inconsistent: neither DBIT nor CRDT") - throw internalServerError("Transactions internal error.") - } - } - } - base - } val camtData = buildCamtString( type, bankAccount.iban, history, - balancePrcd = prcdBalance, - balanceClbd = clbdBalance, bankAccount.demoBank.config.currency ) val paymentsList: String = if (logger.isDebugEnabled) { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt index ad9417f1..fab1fca6 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -264,14 +264,10 @@ class Camt053Tick : CliktCommand( * Resorting the closing (CLBD) balance of the last statement; will * become the PRCD balance of the _new_ one. */ - val lastBalance = getBalance(accountIter, withPending = false) - val balanceClbd = getBalance(accountIter, withPending = true) val camtData = buildCamtString( 53, accountIter.iban, newStatements[accountIter.label]!!, - balanceClbd = balanceClbd, - balancePrcd = lastBalance, currency = accountIter.demoBank.config.currency ) BankAccountStatementEntity.new { @@ -279,7 +275,6 @@ class Camt053Tick : CliktCommand( creationTime = getUTCnow().toInstant().epochSecond xmlMessage = camtData.camtMessage bankAccount = accountIter - this.balanceClbd = balanceClbd.toPlainString() } } BankAccountFreshTransactionsTable.deleteAll() @@ -802,7 +797,7 @@ val sandboxApp: Application.() -> Unit = { val bankAccount = getBankAccountFromLabel(label, demobank) if (!allowOwnerOrAdmin(username, label)) throw unauthorized("'${username}' has no rights over '$label'") - val balance = getBalance(bankAccount, withPending = true) + val balance = getBalance(bankAccount) object { val balance = "${bankAccount.demoBank.config.currency}:${balance}" val iban = bankAccount.iban @@ -1453,10 +1448,11 @@ val sandboxApp: Application.() -> Unit = { val authGranted = !WITH_AUTH || bankAccount.isPublic || username == "admin" if (!authGranted && bankAccount.owner != username) throw forbidden("Customer '$username' cannot access bank account '$accountAccessed'") - val balance = getBalance(bankAccount, withPending = true) + val balance = getBalance(bankAccount) + logger.debug("Balance of '$username': ${balance.toPlainString()}") call.respond(object { val balance = object { - val amount = "${demobank.config.currency}:${balance.abs(). toPlainString()}" + val amount = "${demobank.config.currency}:${balance.abs().toPlainString()}" val credit_debit_indicator = if (balance < BigDecimal.ZERO) "debit" else "credit" } val paytoUri = buildIbanPaytoUri( @@ -1574,10 +1570,7 @@ val sandboxApp: Application.() -> Unit = { BankAccountsTable.demoBank eq demobank.id ) }.forEach { - val balanceIter = getBalance( - it, - withPending = true, - ) + val balanceIter = getBalance(it) ret.publicAccounts.add( PublicAccountInfo( balance = "${demobank.config.currency}:$balanceIter", @@ -1632,7 +1625,7 @@ val sandboxApp: Application.() -> Unit = { demobank = demobank.name, isPublic = req.isPublic ) - val balance = getBalance(newAccount.bankAccount, withPending = true) + val balance = getBalance(newAccount.bankAccount) call.respond(object { val balance = getBalanceForJson(balance, demobank.config.currency) val paytoUri = buildIbanPaytoUri( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt index 748962d5..23c24dbf 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -1,13 +1,13 @@ package tech.libeufin.sandbox import io.ktor.http.* +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.util.* import java.math.BigDecimal - - /** * Check whether the given bank account would surpass the * debit threshold, in case the potential amount gets transferred. @@ -21,7 +21,7 @@ fun maybeDebit( "Demobank '${demobankName}' not found when trying to check the debit threshold" + " for user $accountLabel" ) - val balance = getBalance(accountLabel, demobankName, withPending = true) + val balance = getBalance(accountLabel, demobankName) val maxDebt = if (accountLabel == "admin") { demobank.config.bankDebtLimit } else demobank.config.usersDebtLimit @@ -52,16 +52,18 @@ fun getBalanceForJson(value: BigDecimal, currency: String): BalanceJson { ) } +fun getBalance(bankAccount: BankAccountEntity): BigDecimal { + return BigDecimal(bankAccount.balance) +} + /** - * The last balance is the one mentioned in the bank account's - * last statement. If the bank account does not have any statement - * yet, then zero is returned. When 'withPending' is true, it adds - * the pending transactions to it. - * - * Note: because transactions are searched after the bank accounts - * (numeric) id, the research in the database is not ambiguous. + * This function balances _in bank account statements_. A statement + * witnesses the bank account after a given business time slot. Therefore + * _this_ type of balance is not guaranteed to hold the _actual_ and + * more up-to-date bank account. It'll be used when Sandbox will support + * the issuing of bank statement. */ -fun getBalance( +fun getBalanceForStatement( bankAccount: BankAccountEntity, withPending: Boolean = true ): BigDecimal { @@ -104,8 +106,7 @@ fun getBalance( // Gets the balance of 'accountLabel', which is hosted at 'demobankName'. fun getBalance(accountLabel: String, - demobankName: String = "default", - withPending: Boolean = true + demobankName: String = "default" ): BigDecimal { val demobank = getDemobank(demobankName) ?: throw SandboxError( HttpStatusCode.InternalServerError, @@ -124,7 +125,7 @@ fun getBalance(accountLabel: String, demobank, withBankFault = true ) - return getBalance(account, withPending) + return getBalance(account) } /** @@ -190,6 +191,7 @@ fun wireTransfer( val timeStamp = getUTCnow().toInstant().toEpochMilli() val transactionRef = getRandomString(8) transaction { + // addLogger(StdOutSqlLogger) BankAccountTransactionEntity.new { creditorIban = creditAccount.iban creditorBic = creditAccount.bic @@ -224,6 +226,15 @@ fun wireTransfer( this.demobank = demobank this.pmtInfId = pmtInfId } + + // Adjusting the balances (acceptable debit conditions checked before). + // Debit: + val newDebitBalance = (BigDecimal(debitAccount.balance) - amountAsNumber).roundToTwoDigits() + debitAccount.balance = newDebitBalance.toPlainString() // FIXME: that's ignored! + // Credit: + val newCreditBalance = (BigDecimal(creditAccount.balance) + amountAsNumber).roundToTwoDigits() + creditAccount.balance = newCreditBalance.toPlainString() + // Signaling this wire transfer's event. if (this.isPostgres()) { val creditChannel = buildChannelName( diff --git a/sandbox/src/test/kotlin/BalanceTest.kt b/sandbox/src/test/kotlin/BalanceTest.kt index cf9f3918..aabf2051 100644 --- a/sandbox/src/test/kotlin/BalanceTest.kt +++ b/sandbox/src/test/kotlin/BalanceTest.kt @@ -4,6 +4,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test import tech.libeufin.sandbox.* import tech.libeufin.util.millis +import tech.libeufin.util.roundToTwoDigits import java.math.BigDecimal import java.time.LocalDateTime @@ -28,7 +29,14 @@ class BalanceTest { iban = "IBAN 1" bic = "BIC" label = "label 1" - owner = "test" + owner = "admin" + this.demoBank = demobank + } + val other = BankAccountEntity.new { + iban = "IBAN 2" + bic = "BIC" + label = "label 2" + owner = "admin" this.demoBank = demobank } BankAccountTransactionEntity.new { @@ -82,7 +90,18 @@ class BalanceTest { accountServicerReference = "test-account-servicer-reference" this.demobank = demobank } - assert(BigDecimal.ONE == getBalance(one, withPending = true)) + wireTransfer( + other, one, demobank, "one gets 1", "EUR:1" + ) + wireTransfer( + other, one, demobank, "one gets another 1", "EUR:1" + ) + wireTransfer( + one, other, demobank, "one gives 1", "EUR:1" + ) + val maybeOneBalance: BigDecimal = getBalance(one) + println(maybeOneBalance) + assert(BigDecimal.ONE.roundToTwoDigits() == maybeOneBalance.roundToTwoDigits()) } } } diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt index 043e9af3..671dfbd3 100644 --- a/util/src/main/kotlin/amounts.kt +++ b/util/src/main/kotlin/amounts.kt @@ -3,6 +3,7 @@ package tech.libeufin.util import UtilError import io.ktor.http.* import java.math.BigDecimal +import java.math.RoundingMode /* * This file is part of LibEuFin. @@ -48,4 +49,10 @@ fun isAmountZero(a: BigDecimal): Boolean { return false } return true -} \ No newline at end of file +} + +fun BigDecimal.roundToTwoDigits(): BigDecimal { + // val twoDigitsRounding = MathContext(2) + // return this.round(twoDigitsRounding) + return this.setScale(2, RoundingMode.HALF_UP) +} diff --git a/util/src/test/kotlin/AmountTest.kt b/util/src/test/kotlin/AmountTest.kt index eb7d8493..636c9060 100644 --- a/util/src/test/kotlin/AmountTest.kt +++ b/util/src/test/kotlin/AmountTest.kt @@ -1,6 +1,5 @@ import io.ktor.util.reflect.* import org.junit.Test -import tech.libeufin.sandbox.roundToTwoDigits import tech.libeufin.util.isAmountZero import tech.libeufin.util.parseAmount import tech.libeufin.util.validatePlainAmount -- cgit v1.2.3