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:
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(