libeufin

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

commit f0dd2c8a3a2c28494614330102db90d02b17010c
parent 2588cce790a0ac1fecee98cfd3321fae4cf59268
Author: Antoine A <>
Date:   Wed,  1 Nov 2023 10:38:00 +0000

Add max debt patching and improve config endpoint

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 22++++++++++++++++------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 18++++++++++++++----
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 7+++++--
Mbank/src/test/kotlin/CoreBankApiTest.kt | 48++++++++++++++++++++++++++----------------------
Mdatabase-versioning/libeufin-bank-procedures.sql | 47++++++++++++++++++++---------------------------
5 files changed, 81 insertions(+), 61 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -40,12 +40,14 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.account fun Routing.coreBankApi(db: Database, ctx: BankConfig) { get("/config") { call.respond( - Config( - currency = ctx.currencySpecification, - have_cashout = ctx.haveCashout, - fiat_currency = ctx.fiatCurrency, - conversion_info = ctx.conversionInfo - ) + Config( + currency = ctx.currencySpecification, + have_cashout = ctx.haveCashout, + fiat_currency = ctx.fiatCurrency, + conversion_info = ctx.conversionInfo, + allow_registrations = !ctx.restrictRegistration, + allow_deletions = !ctx.restrictAccountDeletion + ) ) } authAdmin(db, TokenScope.readonly) { @@ -214,6 +216,11 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { if (username == "admin") throw forbidden("admin account not patchable") val req = call.receive<AccountReconfiguration>() + req.debit_threshold?.run { ctx.checkInternalCurrency(this) } + + if (req.is_exchange != null && !isAdmin) + throw forbidden("non-admin user cannot change their exchange nature") + val res = db.accountReconfig( login = username, name = req.name, @@ -221,6 +228,7 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { emailAddress = req.challenge_contact_data?.email, isTalerExchange = req.is_exchange, phoneNumber = req.challenge_contact_data?.phone, + debtLimit = req.debit_threshold, isAdmin = isAdmin ) when (res) { @@ -231,6 +239,8 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { ) CustomerPatchResult.CONFLICT_LEGAL_NAME -> throw forbidden("non-admin user cannot change their legal name") + CustomerPatchResult.CONFLICT_DEBT_LIMIT -> + throw forbidden("non-admin user cannot change their debt limit") } } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -437,13 +437,15 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f phoneNumber: String?, emailAddress: String?, isTalerExchange: Boolean?, + debtLimit: TalerAmount?, isAdmin: Boolean ): CustomerPatchResult = conn { conn -> val stmt = conn.prepareStatement(""" SELECT out_not_found, - out_legal_name_change - FROM account_reconfig(?, ?, ?, ?, ?, ?, ?) + out_legal_name_change, + out_debt_limit_change + FROM account_reconfig(?, ?, ?, ?, ?, ?, (?, ?)::taler_amount, ?) """) stmt.setString(1, login) stmt.setString(2, name) @@ -453,13 +455,20 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f if (isTalerExchange == null) stmt.setNull(6, Types.NULL) else stmt.setBoolean(6, isTalerExchange) - stmt.setBoolean(7, isAdmin) - + if (debtLimit == null) { + stmt.setNull(7, Types.NULL) + stmt.setNull(8, Types.NULL) + } else { + stmt.setLong(7, debtLimit.value) + stmt.setInt(8, debtLimit.frac) + } + stmt.setBoolean(9, isAdmin) stmt.executeQuery().use { when { !it.next() -> throw internalServerError("accountReconfig() returned nothing") it.getBoolean("out_not_found") -> CustomerPatchResult.ACCOUNT_NOT_FOUND it.getBoolean("out_legal_name_change") -> CustomerPatchResult.CONFLICT_LEGAL_NAME + it.getBoolean("out_debt_limit_change") -> CustomerPatchResult.CONFLICT_DEBT_LIMIT else -> CustomerPatchResult.SUCCESS } } @@ -1613,6 +1622,7 @@ enum class CustomerCreationResult { enum class CustomerPatchResult { ACCOUNT_NOT_FOUND, CONFLICT_LEGAL_NAME, + CONFLICT_DEBT_LIMIT, SUCCESS } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -256,7 +256,9 @@ data class Config( val currency: CurrencySpecification, val have_cashout: Boolean, val fiat_currency: String?, - val conversion_info: ConversionInfo? + val conversion_info: ConversionInfo?, + val allow_registrations: Boolean, + val allow_deletions: Boolean ) { val name: String = "libeufin-bank" val version: String = "0:0:0" @@ -619,5 +621,6 @@ data class AccountReconfiguration( val challenge_contact_data: ChallengeContactData?, val cashout_address: IbanPayTo?, val name: String?, - val is_exchange: Boolean? + val is_exchange: Boolean?, + val debit_threshold: TalerAmount? ) \ No newline at end of file diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -8,6 +8,7 @@ import io.ktor.server.engine.* import io.ktor.server.testing.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement import net.taler.wallet.crypto.Base32Crockford import net.taler.common.errorcodes.TalerErrorCode import org.junit.Test @@ -345,13 +346,13 @@ class CoreBankAccountsMgmtApiTest { @Test fun accountReconfig() = bankSetup { _ -> // Successful attempt now. + val cashout = IbanPayTo(genIbanPaytoUri()) val req = json { - "cashout_address" to IbanPayTo(genIbanPaytoUri()).canonical + "cashout_address" to cashout.canonical "challenge_contact_data" to json { - "email" to "new@example.com" - "phone" to "+987" + "phone" to "+99" + "email" to "foo@example.com" } - "is_exchange" to true } client.patch("/accounts/merchant") { basicAuth("merchant", "merchant-password") @@ -363,36 +364,39 @@ class CoreBankAccountsMgmtApiTest { jsonBody(req) }.assertNoContent() - val cashout = IbanPayTo(genIbanPaytoUri()) - val nameReq = json { - "login" to "foo" - "name" to "Another Foo" - "cashout_address" to cashout.canonical - "challenge_contact_data" to json { - "phone" to "+99" - "email" to "foo@example.com" - } + suspend fun checkAdminOnly(req: JsonElement) { + // Checking ordinary user doesn't get to patch + client.patch("/accounts/merchant") { + basicAuth("merchant", "merchant-password") + jsonBody(req) + }.assertForbidden() + // Finally checking that admin does get to patch + client.patch("/accounts/merchant") { + basicAuth("admin", "admin-password") + jsonBody(req) + }.assertNoContent() } - // Checking ordinary user doesn't get to patch their name. - client.patch("/accounts/merchant") { - basicAuth("merchant", "merchant-password") - jsonBody(nameReq) - }.assertForbidden() - // Finally checking that admin does get to patch foo's name. + + checkAdminOnly(json(req) { "name" to "Another Foo" }) + checkAdminOnly(json(req) { "is_exchange" to true }) + checkAdminOnly(json(req) { "debit_threshold" to "KUDOS:100" }) + + // Check currency client.patch("/accounts/merchant") { basicAuth("admin", "admin-password") - jsonBody(nameReq) - }.assertNoContent() + jsonBody(json(req) { "debit_threshold" to "EUR:100" }) + }.assertBadRequest() // Check patch client.get("/accounts/merchant") { basicAuth("admin", "admin-password") - }.assertOk().run { + }.assertOk().run { val obj: AccountData = Json.decodeFromString(bodyAsText()) assertEquals("Another Foo", obj.name) assertEquals(cashout.canonical, obj.cashout_payto_uri?.canonical) assertEquals("+99", obj.contact_data?.phone) assertEquals("foo@example.com", obj.contact_data?.email) + assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold) } } diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -161,54 +161,47 @@ CREATE OR REPLACE FUNCTION account_reconfig( IN in_email TEXT, IN in_cashout_payto TEXT, IN in_is_taler_exchange BOOLEAN, + IN in_max_debt taler_amount, IN in_is_admin BOOLEAN, OUT out_not_found BOOLEAN, - OUT out_legal_name_change BOOLEAN + OUT out_legal_name_change BOOLEAN, + OUT out_debt_limit_change BOOLEAN ) LANGUAGE plpgsql AS $$ DECLARE my_customer_id INT8; BEGIN +-- Get user ID and check reconfig rights SELECT customer_id, - in_name IS NOT NULL AND name != in_name AND NOT in_is_admin - INTO my_customer_id, out_legal_name_change + in_name IS NOT NULL AND name != in_name AND NOT in_is_admin, + in_max_debt IS NOT NULL AND max_debt != in_max_debt AND NOT in_is_admin + INTO my_customer_id, out_legal_name_change, out_debt_limit_change FROM customers + JOIN bank_accounts + ON customer_id=owning_customer_id WHERE login=in_login; IF NOT FOUND THEN out_not_found=TRUE; RETURN; -ELSIF out_legal_name_change THEN +ELSIF out_legal_name_change OR out_debt_limit_change THEN RETURN; END IF; --- optionally updating the Taler exchange flag -IF in_is_taler_exchange IS NOT NULL THEN - UPDATE bank_accounts - SET is_taler_exchange = in_is_taler_exchange - WHERE owning_customer_id = my_customer_id; - IF NOT FOUND THEN - out_not_found=TRUE; - RETURN; - END IF; -END IF; - - --- bank account patching worked, custom must as well --- since this runs in a DB transaction and the customer --- was found earlier in this function. -UPDATE customers -SET +-- Update bank info +UPDATE bank_accounts SET + is_taler_exchange = COALESCE(in_is_taler_exchange, is_taler_exchange), + max_debt = COALESCE(in_max_debt, max_debt) +WHERE owning_customer_id = my_customer_id; +-- Update customer info +UPDATE customers SET cashout_payto=in_cashout_payto, phone=in_phone, - email=in_email + email=in_email, + name = COALESCE(in_name, name) WHERE customer_id = my_customer_id; --- optionally updating the name -IF in_name IS NOT NULL THEN - UPDATE customers SET name=in_name WHERE customer_id = my_customer_id; -END IF; END $$; -COMMENT ON FUNCTION account_reconfig(TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN, BOOLEAN) +COMMENT ON FUNCTION account_reconfig IS 'Updates values on customer and bank account rows based on the input data.'; CREATE OR REPLACE FUNCTION customer_delete(