libeufin

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

commit 48cfa63e024f3b7a8b5c0473cf982fe87a2ee673
parent 9439260e0b60b5a5fa4a1d636e9f0075396a6b76
Author: Antoine A <>
Date:   Tue, 31 Oct 2023 16:27:23 +0000

Add min_amount to conversion info

Diffstat:
Mbank/conf/test.conf | 10+++++-----
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 20++++++++++++--------
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 12++++++++++--
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 39+++++++++++++++++++++++----------------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 11+++++++----
Mdatabase-versioning/libeufin-bank-procedures.sql | 24+++++++++++++++++-------
6 files changed, 74 insertions(+), 42 deletions(-)

diff --git a/bank/conf/test.conf b/bank/conf/test.conf @@ -12,9 +12,10 @@ SQL_DIR = $DATADIR/sql/ CONFIG = postgresql:///libeufincheck [libeufin-bank-conversion] -buy_at_ratio = 0.8 -buy_in_fee = 0.02 +buy_ratio = 0.8 +buy_fee = 0.02 buy_tiny_amount = KUDOS:0.01 buy_rounding_mode = nearest -sell_at_ratio = 1.25 -sell_out_fee = 0.003 -\ No newline at end of file +sell_ratio = 1.25 +sell_fee = 0.003 +sell_min_amount = KUDOS:0.1 diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -84,14 +84,16 @@ data class BankConfig( @Serializable data class ConversionInfo ( - val buy_at_ratio: DecimalNumber, - val buy_in_fee: DecimalNumber, + val buy_ratio: DecimalNumber, + val buy_fee: DecimalNumber, val buy_tiny_amount: TalerAmount, val buy_rounding_mode: RoundingMode, - val sell_at_ratio: DecimalNumber, - val sell_out_fee: DecimalNumber, + val buy_min_amount: TalerAmount, + val sell_ratio: DecimalNumber, + val sell_fee: DecimalNumber, val sell_tiny_amount: TalerAmount, val sell_rounding_mode: RoundingMode, + val sell_min_amount: TalerAmount, ) data class ServerConfig( @@ -150,14 +152,16 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { private fun TalerConfig.loadConversionInfo(currency: String, fiatCurrency: String): ConversionInfo = catchError { ConversionInfo( - buy_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "buy_at_ratio"), - buy_in_fee = requireDecimalNumber("libeufin-bank-conversion", "buy_in_fee"), + buy_ratio = requireDecimalNumber("libeufin-bank-conversion", "buy_ratio"), + buy_fee = requireDecimalNumber("libeufin-bank-conversion", "buy_fee"), buy_tiny_amount = amount("libeufin-bank-conversion", "buy_tiny_amount", currency) ?: TalerAmount(0, 1, currency), buy_rounding_mode = RoundingMode("libeufin-bank-conversion", "buy_rounding_mode") ?: RoundingMode.zero, - sell_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_at_ratio"), - sell_out_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_out_fee"), + buy_min_amount = amount("libeufin-bank-conversion", "buy_min_amount", fiatCurrency) ?: TalerAmount(0, 0, fiatCurrency), + sell_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_ratio"), + sell_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_fee"), sell_tiny_amount = amount("libeufin-bank-conversion", "sell_tiny_amount", fiatCurrency) ?: TalerAmount(0, 1, fiatCurrency), sell_rounding_mode = RoundingMode("libeufin-bank-conversion", "sell_rounding_mode") ?: RoundingMode.zero, + sell_min_amount = amount("libeufin-bank-conversion", "sell_min_amount", currency) ?: TalerAmount(0, 0, currency), ) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -567,7 +567,11 @@ fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { params.credit?.let { ctx.checkFiatCurrency(it) } if (params.debit != null) { - val credit = db.conversionInternalToFiat(params.debit) + val credit = db.conversionInternalToFiat(params.debit) ?: + throw conflict( + "${params.debit} is too small to be converted", + TalerErrorCode.BANK_BAD_CONVERSION + ) call.respond(ConversionResponse(params.debit, credit)) } else { call.respond(HttpStatusCode.NotImplemented) // TODO @@ -580,7 +584,11 @@ fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { params.credit?.let { ctx.checkInternalCurrency(it) } if (params.debit != null) { - val credit = db.conversionFiatToInternal(params.debit) + val credit = db.conversionFiatToInternal(params.debit) ?: + throw conflict( + "${params.debit} is too small to be converted", + TalerErrorCode.BANK_BAD_CONVERSION + ) call.respond(ConversionResponse(params.debit, credit)) } else { call.respond(HttpStatusCode.NotImplemented) // TODO diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -1544,10 +1544,10 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f it.transaction { conn -> var stmt = conn.prepareStatement("CALL config_set_amount(?, (?, ?)::taler_amount)") for ((name, amount) in listOf( - Pair("buy_ratio", cfg.buy_at_ratio), - Pair("buy_fee", cfg.buy_in_fee), - Pair("sell_ratio", cfg.sell_at_ratio), - Pair("sell_fee", cfg.sell_out_fee), + Pair("buy_ratio", cfg.buy_ratio), + Pair("buy_fee", cfg.buy_fee), + Pair("sell_ratio", cfg.sell_ratio), + Pair("sell_fee", cfg.sell_fee), )) { stmt.setString(1, name) stmt.setLong(2, amount.value) @@ -1556,7 +1556,9 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f } for ((name, amount) in listOf( Pair("buy_tiny_amount", cfg.buy_tiny_amount), - Pair("sell_tiny_amount", cfg.sell_tiny_amount) + Pair("buy_min_amount", cfg.buy_min_amount), + Pair("sell_tiny_amount", cfg.sell_tiny_amount), + Pair("sell_min_amount", cfg.sell_min_amount), )) { stmt.setString(1, name) stmt.setLong(2, amount.value) @@ -1575,22 +1577,27 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f } } - private suspend fun conversionTo(amount: TalerAmount, name: String, currency: String): TalerAmount = conn { conn -> - val stmt = conn.prepareStatement("SELECT amount.val AS amount_val, amount.frac AS amount_frac FROM conversion_to((?, ?)::taler_amount, ?) as amount") + private suspend fun conversionTo(amount: TalerAmount, name: String, currency: String): TalerAmount? = conn { conn -> + val stmt = conn.prepareStatement("SELECT too_small, (to_amount).val AS amount_val, (to_amount).frac AS amount_frac FROM conversion_to((?, ?)::taler_amount, ?)") stmt.setLong(1, amount.value) stmt.setInt(2, amount.frac) stmt.setString(3, name) - stmt.oneOrNull { - TalerAmount( - value = it.getLong("amount_val"), - frac = it.getInt("amount_frac"), - currency = currency - ) - }!! + stmt.executeQuery().use { + it.next() + if (!it.getBoolean("too_small")) { + TalerAmount( + value = it.getLong("amount_val"), + frac = it.getInt("amount_frac"), + currency = currency + ) + } else { + null + } + } } - suspend fun conversionInternalToFiat(amount: TalerAmount): TalerAmount = conversionTo(amount, "sell", fiatCurrency!!) - suspend fun conversionFiatToInternal(amount: TalerAmount): TalerAmount = conversionTo(amount, "buy", bankCurrency) + suspend fun conversionInternalToFiat(amount: TalerAmount): TalerAmount? = conversionTo(amount, "sell", fiatCurrency!!) + suspend fun conversionFiatToInternal(amount: TalerAmount): TalerAmount? = conversionTo(amount, "buy", bankCurrency) } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1259,10 +1259,13 @@ class CoreBankCashoutApiTest { // No amount client.get("/cashout-rate").assertBadRequest() + // Too small + client.get("/cashout-rate?amount_debit=KUDOS:0.08") + .assertConflict().assertErr(TalerErrorCode.BANK_BAD_CONVERSION) // Wrong currency - client.get("/cashout-rate?amount_debit=FIAT:1").assertBadRequest() + client.get("/cashout-rate?amount_debit=FIAT:1") .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.get("/cashout-rate?amount_credit=KUDOS:1").assertBadRequest() + client.get("/cashout-rate?amount_credit=KUDOS:1") .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) } @@ -1282,9 +1285,9 @@ class CoreBankCashoutApiTest { // No amount client.get("/cashin-rate").assertBadRequest() // Wrong currency - client.get("/cashin-rate?amount_debit=KUDOS:1").assertBadRequest() + client.get("/cashin-rate?amount_debit=KUDOS:1") .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - client.get("/cashin-rate?amount_credit=FIAT:1").assertBadRequest() + client.get("/cashin-rate?amount_credit=FIAT:1") .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) } } \ No newline at end of file diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -1053,7 +1053,7 @@ DECLARE account_id BIGINT; BEGIN -- check conversion -SELECT in_amount_credit!=expected INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'sell'::text) AS expected; +SELECT too_small OR in_amount_credit!=to_amount INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'sell'::text); IF out_bad_conversion THEN RETURN; END IF; @@ -1284,26 +1284,36 @@ $$; CREATE OR REPLACE FUNCTION conversion_to( IN from_amount taler_amount, IN name TEXT, - OUT to_amount taler_amount + OUT to_amount taler_amount, + OUT too_small BOOLEAN ) LANGUAGE plpgsql AS $$ DECLARE at_ratio taler_amount; out_fee taler_amount; tiny_amount taler_amount; + min_amount taler_amount; mode rounding_mode; - calculation_ok BOOLEAN; BEGIN + -- Check min amount + SELECT value['val']::int8, value['frac']::int4 INTO min_amount.val, min_amount.frac FROM config WHERE key=name||'_min_amount'; + SELECT NOT ok INTO too_small FROM amount_left_minus_right(from_amount, min_amount); + IF too_small THEN + to_amount = (0, 0); + RETURN; + END IF; + + -- Perform conversion SELECT value['val']::int8, value['frac']::int4 INTO at_ratio.val, at_ratio.frac FROM config WHERE key=name||'_ratio'; SELECT value['val']::int8, value['frac']::int4 INTO out_fee.val, out_fee.frac FROM config WHERE key=name||'_fee'; SELECT value['val']::int8, value['frac']::int4 INTO tiny_amount.val, tiny_amount.frac FROM config WHERE key=name||'_tiny_amount'; SELECT (value->>'mode')::rounding_mode INTO mode FROM config WHERE key=name||'_rounding_mode'; - -- TODO minimum amount + SELECT product.val, product.frac INTO to_amount.val, to_amount.frac FROM amount_mul(from_amount, at_ratio, tiny_amount, mode) as product; - SELECT (diff).val, (diff).frac, ok INTO to_amount.val, to_amount.frac, calculation_ok FROM amount_left_minus_right(to_amount, out_fee); + SELECT (diff).val, (diff).frac, NOT ok INTO to_amount.val, to_amount.frac, too_small FROM amount_left_minus_right(to_amount, out_fee); - IF NOT calculation_ok THEN - to_amount = (0, 0); -- TODO how to handle zero and less than zero ? + IF too_small THEN + to_amount = (0, 0); END IF; END $$;