gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (51ce46ae -> d8438142)


From: gnunet
Subject: [libeufin] branch master updated (51ce46ae -> d8438142)
Date: Tue, 20 Dec 2022 17:29:39 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a change to branch master
in repository libeufin.

    from 51ce46ae Report policy.
     new bb46f75d parse also signed decimals
     new b0d98e47 comments
     new ffd714d2 balance (1) and debit limit (2)
     new d8438142 test latest changes

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 nexus/src/test/kotlin/MakeEnv.kt                   |   2 +-
 nexus/src/test/kotlin/SandboxAccessApiTest.kt      |  44 ++++-
 nexus/src/test/kotlin/SandboxBankAccountTest.kt    |  77 ++++++++
 .../src/main/kotlin/tech/libeufin/sandbox/DB.kt    |  22 +--
 .../tech/libeufin/sandbox/EbicsProtocolBackend.kt  |  58 +++---
 .../main/kotlin/tech/libeufin/sandbox/Helpers.kt   | 109 +-----------
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  |  49 +++---
 .../kotlin/tech/libeufin/sandbox/bankAccount.kt    | 194 +++++++++++++++++----
 sandbox/src/test/kotlin/BalanceTest.kt             |   5 +-
 util/src/main/kotlin/DBTypes.kt                    |   4 -
 util/src/main/kotlin/amounts.kt                    |   8 +-
 util/src/main/kotlin/strings.kt                    |   2 +-
 12 files changed, 349 insertions(+), 225 deletions(-)
 create mode 100644 nexus/src/test/kotlin/SandboxBankAccountTest.kt

diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index 58b2affb..a6ade67f 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -176,7 +176,7 @@ fun prepSandboxDb() {
         }
         DemobankCustomerEntity.new {
             username = "foo"
-            passwordHash = "foo"
+            passwordHash = CryptoUtil.hashpw("foo")
             name = "Foo"
         }
     }
diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt 
b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
index ac230203..53763292 100644
--- a/nexus/src/test/kotlin/SandboxAccessApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxAccessApiTest.kt
@@ -12,8 +12,50 @@ import tech.libeufin.sandbox.sandboxApp
 import tech.libeufin.util.buildBasicAuthLine
 
 class SandboxAccessApiTest {
-
     val mapper = ObjectMapper()
+    // Check successful and failing case due to insufficient funds.
+    @Test
+    fun debitWithdraw() {
+        withTestDatabase {
+            prepSandboxDb()
+            withTestApplication(sandboxApp) {
+                runBlocking {
+                    // Normal, successful withdrawal.
+                    
client.post<Any>("/demobanks/default/access-api/accounts/foo/withdrawals") {
+                        expectSuccess = true
+                        headers {
+                            append(
+                                HttpHeaders.ContentType,
+                                ContentType.Application.Json
+                            )
+                            append(
+                                HttpHeaders.Authorization,
+                                buildBasicAuthLine("foo", "foo")
+                            )
+                        }
+                        this.body = "{\"amount\": \"TESTKUDOS:1\"}"
+                    }
+                    // Withdrawal over the debit threshold.
+                    val r: HttpStatusCode = 
client.post("/demobanks/default/access-api/accounts/foo/withdrawals") {
+                        expectSuccess = false
+                        headers {
+                            append(
+                                HttpHeaders.ContentType,
+                                ContentType.Application.Json
+                            )
+                            append(
+                                HttpHeaders.Authorization,
+                                buildBasicAuthLine("foo", "foo")
+                            )
+                        }
+                        this.body = "{\"amount\": \"TESTKUDOS:99999999999\"}"
+                    }
+                    assert(HttpStatusCode.Forbidden.value == r.value)
+                }
+            }
+        }
+    }
+
     @Test
     fun registerTest() {
         // Test IBAN conflict detection.
diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt 
b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
new file mode 100644
index 00000000..f2f25705
--- /dev/null
+++ b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
@@ -0,0 +1,77 @@
+import io.ktor.client.features.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import tech.libeufin.sandbox.SandboxError
+import tech.libeufin.sandbox.getBalance
+import tech.libeufin.sandbox.sandboxApp
+import tech.libeufin.sandbox.wireTransfer
+import tech.libeufin.util.buildBasicAuthLine
+import tech.libeufin.util.parseDecimal
+
+class SandboxBankAccountTest {
+    // Check if the balance shows debit.
+    @Test
+    fun debitBalance() {
+        withTestDatabase {
+            prepSandboxDb()
+            wireTransfer(
+                "bank",
+                "foo",
+                "default",
+                "Show up in logging!",
+                "TESTKUDOS:1"
+            )
+            /**
+             * Bank gave 1 to foo, should be -1 debit now.  Because
+             * the payment is still pending (= not booked), the pending
+             * transactions must be included in the calculation.
+             */
+            var bankBalance = getBalance("bank", true)
+            assert(bankBalance == parseDecimal("-1"))
+            wireTransfer(
+                "foo",
+                "bank",
+                "default",
+                "Show up in logging!",
+                "TESTKUDOS:5"
+            )
+            bankBalance = getBalance("bank", true)
+            assert(bankBalance == parseDecimal("4"))
+            // Trigger Insufficient funds case for users.
+            try {
+                wireTransfer(
+                    "foo",
+                    "bank",
+                    "default",
+                    "Show up in logging!",
+                    "TESTKUDOS:5000"
+                )
+            } catch (e: SandboxError) {
+                // Future versions may wrap this case into a dedicate 
exception type.
+                assert(e.statusCode == HttpStatusCode.PreconditionFailed)
+            }
+            // Trigger Insufficient funds case for the bank.
+            try {
+                wireTransfer(
+                    "bank",
+                    "foo",
+                    "default",
+                    "Show up in logging!",
+                    "TESTKUDOS:5000000"
+                )
+            } catch (e: SandboxError) {
+                // Future versions may wrap this case into a dedicate 
exception type.
+                assert(e.statusCode == HttpStatusCode.PreconditionFailed)
+            }
+            // Check balance didn't change for both parties.
+            bankBalance = getBalance("bank", true)
+            assert(bankBalance == parseDecimal("4"))
+            val fooBalance = getBalance("foo", true)
+            assert(fooBalance == parseDecimal("-4"))
+        }
+    }
+}
\ No newline at end of file
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index b577d0c4..b8ead240 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -309,15 +309,11 @@ object BankAccountTransactionsTable : LongIdTable() {
     val debtorBic = text("debtorBic").nullable()
     val debtorName = text("debtorName")
     val subject = text("subject")
-    /**
-     * Amount is a BigInt in String form.
-     */
+    // Amount is a BigDecimal in String form.
     val amount = text("amount")
     val currency = text("currency")
     val date = long("date")
-    /**
-     * Unique ID for this payment within the bank account.
-     */
+    // Unique ID for this payment within the bank account.
     val accountServicerReference = text("accountServicerReference")
     /**
      * Payment information ID, which is a reference to the payment initiation
@@ -330,10 +326,7 @@ object BankAccountTransactionsTable : LongIdTable() {
      * only both parties to be registered at the running Sandbox.
      */
     val account = reference("account", BankAccountsTable)
-
-    /**
-     * Redundantly storing the demobank for query convenience.
-     */
+    // Redundantly storing the demobank for query convenience.
     val demobank = reference("demobank", DemobankConfigsTable)
 }
 
@@ -423,13 +416,8 @@ object BankAccountStatementsTable : IntIdTable() {
     val creationTime = long("creationTime")
     val xmlMessage = text("xmlMessage")
     val bankAccount = reference("bankAccount", BankAccountsTable)
-    /**
-     * Storing the closing balance (= the one obtained after all
-     * the transactions mentioned in the statement), a.k.a. CLBD.
-     * For statement S, this value will act as the opening balance
-     * (a.k.a. PRCD) of statement S+1.
-     */
-    val balanceClbd = text("balanceClbd") // normally, a BigDecimal
+    // Signed BigDecimal representing a Camt.053 CLBD field.
+    val balanceClbd = text("balanceClbd")
 }
 
 class BankAccountStatementEntity(id: EntityID<Int>) : 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 3d1227ae..8f2fd3b9 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -500,22 +500,6 @@ fun buildCamtString(
     )
 }
 
-/**
- * The last balance is the one accounted in the bank account's
- * last statement.
- */
-fun getLastBalance(
-    bankAccount: BankAccountEntity,
-): BigDecimal {
-    val lastStatement = BankAccountStatementEntity.find {
-        BankAccountStatementsTable.bankAccount eq bankAccount.id
-    }.lastOrNull()
-    val lastBalance = if (lastStatement == null) {
-        BigDecimal.ZERO
-    } else { BigDecimal(lastStatement.balanceClbd) }
-    return lastBalance
-}
-
 /**
  * Builds CAMT response.
  *
@@ -533,28 +517,46 @@ private fun constructCamtResponse(
         if (dateRange != null)
             throw EbicsOrderParamsIgnored("C52 does not support date ranges.")
         val history = mutableListOf<RawPayment>()
-        val lastBalance = transaction {
+        transaction {
             BankAccountFreshTransactionEntity.all().forEach {
                 if (it.transactionRef.account.label == bankAccount.label) {
                     history.add(getHistoryElementFromTransactionRow(it))
                 }
             }
-            getLastBalance(bankAccount)
         }
-        if (history.size == 0)
-            throw EbicsNoDownloadDataAvailable()
-
-        val freshBalance = balanceForAccount(
-            history = history,
-            baseBalance = lastBalance
-        )
-
+        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) {
+                    "DBIT" -> base -= parseDecimal(tx.amount)
+                    "CRDT" -> 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 = lastBalance,
-            balanceClbd = freshBalance
+            balancePrcd = prcdBalance,
+            balanceClbd = clbdBalance
         )
         val paymentsList: String = if (logger.isDebugEnabled) {
             var ret = " It includes the payments:"
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index f7650af5..ed17bc5a 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -203,117 +203,10 @@ fun getDefaultDemobank(): DemobankConfigEntity {
     )
 }
 
-fun wireTransfer(
-    debitAccount: String,
-    creditAccount: String,
-    demobank: String,
-    subject: String,
-    amount: String, // $currency:x.y
-    pmtInfId: String? = null
-): String {
-    val args: Triple<BankAccountEntity, BankAccountEntity, 
DemobankConfigEntity> = transaction {
-        val debitAccountDb = BankAccountEntity.find {
-            BankAccountsTable.label eq debitAccount
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Debit account '$debitAccount' not found"
-        )
-        val creditAccountDb = BankAccountEntity.find {
-            BankAccountsTable.label eq creditAccount
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Credit account '$creditAccount' not found"
-        )
-        val demoBank = DemobankConfigEntity.find {
-            DemobankConfigsTable.name eq demobank
-        }.firstOrNull() ?: throw SandboxError(
-            HttpStatusCode.NotFound,
-            "Demobank '$demobank' not found"
-        )
-
-        Triple(debitAccountDb, creditAccountDb, demoBank)
-    }
-
-    /**
-     * Only validating the amount.  Actual check on the
-     * currency will be done by the callee below.
-     */
-    val amountObj = parseAmount(amount)
-    return wireTransfer(
-        debitAccount = args.first,
-        creditAccount = args.second,
-        demobank = args.third,
-        subject = subject,
-        amount = amountObj.amount.toPlainString(),
-        pmtInfId
-    )
-}
-/**
- * Book a CRDT and a DBIT transaction and return the unique reference thereof.
- *
- * At the moment there is redundancy because all the creditor / debtor details
- * are contained (directly or indirectly) already in the BankAccount 
parameters.
- *
- * This is kept both not to break the existing tests and to allow future 
versions
- * where one party of the transaction is not a customer of the running Sandbox.
- */
-
-fun wireTransfer(
-    debitAccount: BankAccountEntity,
-    creditAccount: BankAccountEntity,
-    demobank: DemobankConfigEntity,
-    subject: String,
-    amount: String,
-    pmtInfId: String? = null
-): String {
-    // sanity check on the amount, no currency allowed here.
-    val checkAmount = parseDecimal(amount)
-    if (checkAmount == BigDecimal.ZERO) throw badRequest("Wire transfers of 
zero not possible.")
-    val timeStamp = getUTCnow().toInstant().toEpochMilli()
-    val transactionRef = getRandomString(8)
-    transaction {
-        BankAccountTransactionEntity.new {
-            creditorIban = creditAccount.iban
-            creditorBic = creditAccount.bic
-            this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
-            debtorIban = debitAccount.iban
-            debtorBic = debitAccount.bic
-            debtorName = getPersonNameFromCustomer(debitAccount.owner)
-            this.subject = subject
-            this.amount = amount
-            this.currency = demobank.currency
-            date = timeStamp
-            accountServicerReference = transactionRef
-            account = creditAccount
-            direction = "CRDT"
-            this.demobank = demobank
-            this.pmtInfId = pmtInfId
-        }
-        BankAccountTransactionEntity.new {
-            creditorIban = creditAccount.iban
-            creditorBic = creditAccount.bic
-            this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
-            debtorIban = debitAccount.iban
-            debtorBic = debitAccount.bic
-            debtorName = getPersonNameFromCustomer(debitAccount.owner)
-            this.subject = subject
-            this.amount = amount
-            this.currency = demobank.currency
-            date = timeStamp
-            accountServicerReference = transactionRef
-            account = debitAccount
-            direction = "DBIT"
-            this.demobank = demobank
-            this.pmtInfId = pmtInfId
-        }
-    }
-    return transactionRef
-}
-
 fun getWithdrawalOperation(opId: String): TalerWithdrawalEntity {
     return transaction {
         TalerWithdrawalEntity.find {
-            TalerWithdrawalsTable.wopid eq java.util.UUID.fromString(opId)
+            TalerWithdrawalsTable.wopid eq UUID.fromString(opId)
         }.firstOrNull() ?: throw SandboxError(
             HttpStatusCode.NotFound, "Withdrawal operation $opId not found."
         )
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 6a26aea3..bf929de0 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -251,11 +251,8 @@ class Camt053Tick : CliktCommand(
                  * Resorting the closing (CLBD) balance of the last statement; 
will
                  * become the PRCD balance of the _new_ one.
                  */
-                val lastBalance = getLastBalance(accountIter)
-                val balanceClbd = balanceForAccount(
-                    history = newStatements[accountIter.label] ?: 
mutableListOf(),
-                    baseBalance = lastBalance
-                )
+                val lastBalance = getBalance(accountIter, withPending = false)
+                val balanceClbd = getBalance(accountIter, withPending = true)
                 val camtData = buildCamtString(
                     53,
                     accountIter.iban,
@@ -671,7 +668,7 @@ val sandboxApp: Application.() -> Unit = {
                 val bankAccount = getBankAccountFromLabel(label, demobank)
                 if (!allowOwnerOrAdmin(username, label))
                     throw unauthorized("'${username}' has no rights over 
'$label'")
-                val balance = balanceForAccount(bankAccount)
+                val balance = getBalance(bankAccount, withPending = true)
                 object {
                     val balance = "${bankAccount.demoBank.currency}:${balance}"
                     val iban = bankAccount.iban
@@ -1192,7 +1189,7 @@ val sandboxApp: Application.() -> Unit = {
                     val ret = TalerWithdrawalStatus(
                         selection_done = wo.selectionDone,
                         transfer_done = wo.confirmationDone,
-                        amount = "${demobank.currency}:${wo.amount}",
+                        amount = wo.amount,
                         suggested_exchange = demobank.suggestedExchangeBaseUrl,
                         aborted = wo.aborted,
                         confirm_transfer_url = captcha_page
@@ -1209,14 +1206,10 @@ val sandboxApp: Application.() -> Unit = {
                     val payto = parsePayto(req.paytoUri)
                     val amount: String? = payto.amount ?: req.amount
                     if (amount == null) throw badRequest("Amount is missing")
-                    val amountParsed = parseAmountAsString(amount)
                     /**
                      * The transaction block below lets the 'demoBank' field
                      * of 'bankAccount' be correctly accessed.  */
                     transaction {
-                        if ((amountParsed.second != null)
-                            && (bankAccount.demoBank.currency != 
amountParsed.second))
-                            throw badRequest("Currency 
'${amountParsed.second}' is wrong")
                         wireTransfer(
                             debitAccount = bankAccount,
                             creditAccount = getBankAccountFromIban(payto.iban),
@@ -1224,7 +1217,7 @@ val sandboxApp: Application.() -> Unit = {
                             subject = payto.message ?: throw badRequest(
                                 "'message' query parameter missing in Payto 
address"
                             ),
-                            amount = amountParsed.first
+                            amount = amount
                         )
                     }
                     call.respond(object {})
@@ -1233,13 +1226,13 @@ val sandboxApp: Application.() -> Unit = {
                 // Information about one withdrawal.
                 get("/accounts/{account_name}/withdrawals/{withdrawal_id}") {
                     val op = 
getWithdrawalOperation(call.getUriComponent("withdrawal_id"))
-                    val demobank = ensureDemobank(call)
+                    ensureDemobank(call)
                     if (!op.selectionDone && op.reservePub != null) throw 
internalServerError(
                         "Unselected withdrawal has a reserve public key",
                         LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE
                     )
                     call.respond(object {
-                        val amount = "${demobank.currency}:${op.amount}"
+                        val amount = op.amount
                         val aborted = op.aborted
                         val confirmation_done = op.confirmationDone
                         val selection_done = op.selectionDone
@@ -1270,12 +1263,25 @@ val sandboxApp: Application.() -> Unit = {
                     val req = call.receiveJson<WithdrawalRequest>()
                     // Check for currency consistency
                     val amount = parseAmount(req.amount)
-                    if (amount.currency != demobank.currency) throw badRequest(
-                        "Currency ${amount.currency} differs from Demobank's: 
${demobank.currency}"
-                    )
+                    if (amount.currency != demobank.currency)
+                        throw badRequest("Currency ${amount.currency} differs 
from Demobank's: ${demobank.currency}")
+                    /**
+                     * Check for debit threshold.  That's however also later 
checked
+                     * after the /confirm call.  Username == null case is 
handled above.
+                     */
+                    val pendingBalance = getBalance(username!!, withPending = 
true)
+                    if ((pendingBalance - amount.amount).abs() > 
BigDecimal.valueOf(demobank.usersDebtLimit.toLong())) {
+                        logger.info("User $username would surpass user debit " 
+
+                                "threshold of ${demobank.usersDebtLimit}.  
Rollback Taler withdrawal"
+                        )
+                        throw SandboxError(
+                            HttpStatusCode.Forbidden,
+                            "Insufficient funds."
+                        )
+                    }
                     val wo: TalerWithdrawalEntity = transaction {
                         TalerWithdrawalEntity.new {
-                        this.amount = amount.amount.toPlainString()
+                        this.amount = req.amount
                         walletBankAccount = maybeOwnedAccount
                         }
                     }
@@ -1336,7 +1342,6 @@ val sandboxApp: Application.() -> Unit = {
                             )
                         )
                         if (!wo.confirmationDone) {
-                            // Need the exchange bank account!
                             wireTransfer(
                                 debitAccount = wo.walletBankAccount,
                                 creditAccount = exchangeBankAccount,
@@ -1382,7 +1387,7 @@ val sandboxApp: Application.() -> Unit = {
                     ) throw forbidden(
                             "Customer '$username' cannot access bank account 
'$accountAccessed'"
                         )
-                    val balance = balanceForAccount(bankAccount)
+                    val balance = getBalance(bankAccount, withPending = true)
                     call.respond(object {
                         val balance = object {
                             val amount = 
"${demobank.currency}:${balance.abs(). toPlainString()}"
@@ -1478,7 +1483,7 @@ val sandboxApp: Application.() -> Unit = {
                                     BankAccountsTable.demoBank eq demobank.id
                             )
                         }.forEach {
-                            val balanceIter = balanceForAccount(it)
+                            val balanceIter = getBalance(it, withPending = 
true)
                             ret.publicAccounts.add(
                                 PublicAccountInfo(
                                     balance = 
"${demobank.currency}:$balanceIter",
@@ -1564,7 +1569,7 @@ val sandboxApp: Application.() -> Unit = {
                             bankAccount.bonus("${demobank.currency}:100")
                         bankAccount
                     }
-                    val balance = balanceForAccount(bankAccount)
+                    val balance = getBalance(bankAccount, withPending = true)
                     call.respond(object {
                         val balance = object {
                             val amount = "${demobank.currency}:$balance"
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
index 852e4b39..74c04bfe 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -2,57 +2,175 @@ package tech.libeufin.sandbox
 
 import io.ktor.http.*
 import org.jetbrains.exposed.sql.and
-import 
org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
-import 
org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
 import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
 import tech.libeufin.util.*
 import java.math.BigDecimal
 
-// Mainly useful inside the Camt generator.
-fun balanceForAccount(
-    history: MutableList<RawPayment>,
-    baseBalance: BigDecimal
+/**
+ * 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.
+ */
+fun getBalance(
+    bankAccount: BankAccountEntity,
+    withPending: Boolean = true
 ): BigDecimal {
-    var ret = baseBalance
-    history.forEach direction@ {
-        if (it.direction == "CRDT") {
-            val amount = parseDecimal(it.amount)
-            ret += amount
-            return@direction
+    val lastStatement = transaction {
+        BankAccountStatementEntity.find {
+            BankAccountStatementsTable.bankAccount eq bankAccount.id
+        }.lastOrNull()
+    }
+    var lastBalance = if (lastStatement == null) {
+        BigDecimal.ZERO
+    } else { BigDecimal(lastStatement.balanceClbd) }
+    if (!withPending) return lastBalance
+    /**
+     * Caller asks to include the pending transactions in the
+     * balance.  The block below gets the transactions happened
+     * later than the last statement and adds them to the balance
+     * that was calculated so far.
+     */
+    transaction {
+        val pendingTransactions = BankAccountTransactionEntity.find {
+            BankAccountTransactionsTable.account eq bankAccount.id and (
+                    
BankAccountTransactionsTable.date.greater(lastStatement?.creationTime ?: 0L))
         }
-        if (it.direction == "DBIT") {
-            val amount = parseDecimal(it.amount)
-            ret -= amount
-            return@direction
+        pendingTransactions.forEach { tx ->
+            when (tx.direction) {
+                "DBIT" -> lastBalance -= parseDecimal(tx.amount)
+                "CRDT" -> lastBalance += parseDecimal(tx.amount)
+                else -> {
+                    logger.error("Transaction ${tx.id} is neither debit nor 
credit.")
+                    throw SandboxError(
+                        HttpStatusCode.InternalServerError,
+                        "Error in transactions state."
+                    )
+                }
+            }
         }
-        throw SandboxError(
-            HttpStatusCode.InternalServerError,
-            "A payment direction was found neither CRDT nor DBIT",
-            LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE
+    }
+    return lastBalance
+}
+
+// Wrapper offering to get bank accounts from a string.
+fun getBalance(accountLabel: String, withPending: Boolean = false): BigDecimal 
{
+    val account = transaction {
+        BankAccountEntity.find { BankAccountsTable.label.eq(accountLabel) 
}.firstOrNull()
+    }
+    if (account == null) throw notFound("Bank account $accountLabel not found")
+    return getBalance(account, withPending)
+}
+
+fun wireTransfer(
+    debitAccount: String,
+    creditAccount: String,
+    demobank: String,
+    subject: String,
+    amount: String, // $currency:x.y
+    pmtInfId: String? = null
+): String {
+    val args: Triple<BankAccountEntity, BankAccountEntity, 
DemobankConfigEntity> = transaction {
+        val debitAccountDb = BankAccountEntity.find {
+            BankAccountsTable.label eq debitAccount
+        }.firstOrNull() ?: throw SandboxError(
+            HttpStatusCode.NotFound,
+            "Debit account '$debitAccount' not found"
+        )
+        val creditAccountDb = BankAccountEntity.find {
+            BankAccountsTable.label eq creditAccount
+        }.firstOrNull() ?: throw SandboxError(
+            HttpStatusCode.NotFound,
+            "Credit account '$creditAccount' not found"
+        )
+        val demoBank = DemobankConfigEntity.find {
+            DemobankConfigsTable.name eq demobank
+        }.firstOrNull() ?: throw SandboxError(
+            HttpStatusCode.NotFound,
+            "Demobank '$demobank' not found"
         )
+
+        Triple(debitAccountDb, creditAccountDb, demoBank)
     }
-    return ret
+
+    return wireTransfer(
+        debitAccount = args.first,
+        creditAccount = args.second,
+        demobank = args.third,
+        subject = subject,
+        amount = amount,
+        pmtInfId
+    )
 }
+/**
+ * Book a CRDT and a DBIT transaction and return the unique reference thereof.
+ *
+ * At the moment there is redundancy because all the creditor / debtor details
+ * are contained (directly or indirectly) already in the BankAccount 
parameters.
+ *
+ * This is kept both not to break the existing tests and to allow future 
versions
+ * where one party of the transaction is not a customer of the running Sandbox.
+ */
 
-fun balanceForAccount(bankAccount: BankAccountEntity): BigDecimal {
-    var balance = BigDecimal.ZERO
+fun wireTransfer(
+    debitAccount: BankAccountEntity,
+    creditAccount: BankAccountEntity,
+    demobank: DemobankConfigEntity,
+    subject: String,
+    amount: String, // $currency:$value
+    pmtInfId: String? = null
+): String {
+    val checkAmount = parseAmount(amount)
+    if (checkAmount.amount == BigDecimal.ZERO)
+        throw badRequest("Wire transfers of zero not possible.")
+    if (checkAmount.currency != demobank.currency)
+        throw badRequest("Won't wire transfer with currency: 
${checkAmount.currency}")
+    // Check funds are sufficient.
+    val pendingBalance = getBalance(debitAccount, withPending = true)
+    val maxDebt = if (debitAccount.label == "bank") {
+        demobank.bankDebtLimit
+    } else demobank.usersDebtLimit
+    if ((pendingBalance - checkAmount.amount).abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
+        logger.info("Account ${debitAccount.label} would surpass debit 
threshold of $maxDebt.  Rollback wire transfer")
+        throw SandboxError(HttpStatusCode.PreconditionFailed, "Insufficient 
funds")
+    }
+    val timeStamp = getUTCnow().toInstant().toEpochMilli()
+    val transactionRef = getRandomString(8)
     transaction {
-        BankAccountTransactionEntity.find {
-            BankAccountTransactionsTable.direction eq "CRDT" and (
-                    BankAccountTransactionsTable.account eq bankAccount.id)
-        }.forEach {
-            val amount = parseDecimal(it.amount)
-            balance += amount
+        BankAccountTransactionEntity.new {
+            creditorIban = creditAccount.iban
+            creditorBic = creditAccount.bic
+            this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
+            debtorIban = debitAccount.iban
+            debtorBic = debitAccount.bic
+            debtorName = getPersonNameFromCustomer(debitAccount.owner)
+            this.subject = subject
+            this.amount = checkAmount.amount.toPlainString()
+            this.currency = demobank.currency
+            date = timeStamp
+            accountServicerReference = transactionRef
+            account = creditAccount
+            direction = "CRDT"
+            this.demobank = demobank
+            this.pmtInfId = pmtInfId
         }
-        BankAccountTransactionEntity.find {
-            BankAccountTransactionsTable.direction eq "DBIT" and (
-                    BankAccountTransactionsTable.account eq bankAccount.id)
-        }.forEach {
-            val amount = parseDecimal(it.amount)
-            balance -= amount
+        BankAccountTransactionEntity.new {
+            creditorIban = creditAccount.iban
+            creditorBic = creditAccount.bic
+            this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
+            debtorIban = debitAccount.iban
+            debtorBic = debitAccount.bic
+            debtorName = getPersonNameFromCustomer(debitAccount.owner)
+            this.subject = subject
+            this.amount = checkAmount.amount.toPlainString()
+            this.currency = demobank.currency
+            date = timeStamp
+            accountServicerReference = transactionRef
+            account = debitAccount
+            direction = "DBIT"
+            this.demobank = demobank
+            this.pmtInfId = pmtInfId
         }
     }
-    return balance
+    return transactionRef
 }
\ No newline at end of file
diff --git a/sandbox/src/test/kotlin/BalanceTest.kt 
b/sandbox/src/test/kotlin/BalanceTest.kt
index f4f99584..48497e98 100644
--- a/sandbox/src/test/kotlin/BalanceTest.kt
+++ b/sandbox/src/test/kotlin/BalanceTest.kt
@@ -16,7 +16,8 @@ class BalanceTest {
                 SchemaUtils.create(
                     BankAccountsTable,
                     BankAccountTransactionsTable,
-                    BankAccountFreshTransactionsTable
+                    BankAccountFreshTransactionsTable,
+                    BankAccountStatementsTable
                 )
                 val demobank = DemobankConfigEntity.new {
                     currency = "EUR"
@@ -84,7 +85,7 @@ class BalanceTest {
                     accountServicerReference = 
"test-account-servicer-reference"
                     this.demobank = demobank
                 }
-                assert(BigDecimal.ONE == balanceForAccount(one))
+                assert(BigDecimal.ONE == getBalance(one, withPending = true))
             }
         }
     }
diff --git a/util/src/main/kotlin/DBTypes.kt b/util/src/main/kotlin/DBTypes.kt
index ec3dc505..c38e63cf 100644
--- a/util/src/main/kotlin/DBTypes.kt
+++ b/util/src/main/kotlin/DBTypes.kt
@@ -28,10 +28,6 @@ import java.math.RoundingMode
 const val SCALE_TWO = 2
 const val NUMBER_MAX_DIGITS = 20
 class BadAmount(badValue: Any?) : Exception("Value '${badValue}' is not a 
valid amount")
-
-/**
- * Any number can become an Amount IF it does NOT need to be rounded to comply 
to the scale == 2.
- */
 typealias Amount = BigDecimal
 
 class AmountColumnType : ColumnType() {
diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt
index 5528a35b..1d157de6 100644
--- a/util/src/main/kotlin/amounts.kt
+++ b/util/src/main/kotlin/amounts.kt
@@ -23,8 +23,10 @@ import io.ktor.http.*
  */
 
 val re = Regex("^([0-9]+(\\.[0-9]+)?)$")
+val reWithSign = Regex("^-?([0-9]+(\\.[0-9]+)?)$")
 
-fun validatePlainAmount(plainAmount: String): Boolean {
+fun validatePlainAmount(plainAmount: String, withSign: Boolean = false): 
Boolean {
+    if (withSign) return reWithSign.matches(plainAmount)
     return re.matches(plainAmount)
 }
 
@@ -44,8 +46,8 @@ fun parseAmountAsString(amount: String): Pair<String, 
String?> {
 }
 
 fun parseAmount(amount: String): AmountWithCurrency {
-    val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?: throw
-    UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")
+    val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?:
+        throw UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")
     val (currency, number) = match.destructured
     return AmountWithCurrency(currency, Amount(number))
 }
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index 85e09cb2..3b94b517 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -104,7 +104,7 @@ data class AmountWithCurrency(
 )
 
 fun parseDecimal(decimalStr: String): BigDecimal {
-    if(!validatePlainAmount(decimalStr))
+    if(!validatePlainAmount(decimalStr, withSign = true))
         throw UtilError(
             HttpStatusCode.BadRequest,
             "Bad string amount given: $decimalStr",

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]