diff options
Diffstat (limited to 'bank/src/main/kotlin')
12 files changed, 122 insertions, 192 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt index 9cdfcf0f..17fef641 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -68,11 +68,6 @@ data class ConversionRate ( val cashout_min_amount: TalerAmount, ) -sealed interface ServerConfig { - data class Unix(val path: String, val mode: Int): ServerConfig - data class Tcp(val addr: String, val port: Int): ServerConfig -} - fun talerConfig(configPath: Path?): TalerConfig = BANK_CONFIG_SOURCE.fromFile(configPath) fun TalerConfig.loadDbConfig(): DatabaseConfig { @@ -82,14 +77,6 @@ fun TalerConfig.loadDbConfig(): DatabaseConfig { ) } -fun TalerConfig.loadServerConfig(): ServerConfig { - return when (val method = requireString("libeufin-bank", "serve")) { - "tcp" -> ServerConfig.Tcp(lookupString("libeufin-bank", "address") ?: requireString("libeufin-bank", "bind_to"), requireNumber("libeufin-bank", "port")) - "unix" -> ServerConfig.Unix(requireString("libeufin-bank", "unixpath"), requireNumber("libeufin-bank", "unixpath_mode")) - else -> throw TalerConfigError.invalid("server method", "libeufin-bank", "serve", "expected 'tcp' or 'unix' got '$method'") - } -} - fun TalerConfig.loadBankConfig(): BankConfig { val regionalCurrency = requireString("libeufin-bank", "currency") var fiatCurrency: String? = null diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt index d91d779d..209ea700 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -37,7 +37,6 @@ 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 index 8733b907..1f3ebbc8 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -62,8 +62,7 @@ import kotlin.io.path.exists import kotlin.io.path.readText private val logger: Logger = LoggerFactory.getLogger("libeufin-bank") -// Dirty local variable to stop the server in test TODO remove this ugly hack -var engine: ApplicationEngine? = null + /** @@ -117,7 +116,7 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") val cfg = talerConfig(common.config) val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() - val serverCfg = cfg.loadServerConfig() + val serverCfg = cfg.loadServerConfig("libeufin-bank") Database(dbCfg, ctx.regionalCurrency, ctx.fiatCurrency).use { db -> if (ctx.allowConversion) { logger.info("Ensure exchange account exists") @@ -142,25 +141,9 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") db.conn { it.execSQLUpdate(sqlProcedures.readText()) } // Remove conversion info from the database ? } - - val env = applicationEngineEnvironment { - when (serverCfg) { - is ServerConfig.Tcp -> { - for (addr in InetAddress.getAllByName(serverCfg.addr)) { - connector { - port = serverCfg.port - host = addr.hostAddress - } - } - } - is ServerConfig.Unix -> - throw Exception("Can only serve libeufin-bank via TCP") - } - module { corebankWebApp(db, ctx) } + serve(serverCfg) { + corebankWebApp(db, ctx) } - val local = embeddedServer(Netty, env) - engine = local - local.start(wait = true) } } } @@ -216,7 +199,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 +222,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 +239,7 @@ class EditAccount : CliktCommand( AccountPatchResult.NonAdminName, AccountPatchResult.NonAdminCashout, AccountPatchResult.NonAdminDebtLimit, + AccountPatchResult.NonAdminMinCashout, is AccountPatchResult.TanRequired -> { // Unreachable as we edit account as admin } @@ -282,6 +278,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 +312,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/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt index 5966cba9..57c3fda8 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt @@ -37,65 +37,6 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit -@Serializable(with = DecimalNumber.Serializer::class) -class DecimalNumber { - val value: Long - val frac: Int - - constructor(value: Long, frac: Int) { - this.value = value - this.frac = frac - } - constructor(encoded: String) { - val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format") - val (value, frac) = match.destructured - this.value = value.toLongOrNull() ?: throw badRequest("Invalid value") - if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value specified in decimal number is too large") - this.frac = if (frac.isEmpty()) { - 0 - } else { - var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value") - repeat(8 - frac.length) { - tmp *= 10 - } - tmp - } - } - - override fun equals(other: Any?): Boolean { - return other is DecimalNumber && - other.value == this.value && - other.frac == this.frac - } - - override fun toString(): String { - if (frac == 0) { - return "$value" - } else { - return "$value.${frac.toString().padStart(8, '0')}" - .dropLastWhile { it == '0' } // Trim useless fractional trailing 0 - } - } - - internal object Serializer : KSerializer<DecimalNumber> { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: DecimalNumber) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): DecimalNumber { - return DecimalNumber(decoder.decodeString()) - } - } - - companion object { - private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?") - } -} - - /** * Internal representation of relative times. The * "forever" case is represented with Long.MAX_VALUE. diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt index 2527bae6..cca56e57 100644 --- 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, ) @@ -327,14 +329,6 @@ data class TalerIntegrationConfigResponse( val version: String = INTEGRATION_API_VERSION } -@Serializable -data class RevenueConfig( - val currency: String -) { - val name: String = "taler-revenue" - val version: String = REVENUE_API_VERSION -} - enum class CreditDebitInfo { credit, debit } @@ -355,6 +349,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 +373,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, @@ -546,21 +542,6 @@ data class ConversionResponse( val amount_credit: TalerAmount, ) -@Serializable -data class RevenueIncomingHistory( - val incoming_transactions : List<RevenueIncomingBankTransaction>, - val credit_account: String -) - -@Serializable -data class RevenueIncomingBankTransaction( - val row_id: Long, - val date: TalerProtocolTimestamp, - val amount: TalerAmount, - val debit_account: String, - val subject: String -) - /** * Response to GET /public-accounts */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt index c659b0f0..d57c504d 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -126,7 +126,7 @@ private fun Routing.coreBankTokenApi(db: Database) { } call.respond( TokenSuccessResponse( - access_token = token.encoded(), + access_token = "$TOKEN_PREFIX$token", expiration = TalerProtocolTimestamp(t_s = expirationTimestamp) ) ) @@ -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,31 +194,33 @@ 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)) - throw badRequest("Expected an IBAN payto uri") + req.payto_uri?.expectRequestIban() var retry = if (req.payto_uri == null) IBAN_ALLOCATION_RETRY_COUNTER else 0 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-- @@ -223,31 +231,13 @@ suspend fun createAccount( } WireMethod.X_TALER_BANK -> { if (req.payto_uri != null) { - if (!(req.payto_uri is XTalerBankPayto)) - throw badRequest("Expected an IBAN payto uri") - else if (req.payto_uri.username != req.username) - throw badRequest("Expected a payto uri for '${req.username}' got one for '${req.payto_uri.username}'") + val payto = req.payto_uri.expectRequestXTalerBank() + if (payto.username != req.username) + throw badRequest("Expected a payto uri for '${req.username}' got one for '${payto.username}'") } 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 +273,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 +358,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 +590,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/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt index c0ed1100..56bdba2d 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -28,6 +28,7 @@ import io.ktor.util.pipeline.* import tech.libeufin.bank.* import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.api.* import tech.libeufin.common.crypto.PwCrypto import java.time.Instant @@ -37,6 +38,8 @@ private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin") /** Used to store used auth token */ private val AUTH_TOKEN = AttributeKey<ByteArray>("auth_token") +const val TOKEN_PREFIX = "secret-token:" + /** Get username of the request account */ val ApplicationCall.username: String get() = parameters.expect("USERNAME") /** Get username of the request account */ @@ -155,7 +158,7 @@ private suspend fun ApplicationCall.doTokenAuth( bearer: String, requiredScope: TokenScope, ): String { - if (!bearer.startsWith("secret-token:")) throw badRequest( + if (!bearer.startsWith(TOKEN_PREFIX)) throw badRequest( "Bearer token malformed", TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt index 036ec116..97ca874b 100644 --- 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 @@ -70,7 +71,7 @@ class AccountDAO(private val db: Database) { ON customer_id=owning_customer_id WHERE login=? """).run { - // TODO check max debt + // TODO check max debt and min checkout ? setString(1, name) setString(2, email) setString(3, phone) @@ -141,7 +142,8 @@ class AccountDAO(private val db: Database) { ,is_public ,is_taler_exchange ,max_debt - ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount) + ,min_cashout + ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount, ${if (minCashout == null) "NULL" else "(?, ?)::taler_amount"}) """).run { setString(1, internalPayto.canonical) setLong(2, customerId) @@ -149,6 +151,10 @@ class AccountDAO(private val db: Database) { setBoolean(4, isTalerExchange) setLong(5, maxDebt.value) setInt(6, maxDebt.frac) + if (minCashout != null) { + setLong(7, minCashout.value) + setInt(8, minCashout.frac) + } if (!executeUpdateViolation()) { conn.rollback() return@transaction AccountCreationResult.PayToReuse @@ -223,6 +229,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 +245,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 +256,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 +266,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 +275,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 +292,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 +323,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 +354,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 +491,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 +525,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 +587,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 +619,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 index 799d466a..d8779613 100644 --- 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 index 04317b68..8afd4f7f 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -20,7 +20,6 @@ package tech.libeufin.bank.db import tech.libeufin.bank.ConversionRate -import tech.libeufin.bank.DecimalNumber import tech.libeufin.bank.RoundingMode import tech.libeufin.common.* import tech.libeufin.common.db.* @@ -110,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/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt index 9efb821a..1236b1ad 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -96,13 +96,13 @@ class Database(dbConfig: DatabaseConfig, internal val bankCurrency: String, inte /** Listen for new bank transactions for [account] */ suspend fun <R> listenBank(account: Long, lambda: suspend (Flow<Long>) -> R): R = listen(bankTxFlows, account, lambda) - /** Listen for new taler outgoing transactions from [account] */ + /** Listen for new taler outgoing transactions from [exchange] */ suspend fun <R> listenOutgoing(exchange: Long, lambda: suspend (Flow<Long>) -> R): R = listen(outgoingTxFlows, exchange, lambda) - /** Listen for new taler incoming transactions to [account] */ + /** Listen for new taler incoming transactions to [exchange] */ suspend fun <R> listenIncoming(exchange: Long, lambda: suspend (Flow<Long>) -> R): R = listen(incomingTxFlows, exchange, lambda) - /** Listen for new taler outgoing transactions to [account] */ + /** Listen for new incoming transactions to [merchant] */ suspend fun <R> listenRevenue(merchant: Long, lambda: suspend (Flow<Long>) -> R): R = listen(revenueTxFlows, merchant, lambda) /** Listen for new withdrawal confirmations */ @@ -163,8 +163,4 @@ enum class AbortResult { Success, UnknownOperation, AlreadyConfirmed -} - -fun ResultSet.getTalerTimestamp(name: String): TalerProtocolTimestamp{ - return TalerProtocolTimestamp(getLong(name).asInstant()) }
\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt index 094b7996..8c7bd21b 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -37,6 +37,7 @@ import tech.libeufin.bank.auth.username import tech.libeufin.bank.db.AccountDAO.AccountCreationResult import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.api.* import java.util.* fun ApplicationCall.uuidPath(name: String): UUID { @@ -128,24 +129,11 @@ suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null phone = null, cashoutPayto = null, tanChannel = null, + minCashout = null, ctx = cfg.payto ) } -fun Route.intercept(callback: Route.() -> Unit, interceptor: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit): Route { - val subRoute = createChild(object : RouteSelector() { - override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = - RouteSelectorEvaluation.Constant - }) - subRoute.intercept(ApplicationCallPipeline.Plugins) { - interceptor() - proceed() - } - - callback(subRoute) - return subRoute -} - fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route = intercept(callback) { if (!implemented) { |