gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] 03/03: Amount representation.


From: gnunet
Subject: [libeufin] 03/03: Amount representation.
Date: Fri, 06 Jan 2023 19:09:44 +0100

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

ms pushed a commit to branch master
in repository libeufin.

commit f5995b846492c7b091c09e204f7c6450b5369034
Author: MS <ms@taler.net>
AuthorDate: Fri Jan 6 18:50:07 2023 +0100

    Amount representation.
    
    Defer the conversion of amount strings into
    BigDecimal until the point where they act as
    numeric operands.
    
    This saves resources because in several cases
    the amount strings do not partecipate in any
    calculation.  For example, an error might occur
    before the calculation, or the calculation is
    not carried at all by the function that gets
    the amount string from the network.
---
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  2 +-
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt |  2 +-
 .../tech/libeufin/nexus/iso20022/Iso20022.kt       |  6 ++--
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt | 11 ++++----
 .../tech/libeufin/nexus/server/NexusServer.kt      |  2 +-
 nexus/src/test/kotlin/DownloadAndSubmit.kt         |  6 ++--
 nexus/src/test/kotlin/Iso20022Test.kt              |  2 +-
 .../kotlin/tech/libeufin/sandbox/CircuitApi.kt     | 16 ++++++-----
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  | 14 +++-------
 .../kotlin/tech/libeufin/sandbox/bankAccount.kt    | 18 +++++++-----
 util/src/main/kotlin/amounts.kt                    | 31 ++++++---------------
 util/src/main/kotlin/strings.kt                    |  2 +-
 util/src/test/kotlin/AmountTest.kt                 | 32 ++++++++++++++++------
 13 files changed, 73 insertions(+), 71 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index a120da7b..9e0798e0 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -201,7 +201,7 @@ object PaymentInitiationsTable : LongIdTable() {
     val bankAccount = reference("bankAccount", NexusBankAccountsTable)
     val preparationDate = long("preparationDate")
     val submissionDate = long("submissionDate").nullable()
-    val sum = amount("sum")
+    val sum = text("sum") // the amount to transfer.
     val currency = text("currency")
     val endToEndId = text("endToEndId")
     val paymentInformationId = text("paymentInformationId")
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index fa8064d5..6cd21c62 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -249,7 +249,7 @@ fun processCamtMessage(
             val rawEntity = NexusBankTransactionEntity.new {
                 bankAccount = acct
                 accountTransactionId = "AcctSvcrRef:$acctSvcrRef"
-                amount = 
singletonBatchedTransaction.amount.value.toPlainString()
+                amount = singletonBatchedTransaction.amount.value
                 currency = singletonBatchedTransaction.amount.currency
                 transactionJson = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
                 creditDebitIndicator = 
singletonBatchedTransaction.creditDebitIndicator.name
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
index 12045f50..98e92a46 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
@@ -632,7 +632,7 @@ private fun XmlElementDestructor.extractParty(): 
PartyIdentification {
 
 private fun XmlElementDestructor.extractCurrencyAmount(): CurrencyAmount {
     return CurrencyAmount(
-        value = BigDecimal(requireUniqueChildNamed("Amt") { 
focusElement.textContent }),
+        value = requireUniqueChildNamed("Amt") { focusElement.textContent },
         currency = requireUniqueChildNamed("Amt") { 
focusElement.getAttribute("Ccy") }
     )
 }
@@ -641,7 +641,7 @@ private fun 
XmlElementDestructor.maybeExtractCurrencyAmount(): CurrencyAmount? {
     return maybeUniqueChildNamed("Amt") {
         CurrencyAmount(
             focusElement.getAttribute("Ccy"),
-            BigDecimal(focusElement.textContent)
+            focusElement.textContent
         )
     }
 }
@@ -667,7 +667,7 @@ private fun XmlElementDestructor.extractBatches(
     if (mapEachChildNamed("NtryDtls") {}.size != 1) throw CamtParsingError(
         "This money movement (AcctSvcrRef: $acctSvcrRef) is not a singleton #0"
     )
-    var txs = requireUniqueChildNamed("NtryDtls") {
+    val txs = requireUniqueChildNamed("NtryDtls") {
         if (mapEachChildNamed("TxDtls") {}.size != 1) {
             throw CamtParsingError("This money movement (AcctSvcrRef: 
$acctSvcrRef) is not a singleton #1")
         }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index 17db9166..8f87c0e2 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -381,7 +381,7 @@ data class Pain001Data(
     val creditorIban: String,
     val creditorBic: String?,
     val creditorName: String,
-    val sum: Amount,
+    val sum: String,
     val currency: String,
     val subject: String
 )
@@ -418,7 +418,7 @@ class CurrencyAmountDeserializer(jc: Class<*> = 
CurrencyAmount::class.java) : St
         val s = p.valueAsString
         val components = s.split(":")
         // FIXME: error handling!
-        return CurrencyAmount(components[0], BigDecimal(components[1]))
+        return CurrencyAmount(components[0], components[1])
     }
 }
 
@@ -430,19 +430,20 @@ class CurrencyAmountSerializer(jc: Class<CurrencyAmount> 
= CurrencyAmount::class
         if (value == null) {
             gen.writeNull()
         } else {
-            gen.writeString("${value.currency}:${value.value.toPlainString()}")
+            gen.writeString("${value.currency}:${value.value}")
         }
     }
 }
 
+// FIXME: this type duplicates AmountWithCurrency.
 @JsonDeserialize(using = CurrencyAmountDeserializer::class)
 @JsonSerialize(using = CurrencyAmountSerializer::class)
 data class CurrencyAmount(
     val currency: String,
-    val value: BigDecimal // allows calculations
+    val value: String // allows calculations
 )
 fun CurrencyAmount.toPlainString(): String {
-    return "${this.currency}:${this.value.toPlainString()}"
+    return "${this.currency}:${this.value}"
 }
 
 data class InitiatedPayments(
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index b39c72ec..23df07a5 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -208,7 +208,7 @@ val nexusApp: Application.() -> Unit = {
                 cause.httpStatusCode,
                 message = ErrorResponse(
                     code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code,
-                    hint = "EBICS protocol error",
+                    hint = "The EBICS communication with the bank failed: 
${cause.ebicsTechnicalCode}",
                     detail = cause.reason,
                 )
             )
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt 
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index 0da30b28..3d51dc45 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -151,7 +151,7 @@ class DownloadAndSubmit {
                             creditorBic = "SANDBOXX",
                             creditorName = "Tester",
                             subject = "test payment",
-                            sum = Amount(1),
+                            sum = "1",
                             currency = "TESTKUDOS"
                         ),
                         transaction {
@@ -246,7 +246,7 @@ class DownloadAndSubmit {
                             creditorBic = "not-a-BIC",
                             creditorName = "Tester",
                             subject = "test payment",
-                            sum = Amount(1),
+                            sum = "1",
                             currency = "TESTKUDOS"
                         ),
                         transaction {
@@ -282,7 +282,7 @@ class DownloadAndSubmit {
                             creditorBic = "SANDBOXX",
                             creditorName = "Tester",
                             subject = "test payment",
-                            sum = Amount(1),
+                            sum = "1",
                             currency = "EUR"
                         ),
                         transaction {
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt 
b/nexus/src/test/kotlin/Iso20022Test.kt
index b639aff6..c14b564b 100644
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -54,7 +54,7 @@ class Iso20022Test {
         assertEquals(1, r.reports.size)
 
         // First Entry
-        
assertTrue(BigDecimal(100).compareTo(r.reports[0].entries[0].amount.value) == 0)
+        assertTrue("100" == r.reports[0].entries[0].amount.value)
         assertEquals("EUR", r.reports[0].entries[0].amount.currency)
         assertEquals(CreditDebitIndicator.CRDT, 
r.reports[0].entries[0].creditDebitIndicator)
         assertEquals(EntryStatus.BOOK, r.reports[0].entries[0].status)
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
index e02bdfb3..1742dc4b 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -113,8 +113,8 @@ fun generateCashoutSubject(
     amountCredit: AmountWithCurrency,
     amountDebit: AmountWithCurrency
 ): String {
-    return "Cash-out of 
${amountDebit.currency}:${amountDebit.amount.toPlainString()}" +
-            " to 
${amountCredit.currency}:${amountCredit.amount.toPlainString()}"
+    return "Cash-out of ${amountDebit.currency}:${amountDebit.amount}" +
+            " to ${amountCredit.currency}:${amountCredit.amount}"
 }
 
 /**
@@ -295,18 +295,20 @@ fun circuitApi(circuitRoute: Route) {
         // check rates correctness
         val sellRatio = BigDecimal(ratiosAndFees.sell_at_ratio.toString())
         val sellFee = BigDecimal(ratiosAndFees.sell_out_fee.toString())
-        val amountCreditCheck = (amountDebit.amount * sellRatio) - sellFee
+        val amountDebitAsNumber = BigDecimal(amountDebit.amount)
+        val expectedAmountCredit = (amountDebitAsNumber * sellRatio) - sellFee
         val commonRounding = MathContext(2) // ensures both amounts end with 
".XY"
-        if (amountCreditCheck.round(commonRounding) != 
amountCredit.amount.round(commonRounding)) {
+        val amountCreditAsNumber = BigDecimal(amountCredit.amount)
+        if (expectedAmountCredit.round(commonRounding) != 
amountCreditAsNumber.round(commonRounding)) {
             val msg = "Rates application are incorrect." +
-                    "  The expected amount to credit is: 
${amountCreditCheck}," +
-                    " but ${amountCredit.amount.toPlainString()} was 
specified."
+                    "  The expected amount to credit is: 
${expectedAmountCredit}," +
+                    " but ${amountCredit.amount} was specified."
             logger.info(msg)
             throw badRequest(msg)
         }
         // check that the balance is sufficient
         val balance = getBalance(user, withPending = true)
-        val balanceCheck = balance - amountDebit.amount
+        val balanceCheck = balance - amountDebitAsNumber
         if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > 
BigDecimal(demobank.usersDebtLimit)) {
             val msg = "Cash-out not possible due to insufficient funds.  
Balance ${balance.toPlainString()} would reach ${balanceCheck.toPlainString()}"
             logger.info(msg)
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index d1f88b43..ca4cb8ff 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -720,16 +720,9 @@ val sandboxApp: Application.() -> Unit = {
                     "invalid BIC"
                 )
             }
-            val (amount, currency) = parseAmountAsString(body.amount)
+            val amount = parseAmount(body.amount)
             transaction {
                 val demobank = getDefaultDemobank()
-                /**
-                 * This API needs compatibility with the currency-less format.
-                 */
-                if (currency != null) {
-                    if (currency != demobank.currency)
-                        throw SandboxError(HttpStatusCode.BadRequest, 
"Currency ${currency} not supported.")
-                }
                 val account = getBankAccountFromLabel(
                     accountLabel, demobank
                 )
@@ -743,7 +736,7 @@ val sandboxApp: Application.() -> Unit = {
                     debtorBic = reqDebtorBic
                     debtorName = body.debtorName
                     subject = body.subject
-                    this.amount = amount
+                    this.amount = amount.amount
                     date = getUTCnow().toInstant().toEpochMilli()
                     accountServicerReference = "sandbox-$randId"
                     this.account = account
@@ -1316,7 +1309,8 @@ val sandboxApp: Application.() -> Unit = {
                     val maxDebt = if (username == "admin") {
                         demobank.bankDebtLimit
                     } else demobank.usersDebtLimit
-                    if ((pendingBalance - amount.amount).abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
+                    val amountAsNumber = BigDecimal(amount.amount)
+                    if ((pendingBalance - amountAsNumber).abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
                         logger.info("User $username would surpass user debit " 
+
                                 "threshold of ${demobank.usersDebtLimit}.  
Rollback Taler withdrawal"
                         )
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
index 33c565cc..eacae3c3 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -108,11 +108,15 @@ fun wireTransfer(
     amount: String, // $currency:$value
     pmtInfId: String? = null
 ): String {
-    val checkAmount = parseAmount(amount)
-    if (checkAmount.amount == BigDecimal.ZERO)
+    val parsedAmount = parseAmount(amount)
+    val amountAsNumber = BigDecimal(parsedAmount.amount)
+    if (amountAsNumber == 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}")
+    if (parsedAmount.currency != demobank.currency)
+        throw badRequest(
+            "Won't wire transfer with currency: ${parsedAmount.currency}." +
+                    "  Only ${demobank.currency} allowed."
+        )
     // Check funds are sufficient.
     /**
      * Using 'pending' balance because Libeufin never books.  The
@@ -122,7 +126,7 @@ fun wireTransfer(
     val maxDebt = if (debitAccount.label == "admin") {
         demobank.bankDebtLimit
     } else demobank.usersDebtLimit
-    val balanceCheck = pendingBalance - checkAmount.amount
+    val balanceCheck = pendingBalance - amountAsNumber
     if (balanceCheck < BigDecimal.ZERO && balanceCheck.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")
@@ -138,7 +142,7 @@ fun wireTransfer(
             debtorBic = debitAccount.bic
             debtorName = getPersonNameFromCustomer(debitAccount.owner)
             this.subject = subject
-            this.amount = checkAmount.amount.toPlainString()
+            this.amount = parsedAmount.amount
             this.currency = demobank.currency
             date = timeStamp
             accountServicerReference = transactionRef
@@ -155,7 +159,7 @@ fun wireTransfer(
             debtorBic = debitAccount.bic
             debtorName = getPersonNameFromCustomer(debitAccount.owner)
             this.subject = subject
-            this.amount = checkAmount.amount.toPlainString()
+            this.amount = parsedAmount.amount
             this.currency = demobank.currency
             date = timeStamp
             accountServicerReference = transactionRef
diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt
index 8ba14192..a1cfe47d 100644
--- a/util/src/main/kotlin/amounts.kt
+++ b/util/src/main/kotlin/amounts.kt
@@ -22,33 +22,18 @@ import io.ktor.http.*
  * <http://www.gnu.org/licenses/>
  */
 
-val re = Regex("^([0-9]+(\\.[0-9]+)?)$")
-val reWithSign = Regex("^-?([0-9]+(\\.[0-9]+)?)$")
-
+const val plainAmountRe = "^([0-9]+(\\.[0-9][0-9]?)?)$"
+const val plainAmountReWithSign = "^-?([0-9]+(\\.[0-9][0-9]?)?)$"
+const val amountWithCurrencyRe = "^([A-Z]+):([0-9]+(\\.[0-9][0-9]?)?)$"
 
 fun validatePlainAmount(plainAmount: String, withSign: Boolean = false): 
Boolean {
-    if (withSign) return reWithSign.matches(plainAmount)
-    return re.matches(plainAmount)
-}
-
-/**
- * Parse an "amount" where the currency is optional.  It returns
- * a pair where the first item is always the amount, and the second
- * is the currency or null (when this one wasn't given in the input)
- */
-fun parseAmountAsString(amount: String): Pair<String, String?> {
-    val match = Regex("^([A-Z]+:)?([0-9]+(\\.[0-9]+)?)$").find(amount) ?: throw
-    UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")
-    var (currency, number) = match.destructured
-    // Currency given, need to strip the ":".
-    if (currency.isNotEmpty())
-        currency = currency.dropLast(1)
-    return Pair(number, if (currency.isEmpty()) null else currency)
+    if (withSign) return Regex(plainAmountReWithSign).matches(plainAmount)
+    return Regex(plainAmountRe).matches(plainAmount)
 }
 
 fun parseAmount(amount: String): AmountWithCurrency {
-    val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?:
+    val match = Regex(amountWithCurrencyRe).find(amount) ?:
         throw UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")
     val (currency, number) = match.destructured
-    return AmountWithCurrency(currency, Amount(number))
-}
+    return AmountWithCurrency(currency = currency, amount = number)
+}
\ No newline at end of file
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index 3b94b517..91c0fd84 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -100,7 +100,7 @@ fun chunkString(input: String): String {
 
 data class AmountWithCurrency(
     val currency: String,
-    val amount: Amount
+    val amount: String
 )
 
 fun parseDecimal(decimalStr: String): BigDecimal {
diff --git a/util/src/test/kotlin/AmountTest.kt 
b/util/src/test/kotlin/AmountTest.kt
index bc7ee39b..1dac0be0 100644
--- a/util/src/test/kotlin/AmountTest.kt
+++ b/util/src/test/kotlin/AmountTest.kt
@@ -1,15 +1,31 @@
 import org.junit.Test
-import tech.libeufin.util.parseAmountAsString
-import kotlin.reflect.typeOf
+import tech.libeufin.util.parseAmount
+import tech.libeufin.util.validatePlainAmount
 
+inline fun <reified ExceptionType> assertException(block: () -> Unit) {
+    try {
+        block()
+    } catch (e: Throwable) {
+        assert(e.javaClass == ExceptionType::class.java)
+        return
+    }
+    return assert(false)
+}
 class AmountTest {
     @Test
     fun parse() {
-        val resWithCurrency = parseAmountAsString("CURRENCY:5.5")
-        assert(resWithCurrency.first == "5.5")
-        assert(resWithCurrency.second == "CURRENCY")
-        val resWithoutCurrency = parseAmountAsString("5.5")
-        assert(resWithoutCurrency.first == "5.5")
-        assert(resWithoutCurrency.second == null)
+        var res = parseAmount("KUDOS:5.5")
+        assert(res.amount == "5.5")
+        assert(res.currency == "KUDOS")
+        assert(validatePlainAmount("1.0"))
+        assert(validatePlainAmount("1.00"))
+        assert(!validatePlainAmount("1.000"))
+        res = parseAmount("TESTKUDOS:1.11")
+        assert(res.amount == "1.11")
+        assert(res.currency == "TESTKUDOS")
+        assertException<UtilError> { parseAmount("TESTKUDOS:1.") }
+        assertException<UtilError> { parseAmount("TESTKUDOS:.1") }
+        assertException<UtilError> { parseAmount("TESTKUDOS:1.000") }
+        assertException<UtilError> { parseAmount("TESTKUDOS:1..") }
     }
 }
\ No newline at end of file

-- 
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]