[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.