commit 5cc21948754598ab23519da826e47e6202696970 parent 0ac89fffacda84335bc1d7a26e3b7b15255767ff Author: Antoine A <> Date: Tue, 7 May 2024 12:55:24 +0900 bank: support per account cashout minimum amount Diffstat:
16 files changed, 505 insertions(+), 154 deletions(-)
diff --git a/API_CHANGES.md b/API_CHANGES.md @@ -35,7 +35,7 @@ This files contains all the API changes for the current release: - POST /accounts/USERNAME/cashouts: remove tan_channel field - POST /accounts/USERNAME/cashouts/CASHOUT_ID: remove confirmation_time, tan_channel, tan_info and status fields -- POST /accounts/$USERNAME/cashouts: remove status field +- POST /accounts/USERNAME/cashouts: remove status field - POST /cashouts: remove status field - PATCH /accounts/USERNAME: add tan_channel - GET /accounts/USERNAME: add tan_channel @@ -52,6 +52,10 @@ This files contains all the API changes for the current release: - GET /accounts/USERNAME: new status field - GET /monitor: new date_s params - GET /config: new base_url field for the advertised base URL +- POST /accounts: add min_cashout field for the custom minimum cashout amount +- PATCH /accounts/USERNAME: add min_cashout field for the custom minimum cashout amount +- GET /accounts: add min_cashout field for the custom minimum cashout amount +- GET /accounts/USERNAME: add min_cashout field for the custom minimum cashout amount ## bank cli diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -37,7 +37,7 @@ val RESERVED_ACCOUNTS = setOf("admin", "bank") const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 // API version -const val COREBANK_API_VERSION: String = "4:7:0" -const val CONVERSION_API_VERSION: String = "0:0:0" +const val COREBANK_API_VERSION: String = "4:8:0" +const val CONVERSION_API_VERSION: String = "0:1:0" const val INTEGRATION_API_VERSION: String = "2:0:2" const val REVENUE_API_VERSION: String = "0:0:0" \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -216,7 +216,14 @@ class EditAccount : CliktCommand( private val tan_channel: String? by option(help = "which channel TAN challenges should be sent to") private val cashout_payto_uri: IbanPayto? by option(help = "Payto URI of a fiant account who receive cashout amount").convert { Payto.parse(it).expectIban() } private val debit_threshold: TalerAmount? by option(help = "Max debit allowed for this account").convert { TalerAmount(it) } - + private val min_cashout: Option<TalerAmount>? by option(help = "Custom minimum cashout amount for this account").convert { + if (it == "") { + Option.None + } else { + Option.Some(TalerAmount(it)) + } + } + override fun run() = cliCmd(logger, common.log) { val cfg = talerConfig(common.config) val ctx = cfg.loadBankConfig() @@ -232,7 +239,12 @@ class EditAccount : CliktCommand( phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null), ), cashout_payto_uri = Option.Some(cashout_payto_uri), - debit_threshold = debit_threshold + debit_threshold = debit_threshold, + min_cashout = when (val tmp = min_cashout) { + null -> Option.None + is Option.None -> Option.Some(null) + is Option.Some -> Option.Some(tmp.value) + } ) when (patchAccount(db, ctx, req, username, true, true)) { AccountPatchResult.Success -> @@ -244,6 +256,7 @@ class EditAccount : CliktCommand( AccountPatchResult.NonAdminName, AccountPatchResult.NonAdminCashout, AccountPatchResult.NonAdminDebtLimit, + AccountPatchResult.NonAdminMinCashout, is AccountPatchResult.TanRequired -> { // Unreachable as we edit account as admin } @@ -282,6 +295,10 @@ class CreateAccountOption: OptionGroup() { val debit_threshold: TalerAmount? by option( help = "Max debit allowed for this account" ).convert { TalerAmount(it) } + val min_cashout: TalerAmount? by option( + help = "Custom minimum cashout amount for this account" + ).convert { TalerAmount(it) } + } class CreateAccount : CliktCommand( @@ -312,7 +329,8 @@ class CreateAccount : CliktCommand( ), cashout_payto_uri = cashout_payto_uri, payto_uri = payto_uri, - debit_threshold = debit_threshold + debit_threshold = debit_threshold, + min_cashout = min_cashout ) } req?.let { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -182,6 +182,7 @@ data class RegisterAccountRequest( val cashout_payto_uri: IbanPayto? = null, val payto_uri: Payto? = null, val debit_threshold: TalerAmount? = null, + val min_cashout: TalerAmount? = null, val tan_channel: TanChannel? = null, ) { init { @@ -209,6 +210,7 @@ data class AccountReconfiguration( val name: String? = null, val is_public: Boolean? = null, val debit_threshold: TalerAmount? = null, + val min_cashout: Option<TalerAmount?> = Option.None, val tan_channel: Option<TanChannel?> = Option.None, val is_taler_exchange: Boolean? = null, ) @@ -355,6 +357,7 @@ data class AccountMinimalData( val payto_uri: String, val balance: Balance, val debit_threshold: TalerAmount, + val min_cashout: TalerAmount? = null, val is_public: Boolean, val is_taler_exchange: Boolean, val row_id: Long, @@ -378,6 +381,7 @@ data class AccountData( val balance: Balance, val payto_uri: String, val debit_threshold: TalerAmount, + val min_cashout: TalerAmount? = null, val contact_data: ChallengeContactData? = null, val cashout_payto_uri: String? = null, val tan_channel: TanChannel? = null, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -160,6 +160,12 @@ suspend fun createAccount( "only admin account can choose the debit limit", TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT ) + + if (req.min_cashout != null) + throw conflict( + "only admin account can choose the minimum cashout amount", + TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT + ) if (req.tan_channel != null) throw conflict( @@ -188,6 +194,25 @@ suspend fun createAccount( TalerErrorCode.END ) + suspend fun doDb(internalPayto: Payto) = db.account.create( + login = req.username, + name = req.name, + email = req.contact_data?.email?.get(), + phone = req.contact_data?.phone?.get(), + cashoutPayto = req.cashout_payto_uri, + password = req.password, + internalPayto = internalPayto, + isPublic = req.is_public, + isTalerExchange = req.is_taler_exchange, + maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit, + bonus = if (!req.is_taler_exchange) cfg.registrationBonus + else TalerAmount(0, 0, cfg.regionalCurrency), + tanChannel = req.tan_channel, + checkPaytoIdempotent = req.payto_uri != null, + ctx = cfg.payto, + minCashout = req.min_cashout + ) + when (cfg.wireMethod) { WireMethod.IBAN -> { if (req.payto_uri != null && !(req.payto_uri is IbanPayto)) @@ -196,23 +221,7 @@ suspend fun createAccount( while (true) { val internalPayto = req.payto_uri ?: IbanPayto.rand() as Payto - val res = db.account.create( - login = req.username, - name = req.name, - email = req.contact_data?.email?.get(), - phone = req.contact_data?.phone?.get(), - cashoutPayto = req.cashout_payto_uri, - password = req.password, - internalPayto = internalPayto, - isPublic = req.is_public, - isTalerExchange = req.is_taler_exchange, - maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit, - bonus = if (!req.is_taler_exchange) cfg.registrationBonus - else TalerAmount(0, 0, cfg.regionalCurrency), - tanChannel = req.tan_channel, - checkPaytoIdempotent = req.payto_uri != null, - ctx = cfg.payto - ) + val res = doDb(internalPayto) // Retry with new IBAN if (res == AccountCreationResult.PayToReuse && retry > 0) { retry-- @@ -230,24 +239,7 @@ suspend fun createAccount( } val internalPayto = XTalerBankPayto.forUsername(req.username) - - return db.account.create( - login = req.username, - name = req.name, - email = req.contact_data?.email?.get(), - phone = req.contact_data?.phone?.get(), - cashoutPayto = req.cashout_payto_uri, - password = req.password, - internalPayto = internalPayto, - isPublic = req.is_public, - isTalerExchange = req.is_taler_exchange, - maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit, - bonus = if (!req.is_taler_exchange) cfg.registrationBonus - else TalerAmount(0, 0, cfg.regionalCurrency), - tanChannel = req.tan_channel, - checkPaytoIdempotent = req.payto_uri != null, - ctx = cfg.payto - ) + return doDb(internalPayto) } } } @@ -283,6 +275,7 @@ suspend fun patchAccount( tan_channel = req.tan_channel, isPublic = req.is_public, debtLimit = req.debit_threshold, + minCashout = req.min_cashout, isAdmin = isAdmin, is2fa = is2fa, faChannel = channel, @@ -367,6 +360,10 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { "non-admin user cannot change their debt limit", TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT ) + AccountPatchResult.NonAdminMinCashout -> throw conflict( + "non-admin user cannot change their min cashout amount", + TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT + ) AccountPatchResult.MissingTanInfo -> throw conflict( "missing info for tan channel ${req.tan_channel.get()}", TalerErrorCode.BANK_MISSING_TAN_INFO @@ -595,6 +592,10 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio "Wrong currency conversion", TalerErrorCode.BANK_BAD_CONVERSION ) + CashoutCreationResult.UnderMin -> throw conflict( + "Amount of currency conversion it less than the minimum allowed", + TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL + ) CashoutCreationResult.AccountIsExchange -> throw conflict( "Exchange account cannot perform cashout operation", TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -47,6 +47,7 @@ class AccountDAO(private val db: Database) { isPublic: Boolean, isTalerExchange: Boolean, maxDebt: TalerAmount, + minCashout: TalerAmount?, bonus: TalerAmount, tanChannel: TanChannel?, // Whether to check [internalPaytoUri] for idempotency @@ -223,6 +224,7 @@ class AccountDAO(private val db: Database) { data object NonAdminName: AccountPatchResult data object NonAdminCashout: AccountPatchResult data object NonAdminDebtLimit: AccountPatchResult + data object NonAdminMinCashout: AccountPatchResult data object MissingTanInfo: AccountPatchResult data class TanRequired(val channel: TanChannel?, val info: String?): AccountPatchResult data object Success: AccountPatchResult @@ -238,6 +240,7 @@ class AccountDAO(private val db: Database) { tan_channel: Option<TanChannel?>, isPublic: Boolean?, debtLimit: TalerAmount?, + minCashout: Option<TalerAmount?>, isAdmin: Boolean, is2fa: Boolean, faChannel: TanChannel?, @@ -248,6 +251,7 @@ class AccountDAO(private val db: Database) { val checkName = !isAdmin && !allowEditName && name != null val checkCashout = !isAdmin && !allowEditCashout && cashoutPayto.isSome() val checkDebtLimit = !isAdmin && debtLimit != null + val checkMinCashout = !isAdmin && minCashout.isSome() data class CurrentAccount( val id: Long, @@ -257,6 +261,7 @@ class AccountDAO(private val db: Database) { val name: String, val cashoutPayTo: String?, val debtLimit: TalerAmount, + val minCashout: TalerAmount? ) // Get user ID and current data @@ -265,6 +270,8 @@ class AccountDAO(private val db: Database) { customer_id, tan_channel, phone, email, name, cashout_payto ,(max_debt).val AS max_debt_val ,(max_debt).frac AS max_debt_frac + ,(min_cashout).val AS min_cashout_val + ,(min_cashout).frac AS min_cashout_frac FROM customers JOIN bank_accounts ON customer_id=owning_customer_id @@ -280,6 +287,7 @@ class AccountDAO(private val db: Database) { name = it.getString("name"), cashoutPayTo = it.getString("cashout_payto"), debtLimit = it.getAmount("max_debt", db.bankCurrency), + minCashout = it.getOptAmount("min_cashout", db.bankCurrency), ) } ?: return@transaction AccountPatchResult.UnknownAccount } @@ -310,10 +318,11 @@ class AccountDAO(private val db: Database) { return@transaction AccountPatchResult.NonAdminCashout if (checkDebtLimit && debtLimit != curr.debtLimit) return@transaction AccountPatchResult.NonAdminDebtLimit + if (checkMinCashout && minCashout.get() != curr.minCashout) + return@transaction AccountPatchResult.NonAdminMinCashout if (patchChannel != null && newInfo == null) return@transaction AccountPatchResult.MissingTanInfo - // Tan channel verification if (!isAdmin) { // Check performed 2fa check @@ -340,11 +349,24 @@ class AccountDAO(private val db: Database) { sequence { if (isPublic != null) yield("is_public=?") if (debtLimit != null) yield("max_debt=(?, ?)::taler_amount") + minCashout.some { + if (it != null) { + yield("min_cashout=(?, ?)::taler_amount") + } else { + yield("min_cashout=null") + } + } }, "WHERE owning_customer_id = ?", sequence { isPublic?.let { yield(it) } debtLimit?.let { yield(it.value); yield(it.frac) } + minCashout.some { + if (it != null) { + yield(it.value) + yield(it.frac) + } + } yield(curr.id) } ) @@ -464,6 +486,8 @@ class AccountDAO(private val db: Database) { ,has_debt ,(max_debt).val AS max_debt_val ,(max_debt).frac AS max_debt_frac + ,(min_cashout).val AS min_cashout_val + ,(min_cashout).frac AS min_cashout_frac ,is_public ,is_taler_exchange ,CASE @@ -496,6 +520,7 @@ class AccountDAO(private val db: Database) { } ), debit_threshold = it.getAmount("max_debt", db.bankCurrency), + min_cashout = it.getOptAmount("min_cashout", db.bankCurrency), is_public = it.getBoolean("is_public"), is_taler_exchange = it.getBoolean("is_taler_exchange"), status = AccountStatus.valueOf(it.getString("status")) @@ -557,6 +582,8 @@ class AccountDAO(private val db: Database) { has_debt AS balance_has_debt, (max_debt).val as max_debt_val, (max_debt).frac as max_debt_frac + ,(min_cashout).val AS min_cashout_val + ,(min_cashout).frac AS min_cashout_frac ,is_public ,is_taler_exchange ,internal_payto_uri @@ -587,6 +614,7 @@ class AccountDAO(private val db: Database) { } ), debit_threshold = it.getAmount("max_debt", db.bankCurrency), + min_cashout = it.getOptAmount("min_cashout", db.bankCurrency), is_public = it.getBoolean("is_public"), is_taler_exchange = it.getBoolean("is_taler_exchange"), payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx), diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -29,6 +29,7 @@ class CashoutDAO(private val db: Database) { /** Result of cashout operation creation */ sealed interface CashoutCreationResult { data class Success(val id: Long): CashoutCreationResult + data object UnderMin: CashoutCreationResult data object BadConversion: CashoutCreationResult data object AccountNotFound: CashoutCreationResult data object AccountIsExchange: CashoutCreationResult @@ -57,7 +58,8 @@ class CashoutDAO(private val db: Database) { out_request_uid_reuse, out_no_cashout_payto, out_tan_required, - out_cashout_id + out_cashout_id, + out_under_min FROM cashout_create(?,?,(?,?)::taler_amount,(?,?)::taler_amount,?,?,?) """) stmt.setString(1, login) @@ -73,6 +75,7 @@ class CashoutDAO(private val db: Database) { when { !it.next() -> throw internalServerError("No result from DB procedure cashout_create") + it.getBoolean("out_under_min") -> CashoutCreationResult.UnderMin it.getBoolean("out_bad_conversion") -> CashoutCreationResult.BadConversion it.getBoolean("out_account_not_found") -> CashoutCreationResult.AccountNotFound it.getBoolean("out_account_is_exchange") -> CashoutCreationResult.AccountIsExchange diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -109,7 +109,7 @@ class ConversionDAO(private val db: Database) { /** Perform [direction] conversion of [amount] using in-db [function] */ private suspend fun conversion(amount: TalerAmount, direction: String, function: String): ConversionResult = db.conn { conn -> - val stmt = conn.prepareStatement("SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?)") + val stmt = conn.prepareStatement("SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?, (0, 0)::taler_amount)") stmt.setLong(1, amount.value) stmt.setInt(2, amount.frac) stmt.setString(3, direction) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -128,6 +128,7 @@ suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null phone = null, cashoutPayto = null, tanChannel = null, + minCashout = null, ctx = cfg.payto ) } diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt @@ -45,7 +45,7 @@ class ConversionApiTest { } // Too small - client.get("/conversion-info/cashout-rate?amount_debit=KUDOS:0.08") + client.get("/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008") .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) // No amount client.get("/conversion-info/cashout-rate") diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. + * Copyright (C) 2023-2024 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -226,11 +226,27 @@ class CoreBankAccountsApiTest { }.assertOk() } - // Check admin only tan_channel + // Check admin only min_cashout obj { "username" to "bat2" "password" to "password" "name" to "Bat" + "min_cashout" to "KUDOS:42" + }.let { req -> + client.post("/accounts") { + json(req) + }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT) + client.post("/accounts") { + json(req) + pwAuth("admin") + }.assertOk() + } + + // Check admin only tan_channel + obj { + "username" to "bat3" + "password" to "password" + "name" to "Bat" "contact_data" to obj { "phone" to "+456" } @@ -459,8 +475,6 @@ class CoreBankAccountsApiTest { }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } - - @Test fun softDelete() = bankSetup { db -> // Create all kind of operations @@ -600,6 +614,10 @@ class CoreBankAccountsApiTest { obj(req) { "debit_threshold" to "KUDOS:100" }, TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT ) + checkAdminOnly( + obj(req) { "min_cashout" to "KUDOS:100" }, + TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT + ) // Check currency client.patch("/accounts/merchant") { @@ -1319,6 +1337,36 @@ class CoreBankCashoutApiTest { } }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) + // Check min amount + client.postA("/accounts/customer/cashouts") { + json(req) { + "request_uid" to ShortHashCode.rand() + "amount_debit" to "KUDOS:0.09" + "amount_credit" to convert("KUDOS:0.09") + } + }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) + + // Check custom min account + client.patch("/accounts/customer") { + pwAuth("admin") + json { + "min_cashout" to "KUDOS:10" + } + }.assertNoContent() + client.postA("/accounts/customer/cashouts") { + json(req) { + "request_uid" to ShortHashCode.rand() + "amount_debit" to "KUDOS:5" + "amount_credit" to convert("KUDOS:5") + } + }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) + client.patch("/accounts/customer") { + pwAuth("admin") + json { + "min_cashout" to (null as String?) + } + }.assertNoContent() + // Check wrong currency client.postA("/accounts/customer/cashouts") { json(req) { diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -96,6 +96,7 @@ fun bankSetup( phone = null, cashoutPayto = null, tanChannel = null, + minCashout = null, ctx = cfg.payto )) assertIs<AccountCreationResult.Success>(db.account.create( @@ -112,6 +113,7 @@ fun bankSetup( phone = null, cashoutPayto = null, tanChannel = null, + minCashout = null, ctx = cfg.payto )) assertIs<AccountCreationResult.Success>(db.account.create( @@ -128,6 +130,7 @@ fun bankSetup( phone = null, cashoutPayto = null, tanChannel = null, + minCashout = null, ctx = cfg.payto )) // Create admin account diff --git a/common/src/main/kotlin/TalerErrorCode.kt b/common/src/main/kotlin/TalerErrorCode.kt @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - Copyright (C) 2012-2020 Taler Systems SA + Copyright (C) 2012-2024 Taler Systems SA GNU Taler is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published @@ -34,7 +34,7 @@ enum class TalerErrorCode(val code: Int) { /** - * A non-integer error code was returned in the JSON response. + * An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -42,7 +42,7 @@ enum class TalerErrorCode(val code: Int) { /** - * An internal failure happened on the client side. + * An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -50,7 +50,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The response we got from the server was not even in JSON format. + * The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -58,7 +58,7 @@ enum class TalerErrorCode(val code: Int) { /** - * An operation timed out. + * The operation timed out. Trying again might help. Check the network connection. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -66,7 +66,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The version string given does not follow the expected CURRENT:REVISION:AGE Format. + * The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -74,7 +74,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes. + * The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -82,7 +82,7 @@ enum class TalerErrorCode(val code: Int) { /** - * There is an error in the client-side configuration, for example the base URL specified is malformed. + * There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -90,7 +90,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The client made a request to a service, but received an error response it does not know how to handle. + * The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ @@ -98,7 +98,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The token used by the client to authorize the request does not grant the required permissions for the request. + * The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed. * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). * (A value of 0 indicates that the error is generated client-side). */ @@ -106,7 +106,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP method used is invalid for this endpoint. + * The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405). * (A value of 0 indicates that the error is generated client-side). */ @@ -114,7 +114,7 @@ enum class TalerErrorCode(val code: Int) { /** - * There is no endpoint defined for the URL provided by the client. + * There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). */ @@ -122,7 +122,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The JSON in the client's request was malformed (generic parse error). + * The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -130,7 +130,7 @@ enum class TalerErrorCode(val code: Int) { /** - * Some of the HTTP headers provided by the client caused the server to not be able to handle the request. + * Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -138,7 +138,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The payto:// URI provided by the client is malformed. + * The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -146,7 +146,7 @@ enum class TalerErrorCode(val code: Int) { /** - * A required parameter in the request was missing. + * A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -154,7 +154,7 @@ enum class TalerErrorCode(val code: Int) { /** - * A parameter in the request was malformed. + * A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -162,7 +162,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The reserve public key given as part of a /reserves/ endpoint was malformed. + * The reserve public key was malformed. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -170,7 +170,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The body in the request could not be decompressed by the server. + * The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -178,7 +178,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The currency involved in the operation is not acceptable for this backend. + * The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -186,7 +186,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The URI is longer than the longest URI the HTTP server is willing to parse. + * The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit. * Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414). * (A value of 0 indicates that the error is generated client-side). */ @@ -194,7 +194,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The body is too large to be permissible for the endpoint. + * The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit. * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). * (A value of 0 indicates that the error is generated client-side). */ @@ -242,7 +242,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service failed initialize its connection to the database. + * The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -250,7 +250,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service encountered an error event to just start the database transaction. + * The service encountered an error event to just start the database transaction. The system administrator should check that the database is running. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -258,7 +258,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service failed to store information in its database. + * The service failed to store information in its database. The system administrator should check that the database is running and review the service logs. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -266,7 +266,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service failed to fetch information from its database. + * The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -274,7 +274,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service encountered an error event to commit the database transaction (hard, unrecoverable error). + * The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -282,7 +282,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.) + * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -290,7 +290,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service's database is inconsistent and violates service-internal invariants. + * The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -298,7 +298,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP server experienced an internal invariant failure (bug). + * The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -306,7 +306,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service could not compute a cryptographic hash over some JSON value. + * The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -314,7 +314,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The service could not compute an amount. + * The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -322,7 +322,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP server had insufficient memory to parse the request. + * The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -330,7 +330,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP server failed to allocate memory. + * The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -338,7 +338,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP server failed to allocate memory for building JSON reply. + * The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -346,7 +346,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The HTTP server failed to allocate memory for making a CURL request. + * The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -354,7 +354,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The backend could not locate a required template to generate an HTML reply. + * The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -362,7 +362,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The backend could not expand the template to generate an HTML reply. + * The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ @@ -2347,7 +2347,7 @@ enum class TalerErrorCode(val code: Int) { /** * After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover. - * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES(2155), @@ -2355,7 +2355,7 @@ enum class TalerErrorCode(val code: Int) { /** * Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. - * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT(2156), @@ -2506,6 +2506,62 @@ enum class TalerErrorCode(val code: Int) { /** + * The payment requires the wallet to select a choice from the choices array and pass it in the 'choice_index' field of the request. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING(2176), + + + /** + * The 'choice_index' field is invalid. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS(2177), + + + /** + * The provided 'tokens' array does not match with the required input tokens of the order. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_INPUT_TOKENS_MISMATCH(2178), + + + /** + * Invalid token issue signature (blindly signed by merchant) for provided token. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID(2179), + + + /** + * Invalid token use signature (EdDSA, signed by wallet) for provided token. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID(2180), + + + /** + * The provided number of tokens does not match the required number. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH(2181), + + + /** + * The provided number of token envelopes does not match the specified number. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH(2182), + + + /** * The contract hash does not match the given order ID. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -2522,6 +2578,22 @@ enum class TalerErrorCode(val code: Int) { /** + * A token family with this ID but conflicting data exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_TOKEN_FAMILY_CONFLICT(2225), + + + /** + * The backend is unaware of a token family with the given ID. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND(2226), + + + /** * The merchant failed to send the exchange the refund request. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). @@ -2570,6 +2642,62 @@ enum class TalerErrorCode(val code: Int) { /** + * We are waiting for the exchange to provide us with key material before checking the wire transfer. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS(2258), + + + /** + * We are waiting for the exchange to provide us with the list of aggregated transactions. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST(2259), + + + /** + * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE(2260), + + + /** + * The exchange indicated in the wire transfer claims to know nothing about the wire transfer. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND(2261), + + + /** + * The interaction with the exchange is delayed due to rate limiting. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED(2262), + + + /** + * We experienced a transient failure in our interaction with the exchange. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE(2263), + + + /** + * The response from the exchange was unacceptable and should be reviewed with an auditor. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE(2264), + + + /** * We could not claim the order because the backend is unaware of it. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -2706,7 +2834,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The order ceration request is invalid because the given payment deadline is in the past. + * The order creation request is invalid because the given payment deadline is in the past. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -2786,6 +2914,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The token family slug provided in this order could not be found in the merchant database. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN(2533), + + + /** * The exchange says it does not know this transfer. * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). * (A value of 0 indicates that the error is generated client-side). @@ -2850,62 +2986,6 @@ enum class TalerErrorCode(val code: Int) { /** - * We are waiting for the exchange to provide us with key material before checking the wire transfer. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS(2258), - - - /** - * We are waiting for the exchange to provide us with the list of aggregated transactions. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST(2259), - - - /** - * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE(2260), - - - /** - * The exchange indicated in the wire transfer claims to know nothing about the wire transfer. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND(2261), - - - /** - * The interaction with the exchange is delayed due to rate limiting. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED(2262), - - - /** - * We experienced a transient failure in our interaction with the exchange. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE(2263), - - - /** - * The response from the exchange was unacceptable and should be reviewed with an auditor. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE(2264), - - - /** * The amount transferred differs between what was submitted and what the exchange claimed. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). @@ -3170,6 +3250,22 @@ enum class TalerErrorCode(val code: Int) { /** + * The requested resource could not be found. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + AUDITOR_RESOURCE_NOT_FOUND(3102), + + + /** + * The URI is missing a path component. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + AUDITOR_URI_MISSING_PATH_COMPONENT(3103), + + + /** * Wire transfer attempted with credit and debit party being the same bank account. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -3522,6 +3618,22 @@ enum class TalerErrorCode(val code: Int) { /** + * A non-admin user has tried to set their minimum cashout amount. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_SET_MIN_CASHOUT(5146), + + + /** + * Amount of currency conversion it less than the minimum allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_CONVERSION_AMOUNT_TO_SMALL(5147), + + + /** * The sync service failed find the account in its database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -3898,6 +4010,46 @@ enum class TalerErrorCode(val code: Int) { /** + * An exchange that is required for some request is currently not available. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_UNAVAILABLE(7032), + + + /** + * An exchange entry is still used by the exchange, thus it can't be deleted without purging. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_ENTRY_USED(7033), + + + /** + * The wallet database is unavailable and the wallet thus is not operational. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_DB_UNAVAILABLE(7034), + + + /** + * A taler:// URI is malformed and can't be parsed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_TALER_URI_MALFORMED(7035), + + + /** + * A wallet-core request was cancelled and thus can't provide a response. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CORE_REQUEST_CANCELLED(7036), + + + /** * We encountered a timeout with our payment backend. * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). * (A value of 0 indicates that the error is generated client-side). @@ -4426,6 +4578,38 @@ enum class TalerErrorCode(val code: Int) { /** + * The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request. + * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). + * (A value of 0 indicates that the error is generated client-side). + */ + DONAU_GENERIC_KEYS_MISSING(8607), + + + /** + * The signature of the charity key is not valid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + DONAU_CHARITY_SIGNATURE_INVALID(8608), + + + /** + * The charity is unknown. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + DONAU_CHARITY_NOT_FOUND(8609), + + + /** + * The donation amount specified in the request exceeds the limit of the charity. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + DONAU_EXCEEDING_DONATION_LIMIT(8610), + + + /** * A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). diff --git a/common/src/main/kotlin/db/types.kt b/common/src/main/kotlin/db/types.kt @@ -33,6 +33,12 @@ fun ResultSet.getAmount(name: String, currency: String): TalerAmount { ) } +fun ResultSet.getOptAmount(name: String, currency: String): TalerAmount? { + val amount = getAmount(name, currency) + if (wasNull()) return null + return amount +} + fun ResultSet.getDecimal(name: String): DecimalNumber { return DecimalNumber( getLong("${name}_val"), diff --git a/database-versioning/libeufin-bank-0004.sql b/database-versioning/libeufin-bank-0004.sql @@ -0,0 +1,25 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +BEGIN; + +SELECT _v.register_patch('libeufin-bank-0004', NULL, NULL); +SET search_path TO libeufin_bank; + +ALTER TABLE bank_accounts ADD min_cashout taler_amount; +COMMENT ON COLUMN bank_accounts.min_cashout + IS 'Custom minimum cashout amount for this account'; + +COMMIT; diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -984,7 +984,7 @@ SELECT bank_account_id -- Perform conversion SELECT (converted).val, (converted).frac, too_small, no_config INTO converted_amount.val, converted_amount.frac, out_too_small, out_no_config - FROM conversion_to(in_amount, 'cashin'::text); + FROM conversion_to(in_amount, 'cashin'::text, null); IF out_too_small OR out_no_config THEN RETURN; END IF; @@ -1036,6 +1036,7 @@ CREATE FUNCTION cashout_create( OUT out_request_uid_reuse BOOLEAN, OUT out_no_cashout_payto BOOLEAN, OUT out_tan_required BOOLEAN, + OUT out_under_min BOOLEAN, -- Success return OUT out_cashout_id INT8 ) @@ -1044,17 +1045,18 @@ DECLARE account_id INT8; admin_account_id INT8; tx_id INT8; +custom_min_cashout taler_amount; BEGIN --- check conversion -SELECT too_small OR no_config OR in_amount_credit!=converted INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'cashout'::text); -IF out_bad_conversion THEN - RETURN; -END IF; -- Check account exists, has all info and if 2FA is required SELECT - bank_account_id, is_taler_exchange, cashout_payto IS NULL, (NOT in_is_tan AND tan_channel IS NOT NULL) - INTO account_id, out_account_is_exchange, out_no_cashout_payto, out_tan_required + bank_account_id, is_taler_exchange, + (min_cashout).val, (min_cashout).frac, + cashout_payto IS NULL, (NOT in_is_tan AND tan_channel IS NOT NULL) + INTO + account_id, out_account_is_exchange, + custom_min_cashout.val, custom_min_cashout.frac, + out_no_cashout_payto, out_tan_required FROM bank_accounts JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id WHERE login=in_login; @@ -1065,6 +1067,17 @@ ELSIF out_account_is_exchange OR out_no_cashout_payto THEN RETURN; END IF; +-- check conversion TODO use custom min +IF custom_min_cashout.val IS NULL THEN + custom_min_cashout = NULL; +END IF; +SELECT under_min, too_small OR no_config OR in_amount_credit!=converted + INTO out_under_min, out_bad_conversion + FROM conversion_to(in_amount_debit, 'cashout'::text, custom_min_cashout); +IF out_bad_conversion THEN + RETURN; +END IF; + -- Retrieve admin account id SELECT bank_account_id INTO admin_account_id @@ -1487,8 +1500,10 @@ COMMENT ON FUNCTION conversion_revert_ratio CREATE FUNCTION conversion_to( IN amount taler_amount, IN direction TEXT, + IN custom_min_amount taler_amount, OUT converted taler_amount, OUT too_small BOOLEAN, + OUT under_min BOOLEAN, OUT no_config BOOLEAN ) LANGUAGE plpgsql AS $$ @@ -1505,8 +1520,13 @@ BEGIN no_config = true; RETURN; END IF; + IF custom_min_amount IS NOT NULL THEN + min_amount = custom_min_amount; + END IF; + SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount); IF too_small THEN + under_min = true; converted = (0, 0); RETURN; END IF; @@ -1528,8 +1548,10 @@ END $$; CREATE FUNCTION conversion_from( IN amount taler_amount, IN direction TEXT, + IN custom_min_amount taler_amount, OUT converted taler_amount, OUT too_small BOOLEAN, + OUT under_min BOOLEAN, OUT no_config BOOLEAN ) LANGUAGE plpgsql AS $$ @@ -1554,8 +1576,12 @@ BEGIN -- Check min amount SELECT value['val']::int8, value['frac']::int4 INTO min_amount.val, min_amount.frac FROM config WHERE key=direction||'_min_amount'; + IF custom_min_amount IS NOT NULL THEN + min_amount = custom_min_amount; + END IF; SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount); IF too_small THEN + under_min = true; converted = (0, 0); END IF; END $$;