summaryrefslogtreecommitdiff
path: root/bank/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'bank/src/main/kotlin')
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Config.kt13
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Constants.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt49
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt59
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt27
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt83
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt39
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt10
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt16
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) {