libeufin

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

commit bc085ebfbe87c4028e9f1257605872c53e213b15
parent 6b27dd42075a24591aed00d34c67b23b26e9f65a
Author: Antoine A <>
Date:   Tue, 22 Jul 2025 12:24:51 +0200

common: use default conversion rate with 0 ratio and 0.01 tiny amount

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 19++++++++++++++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 2--
Mbank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt | 42+++---------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt | 2--
Mbank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 4++--
Mbank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt | 27+++++++++++----------------
Mbank/src/test/kotlin/ConversionApiTest.kt | 25++++++++++++++++---------
Mdatabase-versioning/libeufin-bank-procedures.sql | 50+++++++++++++++++++++++++++++++-------------------
Mdatabase-versioning/libeufin-conversion-setup.sql | 8++------
Mtestbench/src/test/kotlin/IntegrationTest.kt | 17++++++++++-------
10 files changed, 93 insertions(+), 103 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -81,7 +81,24 @@ data class ConversionRate ( val cashout_tiny_amount: TalerAmount, val cashout_rounding_mode: RoundingMode, val cashout_min_amount: TalerAmount, -) +) { + fun check(cfg: BankConfig) { + for (regionalAmount in sequenceOf(cashin_fee, cashin_tiny_amount, cashout_min_amount)) { + cfg.checkRegionalCurrency(regionalAmount) + } + for (fiatAmount in sequenceOf(cashout_fee, cashout_tiny_amount, cashin_min_amount)) { + cfg.checkFiatCurrency(fiatAmount) + } + + if (cashout_tiny_amount.isZero()) { + throw badRequest("cashout_tiny_amount must be > 0") + } else if (cashin_tiny_amount.isZero()) { + throw badRequest("cashin_tiny_amount must be > 0") + } else if (cashout_tiny_amount.isSubCent()) { + throw badRequest("Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01") + } + } +} /** Load bank config at [configPath] */ fun bankConfig(configPath: Path?): BankConfig = BANK_CONFIG_SOURCE.fromFile(configPath).loadBankConfig() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -342,8 +342,6 @@ data class TokenInfos ( data class Config( val currency: String, val currency_specification: CurrencySpecification, - val fiat_currency: String? = null, - val fiat_currency_specification: CurrencySpecification? = null, val base_url: BaseURL?, val bank_name: String, val allow_conversion: Boolean, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt @@ -31,34 +31,19 @@ import tech.libeufin.bank.db.Database import tech.libeufin.common.* private suspend fun ApplicationCall.config(db: Database, cfg: BankConfig) { - val rate = db.conversion.getDefaultRate() - ?: throw apiError( - HttpStatusCode.NotImplemented, - "conversion rate not configured yet", - TalerErrorCode.END - ) respond( ConversionConfig( regional_currency = cfg.regionalCurrency, regional_currency_specification = cfg.regionalCurrencySpec, fiat_currency = cfg.fiatCurrency!!, fiat_currency_specification = cfg.fiatCurrencySpec!!, - conversion_rate = rate + conversion_rate = db.conversion.getDefaultRate() ) ) } private suspend fun ApplicationCall.setGlobal(db: Database, cfg: BankConfig) { val req = receive<ConversionRate>() - for (regionalAmount in sequenceOf(req.cashin_fee, req.cashin_tiny_amount, req.cashout_min_amount)) { - cfg.checkRegionalCurrency(regionalAmount) - } - for (fiatAmount in sequenceOf(req.cashout_fee, req.cashout_tiny_amount, req.cashin_min_amount)) { - cfg.checkFiatCurrency(fiatAmount) - } - // cashout conversion tiny amount must >= 0.01 as it is what EBICS supports - if (req.cashout_tiny_amount.isSubCent()) { - throw badRequest("Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01") - } + req.check(cfg) db.conversion.updateConfig(req) respond(HttpStatusCode.NoContent) } @@ -82,11 +67,6 @@ fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allow "$input is too small to be converted", TalerErrorCode.BANK_BAD_CONVERSION ) - ConversionResult.MissingConfig -> throw apiError( - HttpStatusCode.NotImplemented, - "conversion rate not configured yet", - TalerErrorCode.END - ) ConversionResult.IsExchange -> throw conflict( "exchange accounts cannot cashout", TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE @@ -98,13 +78,7 @@ fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allow } } get("/conversion-info/rate") { - val rate = db.conversion.getDefaultRate() - ?: throw apiError( - HttpStatusCode.NotImplemented, - "conversion rate not configured yet", - TalerErrorCode.END - ) - call.respond(rate) + call.respond(db.conversion.getDefaultRate()) } get("/conversion-info/cashout-rate") { val params = RateParams.extract(call.request.queryParameters) @@ -142,11 +116,6 @@ fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allow get("/conversion-rate-classes/{CLASS_ID}/conversion-info/rate") { val id = call.longPath("CLASS_ID") val rate = db.conversion.getClassRate(id) - ?: throw apiError( - HttpStatusCode.NotImplemented, - "conversion rate not configured yet", - TalerErrorCode.END - ) call.respond(rate) } get("/conversion-rate-classes/{CLASS_ID}/conversion-info/cashout-rate") { @@ -203,11 +172,6 @@ fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allow optAuth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { get("/accounts/{USERNAME}/conversion-info/rate") { val (isExchange, rate) = db.conversion.getUserRate(call.username) - ?: throw apiError( - HttpStatusCode.NotImplemented, - "conversion rate not configured yet", - TalerErrorCode.END - ) if (!isExchange && !call.isAuthenticated) { throw forbidden("Non exchange account rates are private") } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -58,8 +58,6 @@ fun Routing.coreBankApi(db: Database, cfg: BankConfig) { base_url = cfg.baseUrl, currency = cfg.regionalCurrency, currency_specification = cfg.regionalCurrencySpec, - fiat_currency = cfg.fiatCurrency, - fiat_currency_specification = cfg.fiatCurrencySpec, allow_conversion = cfg.allowConversion, allow_registrations = cfg.allowRegistration, allow_deletions = cfg.allowAccountDeletion, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -561,7 +561,7 @@ class AccountDAO(private val db: Database) { cashout_rounding_mode FROM customers JOIN bank_accounts ON customer_id=owning_customer_id - LEFT JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) on true + CROSS JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) WHERE username=? """ ) { @@ -679,7 +679,7 @@ class AccountDAO(private val db: Database) { cashout_rounding_mode FROM bank_accounts JOIN customers ON owning_customer_id = customer_id - LEFT JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) on true + CROSS JOIN LATERAL get_conversion_class_rate(conversion_rate_class_id) WHERE ${if (params.usernameFilter != null) "name ILIKE ? AND" else ""} ${when (params.conversionRateClassId) { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -29,8 +29,7 @@ import org.postgresql.util.PSQLState class ConversionDAO(private val db: Database) { companion object { fun userRate(db: Database, it: ResultSet, username: String, isTalerExchange: Boolean): ConversionRate? { - val hasRate = it.getObject("cashin_rounding_mode") != null - return if (!hasRate || db.fiatCurrency == null) { + return if (db.fiatCurrency == null) { null } else if (username == "admin") { ConversionRate( @@ -38,7 +37,7 @@ class ConversionDAO(private val db: Database) { cashin_fee = TalerAmount.zero(db.bankCurrency), cashin_tiny_amount = TalerAmount.zero(db.bankCurrency), cashin_rounding_mode = RoundingMode.zero, - cashin_min_amount = TalerAmount.zero(db.fiatCurrency!!), + cashin_min_amount = TalerAmount.zero(db.fiatCurrency), cashout_ratio = DecimalNumber.ZERO, cashout_fee = TalerAmount.zero(db.fiatCurrency), cashout_tiny_amount = TalerAmount.zero(db.fiatCurrency), @@ -51,7 +50,7 @@ class ConversionDAO(private val db: Database) { cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), cashin_tiny_amount = it.getAmount("cashin_tiny_amount", db.bankCurrency), cashin_rounding_mode = it.getEnum("cashin_rounding_mode"), - cashin_min_amount = it.getAmount("cashin_min_amount", db.fiatCurrency!!), + cashin_min_amount = it.getAmount("cashin_min_amount", db.fiatCurrency), cashout_ratio = DecimalNumber.ZERO, cashout_fee = TalerAmount.zero(db.fiatCurrency), cashout_tiny_amount = TalerAmount.zero(db.fiatCurrency), @@ -64,7 +63,7 @@ class ConversionDAO(private val db: Database) { cashin_fee = TalerAmount.zero(db.bankCurrency), cashin_tiny_amount = TalerAmount.zero(db.bankCurrency), cashin_rounding_mode = RoundingMode.zero, - cashin_min_amount = TalerAmount.zero(db.fiatCurrency!!), + cashin_min_amount = TalerAmount.zero(db.fiatCurrency), cashout_ratio = it.getDecimal("cashout_ratio"), cashout_fee = it.getAmount("cashout_fee", db.fiatCurrency), cashout_tiny_amount = it.getAmount("cashout_tiny_amount", db.fiatCurrency), @@ -96,7 +95,7 @@ class ConversionDAO(private val db: Database) { } /** Get default conversion rate */ - suspend fun getDefaultRate(): ConversionRate? = db.serializable(""" + suspend fun getDefaultRate(): ConversionRate = db.serializable(""" SELECT (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, @@ -110,7 +109,7 @@ class ConversionDAO(private val db: Database) { cashout_rounding_mode FROM config_get_conversion_rate() """) { - oneOrNull { + one { ConversionRate( cashin_ratio = it.getDecimal("cashin_ratio"), cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), @@ -127,7 +126,7 @@ class ConversionDAO(private val db: Database) { } /** Get conversion class rate */ - suspend fun getClassRate(conversionRateClassId: Long): ConversionRate? = db.serializable(""" + suspend fun getClassRate(conversionRateClassId: Long): ConversionRate = db.serializable(""" SELECT (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, @@ -142,7 +141,7 @@ class ConversionDAO(private val db: Database) { FROM get_conversion_class_rate(?) """) { bind(conversionRateClassId) - oneOrNull { + one { ConversionRate( cashin_ratio = it.getDecimal("cashin_ratio"), cashin_fee = it.getAmount("cashin_fee", db.bankCurrency), @@ -159,7 +158,7 @@ class ConversionDAO(private val db: Database) { } /** Get user rate */ - suspend fun getUserRate(username: String): Pair<Boolean, ConversionRate>? = db.serializable(""" + suspend fun getUserRate(username: String): Pair<Boolean, ConversionRate> = db.serializable(""" SELECT (cashin_ratio).val as cashin_ratio_val, (cashin_ratio).frac as cashin_ratio_frac, (cashin_fee).val as cashin_fee_val, (cashin_fee).frac as cashin_fee_frac, @@ -178,7 +177,7 @@ class ConversionDAO(private val db: Database) { WHERE username=? """) { bind(username) - oneOrNull { + one { val isTalerExchange = it.getBoolean("is_taler_exchange") val rate = ConversionDAO.userRate(db, it, username,isTalerExchange)!! Pair(isTalerExchange, rate) @@ -196,21 +195,19 @@ class ConversionDAO(private val db: Database) { sealed interface ConversionResult { data class Success(val converted: TalerAmount): ConversionResult data object ToSmall: ConversionResult - data object MissingConfig: ConversionResult data object IsExchange: ConversionResult data object NotExchange: ConversionResult } /** Perform [direction] conversion of [amount] using in-db [function] */ private suspend fun conversion(amount: TalerAmount, function: String, direction: String, conversionRateClassId: Long?): ConversionResult = db.serializable( - "SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM conversion_$function((?, ?)::taler_amount, ?, ?)" + "SELECT too_small, (converted).val AS amount_val, (converted).frac AS amount_frac FROM conversion_$function((?, ?)::taler_amount, ?, ?)" ) { bind(amount) bind(direction) bind(conversionRateClassId) one { when { - it.getBoolean("no_config") -> ConversionResult.MissingConfig it.getBoolean("too_small") -> ConversionResult.ToSmall else -> ConversionResult.Success( it.getAmount("amount", if (amount.currency == db.bankCurrency) db.fiatCurrency!! else db.bankCurrency) @@ -223,7 +220,6 @@ class ConversionDAO(private val db: Database) { SELECT is_taler_exchange, too_small, - no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM bank_accounts @@ -238,7 +234,6 @@ class ConversionDAO(private val db: Database) { one { val isExchange = it.getBoolean("is_taler_exchange") when { - it.getBoolean("no_config") -> ConversionResult.MissingConfig direction == "cashout" && isExchange -> ConversionResult.IsExchange direction == "cashin" && !isExchange -> ConversionResult.NotExchange it.getBoolean("too_small") -> ConversionResult.ToSmall diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt @@ -29,9 +29,9 @@ class ConversionApiTest { // GET /conversion-info/config @Test fun config() = bankSetup { - client.get("/conversion-info/config").assertOk() - client.get("/conversion-rate-classes/1/conversion-info/config").assertOk() - client.get("/accounts/merchant/conversion-info/config").assertOk() + client.get("/conversion-info/config").assertOkJson<ConversionConfig>() + client.get("/conversion-rate-classes/1/conversion-info/config").assertOkJson<ConversionConfig>() + client.get("/accounts/merchant/conversion-info/config").assertOkJson<ConversionConfig>() } // POST /conversion-info/conversion-rate @@ -63,6 +63,17 @@ class ConversionApiTest { "cashout_fee" to "CHF:0.003" } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + // Zero tiny amount + client.postAdmin("$prefix/conversion-info/conversion-rate") { + json(ok) { + "cashout_tiny_amount" to "EUR:0" + } + }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID) + client.postAdmin("$prefix/conversion-info/conversion-rate") { + json(ok) { + "cashin_tiny_amount" to "KUDOS:0" + } + }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID) // Subcent cashout tiny amount client.postAdmin("$prefix/conversion-info/conversion-rate") { json(ok) { @@ -172,15 +183,11 @@ class ConversionApiTest { } for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) { client.getAdmin("$prefix/conversion-info/config") - .assertNotImplemented() - client.getAdmin("$prefix/conversion-info/cashin-rate") - .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) + .assertOkJson<ConversionConfig>() client.getAdmin("$prefix/conversion-info/cashout-rate") .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) - client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=KUDOS:1") - .assertNotImplemented() client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=EUR:1") - .assertNotImplemented() + .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) } } diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -1268,7 +1268,6 @@ CREATE FUNCTION cashin( -- Error status OUT out_no_account BOOLEAN, OUT out_too_small BOOLEAN, - OUT out_no_config BOOLEAN, OUT out_balance_insufficient BOOLEAN ) LANGUAGE plpgsql AS $$ @@ -1302,10 +1301,10 @@ SELECT bank_account_id WHERE username = 'admin'; -- Perform conversion -SELECT (converted).val, (converted).frac, too_small, no_config - INTO converted_amount.val, converted_amount.frac, out_too_small, out_no_config +SELECT (converted).val, (converted).frac, too_small + INTO converted_amount.val, converted_amount.frac, out_too_small FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id); -IF out_too_small OR out_no_config THEN +IF out_too_small THEN RETURN; END IF; @@ -1386,7 +1385,7 @@ ELSIF out_account_is_exchange OR out_no_cashout_payto THEN END IF; -- check conversion -SELECT under_min, too_small OR no_config OR in_amount_credit!=converted +SELECT under_min, too_small OR in_amount_credit!=converted INTO out_under_min, out_bad_conversion FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id); IF out_bad_conversion THEN @@ -1803,6 +1802,12 @@ DECLARE amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error tiny_numeric NUMERIC(24); BEGIN + -- Handle no config case + IF ratio = (0, 0)::taler_amount THEN + out_too_small=TRUE; + RETURN; + END IF; + -- Perform multiplication using big numbers amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); @@ -1848,6 +1853,12 @@ DECLARE tiny_numeric NUMERIC(24); roundtrip BOOLEAN; BEGIN + -- Handle no config case + IF ratio = (0, 0)::taler_amount THEN + bad_value=TRUE; + RETURN; + END IF; + -- Apply fees amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); @@ -1885,8 +1896,7 @@ CREATE FUNCTION conversion_to( IN conversion_rate_class_id INT8, OUT converted taler_amount, OUT too_small BOOLEAN, - OUT under_min BOOLEAN, - OUT no_config BOOLEAN + OUT under_min BOOLEAN ) LANGUAGE plpgsql STABLE AS $$ DECLARE @@ -1926,8 +1936,6 @@ BEGIN mode FROM get_conversion_class_rate(conversion_rate_class_id); END IF; - no_config = NOT FOUND; - IF no_config THEN RETURN; END IF; -- Check min amount SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount); @@ -1948,12 +1956,11 @@ CREATE FUNCTION conversion_from( IN conversion_rate_class_id INT8, OUT converted taler_amount, OUT too_small BOOLEAN, - OUT under_min BOOLEAN, - OUT no_config BOOLEAN + OUT under_min BOOLEAN ) LANGUAGE plpgsql STABLE AS $$ DECLARE - at_ratio taler_amount; + ratio taler_amount; out_fee taler_amount; tiny_amount taler_amount; reverse_tiny_amount taler_amount; @@ -1970,7 +1977,7 @@ BEGIN (cashin_min_amount).val, (cashin_min_amount).frac, cashin_rounding_mode INTO - at_ratio.val, at_ratio.frac, + ratio.val, ratio.frac, out_fee.val, out_fee.frac, tiny_amount.val, tiny_amount.frac, reverse_tiny_amount.val, reverse_tiny_amount.frac, @@ -1986,7 +1993,7 @@ BEGIN (cashout_min_amount).val, (cashout_min_amount).frac, cashout_rounding_mode INTO - at_ratio.val, at_ratio.frac, + ratio.val, ratio.frac, out_fee.val, out_fee.frac, tiny_amount.val, tiny_amount.frac, reverse_tiny_amount.val, reverse_tiny_amount.frac, @@ -1994,13 +2001,14 @@ BEGIN mode FROM get_conversion_class_rate(conversion_rate_class_id); END IF; - no_config = NOT FOUND; - IF no_config THEN RETURN; END IF; -- Perform conversion - SELECT (result).val, (result).frac INTO converted.val, converted.frac - FROM conversion_revert_ratio(amount, at_ratio, out_fee, tiny_amount, mode, reverse_tiny_amount); - + SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small + FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount); + IF too_small THEN + RETURN; + END IF; + -- Check min amount SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount); IF too_small THEN @@ -2035,6 +2043,10 @@ LANGUAGE sql STABLE AS $$ (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount, (value->'cashout'->>'rounding_mode')::rounding_mode FROM config WHERE key='conversion_rate' + UNION ALL + SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode, + (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode + LIMIT 1 $$; CREATE FUNCTION get_conversion_class_rate( diff --git a/database-versioning/libeufin-conversion-setup.sql b/database-versioning/libeufin-conversion-setup.sql @@ -66,7 +66,6 @@ LANGUAGE plpgsql AS $$ too_small BOOLEAN; balance_insufficient BOOLEAN; no_account BOOLEAN; - no_config BOOLEAN; BEGIN -- Only reserve transaction triggers cashin IF NEW.type != 'reserve' THEN @@ -78,8 +77,8 @@ LANGUAGE plpgsql AS $$ FROM libeufin_nexus.incoming_transactions WHERE incoming_transaction_id = NEW.incoming_transaction_id; SET search_path TO libeufin_bank; - SELECT out_too_small, out_balance_insufficient, out_no_account, out_no_config - INTO too_small, balance_insufficient, no_account, no_config + SELECT out_too_small, out_balance_insufficient, out_no_account + INTO too_small, balance_insufficient, no_account FROM libeufin_bank.cashin(now_date, NEW.metadata, local_amount, local_subject); SET search_path TO libeufin_nexus; @@ -101,9 +100,6 @@ LANGUAGE plpgsql AS $$ END IF; -- Error on hard failures - IF no_config THEN - RAISE EXCEPTION 'cashin currency conversion failed: missing conversion rates'; - END IF; IF no_account THEN RAISE EXCEPTION 'cashin failed: missing exchange account'; END IF; diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt @@ -175,6 +175,7 @@ class IntegrationTest { assertException("ERROR: cashin failed: missing exchange account") { registerIncomingPayment(db, cfg, reservePayment) } + db.checkCount(0, 0, 0) // But KYC works registerIncomingPayment( @@ -184,13 +185,15 @@ class IntegrationTest { subject = "Error test KYC:${EddsaPublicKey.randEdsaKey()}" ) ) + db.checkCount(1, 0, 1) // Create exchange account bankCmd.run("create-account $flags -u exchange -p exchange-password --name 'Mr Money' --exchange") - assertException("ERROR: cashin currency conversion failed: missing conversion rates") { - registerIncomingPayment(db, cfg, reservePayment) - } + + // Missing rates + registerIncomingPayment(db, cfg, reservePayment.copy(id = IncomingId(null, "rate_error", null))) + db.checkCount(2, 1, 1) // Start server server { @@ -221,11 +224,11 @@ class IntegrationTest { bankCmd.run("edit-account admin --debit_threshold KUDOS:100 $flags") // Too small amount - db.checkCount(1, 0, 1) + db.checkCount(2, 1, 1) registerIncomingPayment(db, cfg, reservePayment.copy( amount = TalerAmount("EUR:0.01"), )) - db.checkCount(2, 1, 1) + db.checkCount(3, 2, 1) client.getA("/accounts/exchange/transactions").assertNoContent() // Check success @@ -234,7 +237,7 @@ class IntegrationTest { id = IncomingId(null, "success", null), ) registerIncomingPayment(db, cfg, validPayment) - db.checkCount(3, 1, 2) + db.checkCount(4, 2, 2) client.getA("/accounts/exchange/transactions") .assertOkJson<BankAccountTransactionsResponse>() @@ -243,7 +246,7 @@ class IntegrationTest { registerIncomingPayment(db, cfg, validPayment.copy( subject="Success 2 $reservePub" )) - db.checkCount(3, 1, 2) + db.checkCount(4, 2, 2) } }