libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit b92daf77201c59b31820db077493ec81d94829aa
parent d943e1820abe29b7efbd2a8e6e66f0eb4d6504cd
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Fri,  6 Dec 2019 22:25:32 +0100

database primitives to check fractional values

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 10++++++++++
Msandbox/src/test/kotlin/DbTest.kt | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
3 files changed, 146 insertions(+), 21 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -19,10 +19,15 @@ package tech.libeufin.sandbox +import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction +import java.lang.ArithmeticException +import java.math.BigDecimal +import java.math.MathContext +import java.math.RoundingMode import java.sql.Blob import java.sql.Connection @@ -92,14 +97,67 @@ fun Blob.toByteArray(): ByteArray { return this.binaryStream.readAllBytes() } -object BankTransactionsTable : IntIdTable() { +/** + * Any number can become a Amount IF it does NOT need to be rounded to comply to the scale == 2. + */ +typealias Amount = BigDecimal + +open class IntIdTableWithAmount : IntIdTable() { + + 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(java.lang.Float.toString(valueFromDB)).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) + } + + return super.valueToDB(value) + } + } + + /** + * Make sure the number entered by upper layers does not need any rounding + * to conform to scale == 2 + */ + fun amount(name: String): Column<Amount> { + return registerColumn(name, AmountColumnType()) + } +} + + +object BankTransactionsTable : IntIdTableWithAmount() { /* Using varchar to store the IBAN - or possibly other formats * - from the counterpart. */ val counterpart = varchar("counterpart", MAX_ID_LENGTH) - val amountSign = integer("amountSign").check { (it eq 1) or (it eq -1)} - val amountValue = integer("amountValue").check { GreaterEqOp(it, intParam(0)) } - val amountFraction = integer("amountFraction").check { LessOp(it, intParam(100)) } + val amount = amount("amount") val subject = varchar("subject", MAX_SUBJECT_LENGTH) val date = date("date") val localCustomer = reference("localCustomer", BankCustomersTable) @@ -119,12 +177,12 @@ class BankTransactionEntity(id: EntityID<Int>) : IntEntity(id) { var subject by BankTransactionsTable.subject var date by BankTransactionsTable.date - - var amountValue by BankTransactionsTable.amountValue - var amountFraction by BankTransactionsTable.amountFraction - var amountSign by BankTransactionsTable.amountSign + var amount by BankTransactionsTable.amount } + + + /** * This table information *not* related to EBICS, for all * its customers. @@ -311,7 +369,6 @@ fun dbCreateTables() { TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE transaction { - // addLogger(StdOutSqlLogger) SchemaUtils.createMissingTablesAndColumns( BankCustomersTable, @@ -323,4 +380,4 @@ fun dbCreateTables() { EbicsOrderSignaturesTable ) } -} +} +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -43,6 +43,8 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.w3c.dom.Document +import java.lang.ArithmeticException +import java.math.BigDecimal import java.security.interfaces.RSAPublicKey import java.text.DateFormat import javax.sql.rowset.serial.SerialBlob @@ -53,6 +55,10 @@ val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") class CustomerNotFound(id: String?) : Exception("Customer ${id} not found") class BadInputData(inputData: String?) : Exception("Customer provided invalid input data: ${inputData}") +class BadAmount(badValue: Any?) : Exception("Value '${badValue}' is not a valid amount") +class UnacceptableFractional(statusCode: HttpStatusCode, badNumber: BigDecimal) : Exception( + "Unacceptable fractional part ${badNumber}" +) fun findCustomer(id: String?): BankCustomerEntity { @@ -156,6 +162,10 @@ fun main() { logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText("Internal server error.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) } + exception<ArithmeticException> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Invalid arithmetic attempted.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + } } // TODO: add another intercept call that adds schema validation before the response is sent intercept(ApplicationCallPipeline.Fallback) { diff --git a/sandbox/src/test/kotlin/DbTest.kt b/sandbox/src/test/kotlin/DbTest.kt @@ -5,34 +5,91 @@ import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction import org.joda.time.DateTime +import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.math.BigDecimal +import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode +import kotlin.math.abs import kotlin.test.assertFailsWith import kotlin.test.assertTrue + + + class DbTest { - @Test - fun valuesRange() { + @Before + fun muteStderr() { + System.setErr(PrintStream(ByteArrayOutputStream())) + } + @Before + fun connectAndMakeTables() { Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - transaction { - SchemaUtils.create(BankTransactionsTable) SchemaUtils.create(BankCustomersTable) + } + + } + + @Test + fun goodAmount() { + + transaction { - val customer = BankCustomerEntity.new { - name = "employee" + BankTransactionEntity.new { + amount = Amount("1") + counterpart = "IBAN" + subject = "Salary" + date = DateTime.now() + localCustomer = BankCustomerEntity.new { + name = "employee" + } + } + + BankTransactionEntity.new { + amount = Amount("1.11") + counterpart = "IBAN" + subject = "Salary" + date = DateTime.now() + localCustomer = BankCustomerEntity.new { + name = "employee" + } } - val ledgerEntry = BankTransactionEntity.new { - amountSign = 1 - amountValue = 5 - amountFraction = 0 + val x = BankTransactionEntity.new { + amount = Amount("1.110000000000") // BigDecimal does not crop the trailing zeros counterpart = "IBAN" subject = "Salary" date = DateTime.now() - localCustomer = customer + localCustomer = BankCustomerEntity.new { + name = "employee" + } + } + } + } + + @Test + fun badAmount() { + + assertFailsWith<BadAmount> { + transaction { + BankTransactionEntity.new { + amount = Amount("1.10001") + counterpart = "IBAN" + subject = "Salary" + date = DateTime.now() + localCustomer = BankCustomerEntity.new { + name = "employee" + } + } } } }