commit f9378fd23f581a48ea0d1754c5d5beb3cbc7bd1e parent b5c14d235d63aacf5fa00ddf05a36dde20860106 Author: Christian Grothoff <christian@grothoff.org> Date: Thu, 2 Mar 2023 14:41:46 +0100 update Debian package of libeufin to include more DB setup logic Diffstat:
18 files changed, 372 insertions(+), 163 deletions(-)
diff --git a/Makefile b/Makefile @@ -5,19 +5,26 @@ escaped_pwd = $(shell pwd | sed 's/\//\\\//g') all: assemble install: install-nexus install-sandbox install-cli git-archive-all = ./build-system/taler-build-scripts/archive-with-submodules/git_archive_all.py - +git_tag=$(shell git describe --tags) +gradle_version=$(shell ./gradlew -q libeufinVersion) +define versions_check = + if test $(git_tag) != "v$(gradle_version)"; \ + then echo WARNING: Project version from Gradle: $(gradle_version) differs from current Git tag: $(git_tag); fi +endef .PHONY: dist dist: + @$(call versions_check) @mkdir -p build/distributions @$(git-archive-all) --include ./configure build/distributions/libeufin-$(shell ./gradlew -q libeufinVersion)-sources.tar.gz .PHONY: exec-arch exec-arch: + @$(call versions_check) @./gradlew -q execArch .PHONY: deb -deb: dist +deb: exec-arch @dpkg-buildpackage -rfakeroot -b -uc -us diff --git a/build.gradle b/build.gradle @@ -12,7 +12,7 @@ plugins { } group = 'tech.libeufin' -version = '0.9.1' +version = '0.9.2' if (!JavaVersion.current().isJava11Compatible()){ throw new GradleException( @@ -85,4 +85,4 @@ task execArch(type: Zip) { rename { "bin/libeufin-cli" } } into(topDir) -} -\ No newline at end of file +} diff --git a/debian/control b/debian/control @@ -17,6 +17,7 @@ Depends: openjdk-11-jdk-headless | openjdk-11-jdk | openjdk-12-jdk-headless | op python3 (>= 3.7), python3-click, python3-requests, + apache2 | nginx | httpd, ${misc:Depends} Recommends: Description: Software package to access FinTS/EBICS based diff --git a/debian/etc/nginx/sites-available/libeufin-sandbox.conf b/debian/etc/nginx/sites-available/libeufin-sandbox.conf @@ -0,0 +1,37 @@ +server { + include /etc/nginx/mime.types; + + # NOTE: + # - urgently consider configuring TLS instead + # - maybe keep a forwarder from HTTP to HTTPS + listen 80; + + # NOTE: + # - Comment out this line if you have no IPv6 + listen [::]:80; + + # NOTE: + # - replace with your actual server name. + server_name localhost; + + # Doesn't take requests away from the backend, + # because that expects always a "/demobanks/default" + # prefix. + rewrite ^/$ /webui/index.html; + + # FRONTEND + location /webui { + # This location has both the SPA HTML and the + # JavaScript configuration demobank-ui-settings.js + alias /usr/share/taler/demobank-ui; + } + + # BACKEND + location / { + # NOTE: urgently change to 'https' once TLS has been configured. + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "localhost"; + proxy_set_header X-Forwarded-Prefix /; + proxy_pass http://localhost:5000/; + } +} diff --git a/debian/libeufin.install b/debian/libeufin.install @@ -2,3 +2,7 @@ debian/etc/* etc/ # Files needed by dbconf debian/db/install/* usr/share/dbconfig-common/scripts/libeufin/install/ + +# Install the SPA + JS config: +debian/usr/share/libeufin/* usr/share/libeufin/ + diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -27,7 +27,6 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.iso20022.EntryStatus import tech.libeufin.util.EbicsInitState -import tech.libeufin.util.amount import java.sql.Connection /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -27,9 +27,7 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction import org.w3c.dom.Document import tech.libeufin.nexus.* -import tech.libeufin.nexus.iso20022.CamtParsingError -import tech.libeufin.nexus.iso20022.CreditDebitIndicator -import tech.libeufin.nexus.iso20022.parseCamtMessage +import tech.libeufin.nexus.iso20022.* import tech.libeufin.nexus.server.FetchSpecJson import tech.libeufin.nexus.server.Pain001Data import tech.libeufin.nexus.server.requireBankConnection @@ -232,7 +230,7 @@ fun processCamtMessage( } } } - val entries = res.reports.map { it.entries }.flatten() + val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries }.flatten() var newPaymentsLog = "" downloadedTransactions = entries.size txloop@ for (entry in entries) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt @@ -440,7 +440,7 @@ class CurrencyAmountSerializer(jc: Class<CurrencyAmount> = CurrencyAmount::class @JsonSerialize(using = CurrencyAmountSerializer::class) data class CurrencyAmount( val currency: String, - val value: String // allows calculations + val value: String ) fun CurrencyAmount.toPlainString(): String { return "${this.currency}:${this.value}" diff --git a/nexus/src/test/kotlin/DBTest.kt b/nexus/src/test/kotlin/DBTest.kt @@ -1,67 +1,31 @@ package tech.libeufin.nexus +import kotlinx.coroutines.* +import org.jetbrains.exposed.dao.flushCache import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.api.ExposedConnection +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.transactions.transactionManager import org.junit.Test +import org.postgresql.PGConnection +import org.postgresql.jdbc.PgConnection +import tech.libeufin.util.PostgresListenNotify import withTestDatabase -import java.io.File - -object MyTable : Table() { - val col1 = text("col1") - val col2 = text("col2") - override val primaryKey = PrimaryKey(col1, col2) -} +import java.sql.Connection +import java.sql.DriverManager class DBTest { - @Test(expected = ExposedSQLException::class) - fun sqlDslTest() { - withTestDatabase { - transaction { - addLogger(StdOutSqlLogger) - SchemaUtils.create(MyTable) - MyTable.insert { - it[col1] = "foo" - it[col2] = "bar" - } - // should throw ExposedSQLException - MyTable.insert { - it[col1] = "foo" - it[col2] = "bar" - } - MyTable.insert { } // shouldn't it fail for non-NULL constraint violation? - } - } - } + // Testing database notifications (only postgresql) @Test - fun facadeConfigTest() { - withTestDatabase { - transaction { - addLogger(StdOutSqlLogger) - SchemaUtils.create( - FacadesTable, - FacadeStateTable, - NexusUsersTable - ) - val user = NexusUserEntity.new { - username = "testuser" - passwordHash = "x" - superuser = true - } - val facade = FacadeEntity.new { - facadeName = "testfacade" - type = "any" - creator = user - } - FacadeStateEntity.new { - bankAccount = "b" - bankConnection = "b" - reserveTransferLevel = "any" - this.facade = facade - currency = "UNUSED" - } - } - } + fun notifications() { + val genCon = DriverManager.getConnection("jdbc:postgresql://localhost:5432/talercheck?user=job") + val pgCon = genCon.unwrap(org.postgresql.jdbc.PgConnection::class.java) + val ln = PostgresListenNotify(pgCon, "x") + ln.postrgesListen() + ln.postgresNotify() + runBlocking { ln.postgresWaitNotification(2000L) } } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt @@ -7,6 +7,8 @@ import org.jetbrains.exposed.sql.transactions.transactionManager import tech.libeufin.nexus.* import tech.libeufin.nexus.dbCreateTables import tech.libeufin.nexus.dbDropTables +import tech.libeufin.nexus.iso20022.* +import tech.libeufin.nexus.server.CurrencyAmount import tech.libeufin.nexus.server.FetchLevel import tech.libeufin.nexus.server.FetchSpecAllJson import tech.libeufin.sandbox.* @@ -21,9 +23,9 @@ data class EbicsKeys( val sig: CryptoUtil.RsaCrtKeyPair ) const val TEST_DB_FILE = "/tmp/nexus-test.sqlite3" -const val TEST_DB_CONN = "jdbc:sqlite:$TEST_DB_FILE" +// const val TEST_DB_CONN = "jdbc:sqlite:$TEST_DB_FILE" // Convenience DB connection to switch to Postgresql: -// const val TEST_DB_CONN = "jdbc:postgresql://localhost:5432/talercheck?user=" +const val TEST_DB_CONN = "jdbc:postgresql://localhost:5432/talercheck?user=job" val BANK_IBAN = getIban() val FOO_USER_IBAN = getIban() val BAR_USER_IBAN = getIban() @@ -292,4 +294,80 @@ fun withSandboxTestDatabase(f: () -> Unit) { } f() } -} -\ No newline at end of file +} + +fun talerIncomingForFoo(currency: String, value: String, subject: String) { + transaction { + val inc = NexusBankTransactionEntity.new { + bankAccount = NexusBankAccountEntity.findByName("foo")!! + accountTransactionId = "mock" + creditDebitIndicator = "CRDT" + this.currency = currency + this.amount = value + status = EntryStatus.BOOK + transactionJson = jacksonObjectMapper( + ).writerWithDefaultPrettyPrinter( + ).writeValueAsString( + genNexusIncomingPayment( + amount = CurrencyAmount(currency,value), + subject = subject + ) + ) + } + TalerIncomingPaymentEntity.new { + payment = inc + reservePublicKey = "mock" + timestampMs = 0L + debtorPaytoUri = "mock" + } + } +} + + +fun genNexusIncomingPayment( + amount: CurrencyAmount, + subject: String, +): CamtBankAccountEntry = + CamtBankAccountEntry( + amount = amount, + creditDebitIndicator = CreditDebitIndicator.CRDT, + status = EntryStatus.BOOK, + bankTransactionCode = "mock", + valueDate = null, + bookingDate = null, + accountServicerRef = null, + entryRef = null, + currencyExchange = null, + counterValueAmount = null, + instructedAmount = null, + batches = listOf( + Batch( + paymentInformationId = null, + messageId = null, + batchTransactions = listOf( + BatchTransaction( + amount = amount, + creditDebitIndicator = CreditDebitIndicator.CRDT, + details = TransactionDetails( + unstructuredRemittanceInformation = subject, + debtor = null, + debtorAccount = null, + debtorAgent = null, + creditor = null, + creditorAccount = null, + creditorAgent = null, + ultimateCreditor = null, + ultimateDebtor = null, + purpose = null, + proprietaryPurpose = null, + currencyExchange = null, + instructedAmount = null, + counterValueAmount = null, + interBankSettlementAmount = null, + returnInfo = null + ) + ) + ) + ) + ) + ) +\ No newline at end of file diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt @@ -11,6 +11,7 @@ import org.junit.Ignore import org.junit.Test import tech.libeufin.sandbox.* import java.io.File +import java.math.BigDecimal import java.util.* class SandboxCircuitApiTest { @@ -28,6 +29,10 @@ class SandboxCircuitApiTest { } } + /** + * Checking that the ordinary user foo doesn't get to access bar's + * data, but admin does. + */ @Test fun accessAccountsTest() { withTestDatabase { @@ -258,12 +263,12 @@ class SandboxCircuitApiTest { basicAuth("shop", "secret") setBody("""{ "amount_debit": "TESTKUDOS:20", - "amount_credit": "KUDOS:19", + "amount_credit": "CHF:19", "tan_channel": "file" }""".trimIndent()) } assert(R.status.value == HttpStatusCode.Accepted.value) - var operationUuid = mapper.readTree(R.readBytes()).get("uuid").asText() + val operationUuid = mapper.readTree(R.readBytes()).get("uuid").asText() // Check that the operation is found by the bank. R = client.get("/demobanks/default/circuit-api/cashouts/${operationUuid}") { // Asking as the Admin but for the 'shop' account. @@ -290,14 +295,37 @@ class SandboxCircuitApiTest { } respJson = mapper.readTree(R.bodyAsText()) assert(respJson.get("balance").get("amount").asText() == balanceAfterCashout) - + // Attempt to cash-out with wrong regional currency. + R = client.post("/demobanks/default/circuit-api/cashouts") { + contentType(ContentType.Application.Json) + basicAuth("shop", "secret") + setBody("""{ + "amount_debit": "NOTFOUND:20", + "amount_credit": "CHF:19", + "tan_channel": "file" + }""".trimIndent()) + expectSuccess = false + } + assert(R.status.value == HttpStatusCode.BadRequest.value) + // Attempt to cash-out with wrong fiat currency. + R = client.post("/demobanks/default/circuit-api/cashouts") { + contentType(ContentType.Application.Json) + basicAuth("shop", "secret") + setBody("""{ + "amount_debit": "TESTKUDOS:20", + "amount_credit": "NOTFOUND:19", + "tan_channel": "file" + }""".trimIndent()) + expectSuccess = false + } + assert(R.status.value == HttpStatusCode.BadRequest.value) // Create a new cash-out and delete it. R = client.post("/demobanks/default/circuit-api/cashouts") { contentType(ContentType.Application.Json) basicAuth("shop", "secret") setBody("""{ "amount_debit": "TESTKUDOS:20", - "amount_credit": "KUDOS:19", + "amount_credit": "CHF:19", "tan_channel": "file" }""".trimIndent()) } @@ -451,6 +479,46 @@ class SandboxCircuitApiTest { } } + /** + * Testing that deleting a user doesn't cause a _different_ user + * to lose data. + */ + @Test + fun deletionIsolation() { + withTestDatabase { + prepSandboxDb() + transaction { + // Admin makes sure foo has balance 100. + wireTransfer( + "admin", + "foo", + subject = "set to 100", + amount = "TESTKUDOS:100" + ) + val fooBalance = getBalance("foo") + assert(fooBalance == BigDecimal("100")) + // Foo pays 3 to bar. + wireTransfer( + "foo", + "bar", + subject = "donation", + amount = "TESTKUDOS:3" + ) + val barBalance = getBalance("bar") + assert(barBalance == BigDecimal("3")) + // Deleting foo from the system. + transaction { + val uBankAccount = getBankAccountFromLabel("foo") + val uCustomerProfile = getCustomer("foo") + uBankAccount.delete() + uCustomerProfile.delete() + } + val barBalanceUpdate = getBalance("bar") + assert(barBalance == BigDecimal("3")) + } + } + } + @Test fun tanCommandTest() { /** diff --git a/nexus/src/test/kotlin/TalerTest.kt b/nexus/src/test/kotlin/TalerTest.kt @@ -1,25 +1,50 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.client.plugins.* import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.coroutines.* +import kotlinx.coroutines.future.future import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Ignore import org.junit.Test import tech.libeufin.nexus.* import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions -import tech.libeufin.nexus.server.FetchLevel -import tech.libeufin.nexus.server.FetchSpecAllJson -import tech.libeufin.nexus.server.client -import tech.libeufin.nexus.server.nexusApp +import tech.libeufin.nexus.iso20022.EntryStatus +import tech.libeufin.nexus.server.* import tech.libeufin.sandbox.sandboxApp import tech.libeufin.sandbox.wireTransfer // This class tests the features related to the Taler facade. class TalerTest { + /** + * Tests that a client (normally represented by the wire-watch) + * gets incoming transactions. + */ + @Test + fun historyIncomingTest() { + withNexusAndSandboxUser { + testApplication { + application(nexusApp) + runBlocking { + val future = async { + client.get( + "/facades/taler/taler-wire-gateway/history/incoming?delta=5" + ) { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "foo") + } + } + talerIncomingForFoo("KUDOS", "10", "Invalid") + } + } + } + } + @Ignore // Ignoring because no assert takes place. @Test // Triggering a refund because of a duplicate reserve pub. fun refundTest() { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -33,13 +33,13 @@ data class CircuitCashoutRequest( */ val tan_channel: String? ) - +const val FIAT_CURRENCY = "CHF" // FIXME: make configurable. // Configuration response: data class ConfigResp( val name: String = "circuit", val version: String = SANDBOX_VERSION, val ratios_and_fees: RatioAndFees, - val fiat_currency: String = "CHF" // FIXME: make configurable. + val fiat_currency: String = FIAT_CURRENCY ) // After fixing #7527, the values held by this @@ -370,10 +370,16 @@ fun circuitApi(circuitRoute: Route) { val amountDebit = parseAmount(req.amount_debit) // amount before rates. val amountCredit = parseAmount(req.amount_credit) // amount after rates, as expected by the client val demobank = ensureDemobank(call) + // Currency check of the cash-out's circuit part. if (amountDebit.currency != demobank.currency) - throw badRequest("The '${req::amount_debit.name}' field has the wrong currency") - if (amountCredit.currency == demobank.currency) - throw badRequest("The '${req::amount_credit.name}' field didn't change the currency.") + throw badRequest("'${req::amount_debit.name}' (${req.amount_debit})" + + " doesn't match the regional currency (${demobank.currency})" + ) + // Currency check of the cash-out's fiat part. + if (amountCredit.currency != FIAT_CURRENCY) + throw badRequest("'${req::amount_credit.name}' (${req.amount_credit})" + + " doesn't match the fiat currency ($FIAT_CURRENCY)." + ) // check if TAN is supported. Default to SMS, if that's missing. val tanChannel = req.tan_channel?.uppercase() ?: SupportedTanChannels.SMS.name if (!isTanChannelSupported(tanChannel)) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -182,7 +182,11 @@ object EbicsSubscribersTable : IntIdTable() { val authenticationKey = reference("authorizationKey", EbicsSubscriberPublicKeysTable).nullable() val nextOrderID = integer("nextOrderID") val state = enumeration("state", SubscriberState::class) - val bankAccount = reference("bankAccount", BankAccountsTable).nullable() + val bankAccount = reference( + "bankAccount", + BankAccountsTable, + onDelete = ReferenceOption.CASCADE + ).nullable() } class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { @@ -297,7 +301,11 @@ class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : Entity<String>(i * to the main ledger. */ object BankAccountFreshTransactionsTable : LongIdTable() { - val transactionRef = reference("transaction", BankAccountTransactionsTable) + val transactionRef = reference( + "transaction", + BankAccountTransactionsTable, + onDelete = ReferenceOption.CASCADE + ) } class BankAccountFreshTransactionEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<BankAccountFreshTransactionEntity>(BankAccountFreshTransactionsTable) @@ -331,7 +339,10 @@ object BankAccountTransactionsTable : LongIdTable() { * Bank account of the party whose 'direction' refers. This version allows * only both parties to be registered at the running Sandbox. */ - val account = reference("account", BankAccountsTable) + val account = reference( + "account", BankAccountsTable, + onDelete = ReferenceOption.CASCADE + ) // Redundantly storing the demobank for query convenience. val demobank = reference("demobank", DemobankConfigsTable) } diff --git a/util/build.gradle b/util/build.gradle @@ -45,6 +45,8 @@ dependencies { // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: '1.5.21' + // Database helper + implementation group: 'org.postgresql', name: 'postgresql', version: '42.5.4' implementation "org.jetbrains.exposed:exposed-core:$exposed_version" implementation "org.jetbrains.exposed:exposed-dao:$exposed_version" implementation "io.netty:netty-all:$netty_version" diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -0,0 +1,84 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2020 Taler Systems S.A. + * + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + * + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.util +import UtilError +import io.ktor.http.* +import kotlinx.coroutines.delay +import logger +import org.postgresql.jdbc.PgConnection +import java.lang.Long.max + +// This class abstracts the LISTEN/NOTIFY construct supported +class PostgresListenNotify( + private val conn: PgConnection, + private val channel: String +) { + fun postrgesListen() { + val stmt = conn.createStatement() + stmt.execute("LISTEN $channel") + stmt.close() + } + fun postgresNotify() { + val stmt = conn.createStatement() + stmt.execute("NOTIFY $channel") + stmt.close() + } + + suspend fun postgresWaitNotification(timeoutMs: Long) { + // Splits the checks into 10ms chunks. + val sleepTimeMs = 10L + var notificationFound = false + val iterations = timeoutMs / sleepTimeMs + for (i in 0..iterations) { + val maybeNotifications = conn.notifications + // If a notification arrived, stop fetching for it. + if (maybeNotifications.isNotEmpty()) { + // Double checking that the channel is correct. + // Notification(s) arrived, double-check channel name. + maybeNotifications.forEach { + if (it.name != channel) { + throw UtilError( + statusCode = HttpStatusCode.InternalServerError, + reason = "Listener got wrong notification. Expected: $channel, but got: ${it.name}" + ) + } + } + notificationFound = true + break + } + /* Notification didn't arrive, release the thread and + * retry in the next chunk. */ + delay(sleepTimeMs) + } + + if (!notificationFound) { + throw UtilError( + statusCode = HttpStatusCode.NotFound, + reason = "Timeout expired for notification on channel $channel", + ec = LibeufinErrorCode.LIBEUFIN_EC_TIMEOUT_EXPIRED + ) + } + /* Notification arrived. In this current version + * we don't pass any data to the caller; the channel + * name itself means that the awaited information arrived. + * */ + return + } + } +\ No newline at end of file diff --git a/util/src/main/kotlin/DBTypes.kt b/util/src/main/kotlin/DBTypes.kt @@ -1,76 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2020 Taler Systems S.A. - * - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - * - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.util - -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.ColumnType -import org.jetbrains.exposed.sql.Table -import java.math.BigDecimal -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") -typealias Amount = BigDecimal - -class AmountColumnType : ColumnType() { - override fun sqlType(): String = "DECIMAL(${NUMBER_MAX_DIGITS}, ${SCALE_TWO})" - override fun valueFromDB(value: Any): Any { - val valueFromDB = super.valueFromDB(value) - try { - return when (valueFromDB) { - is BigDecimal -> valueFromDB.setScale(SCALE_TWO, RoundingMode.UNNECESSARY) - is Double -> BigDecimal.valueOf(valueFromDB).setScale(SCALE_TWO, RoundingMode.UNNECESSARY) - is Float -> BigDecimal(valueFromDB.toString()).setScale( - SCALE_TWO, - RoundingMode.UNNECESSARY - ) - is Int -> BigDecimal(valueFromDB) - is Long -> BigDecimal.valueOf(valueFromDB) - else -> valueFromDB - } - } catch (e: Exception) { - e.printStackTrace() - throw BadAmount(value) - } - } - - override fun valueToDB(value: Any?): Any? { - try { - (value as BigDecimal).setScale(SCALE_TWO, RoundingMode.UNNECESSARY) - } catch (e: Exception) { - e.printStackTrace() - throw BadAmount(value) - } - - if (value.compareTo(BigDecimal.ZERO) == 0) { - throw BadAmount(value) - } - return super.valueToDB(value) - } -} - -/** - * Make sure the number entered by upper layers does not need any rounding - * to conform to scale == 2 - */ -fun Table.amount(name: String): Column<Amount> { - return registerColumn(name, AmountColumnType()) -} -\ No newline at end of file diff --git a/util/src/main/kotlin/LibeufinErrorCodes.kt b/util/src/main/kotlin/LibeufinErrorCodes.kt @@ -71,5 +71,7 @@ enum class LibeufinErrorCode(val code: Int) { * A request is using a unsupported currency. Usually returned * along 400 Bad Request */ - LIBEUFIN_EC_BAD_CURRENCY(7) + LIBEUFIN_EC_BAD_CURRENCY(7), + + LIBEUFIN_EC_TIMEOUT_EXPIRED(8) } \ No newline at end of file