libeufin

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

commit e9cc8a8e44b6ad1269a53056500b45d564dc091b
parent 6647945e3ebba4fce2fd8eabf871ffb448a9f274
Author: ms <ms@taler.net>
Date:   Wed,  4 May 2022 10:46:47 +0200

Snack-machine demo changes.

- set creditor name
- make signup bonus optional
- make currency mandatory on the CLI (simulate-incoming-transaction subcommand)

Diffstat:
Mcli/bin/libeufin-cli | 14+++++++++++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 2++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 10+++++++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 4+++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 32++++++++++++++++++++------------
Mutil/src/main/kotlin/amounts.kt | 15+++++++++++++++
6 files changed, 58 insertions(+), 19 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -1273,13 +1273,21 @@ def sandbox_demobank_info(obj, bank_account): default=False, help="Decides whether a bank account is public.", ) +@click.option( + "--name", + default=False, + help="Person name", +) @click.pass_obj -def sandbox_demobank_register(obj, public): +def sandbox_demobank_register(obj, public, name): url = obj.access_api_url ("/testing/register") + req = dict(username=obj.username, password=obj.password, isPublic=public) + if name: + req.update(name=name) try: resp = post( url, - json=dict(username=obj.username, password=obj.password, isPublic=public), + json=req, ) except Exception as e: print(e) @@ -1386,7 +1394,7 @@ def bankaccount_generate_transactions(obj, account_label): @click.option( "--debtor-name", help="name of the person who is sending the payment", prompt=True ) -@click.option("--amount", help="amount, no currency", prompt=True) +@click.option("--amount", help="amount with currency (currency:x.y)", prompt=True) @click.option("--subject", help="payment subject", prompt=True) @click.pass_obj def simulate_incoming_transaction( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -92,6 +92,7 @@ enum class KeyState { object DemobankConfigsTable : LongIdTable() { val currency = text("currency") val allowRegistrations = bool("allowRegistrations") + val withSignupBonus = bool("withSignupBonus") val bankDebtLimit = integer("bankDebtLimit") val usersDebtLimit = integer("usersDebtLimit") val name = text("hostname") @@ -103,6 +104,7 @@ class DemobankConfigEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<DemobankConfigEntity>(DemobankConfigsTable) var currency by DemobankConfigsTable.currency var allowRegistrations by DemobankConfigsTable.allowRegistrations + var withSignupBonus by DemobankConfigsTable.withSignupBonus var bankDebtLimit by DemobankConfigsTable.bankDebtLimit var usersDebtLimit by DemobankConfigsTable.usersDebtLimit var name by DemobankConfigsTable.name diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -136,7 +136,10 @@ fun getCustomer(username: String): DemobankCustomerEntity { /** * Get person name from a customer's username. */ -fun getPersonNameFromCustomer(ownerUsername: String): String { +fun getPersonNameFromCustomer(ownerUsername: String?): String { + if (ownerUsername == null) { + return "Name unknown" + } return when (ownerUsername) { "admin" -> "admin" // Could be changed to Admin, or some different value. "bank" -> "The Bank" @@ -146,7 +149,7 @@ fun getPersonNameFromCustomer(ownerUsername: String): String { ).firstOrNull() ?: throw internalServerError( "Person name of '$ownerUsername' not found" ) - ownerCustomer.name ?: "Unknown" + ownerCustomer.name ?: "Name unknown" } } } @@ -170,7 +173,7 @@ fun getDefaultDemobank(): DemobankConfigEntity { ) } -fun maybeCreateDefaultDemobank() { +fun maybeCreateDefaultDemobank(withSignupBonus: Boolean = false) { transaction { if (DemobankConfigEntity.all().empty()) { DemobankConfigEntity.new { @@ -179,6 +182,7 @@ fun maybeCreateDefaultDemobank() { usersDebtLimit = 10000 allowRegistrations = true name = "default" + this.withSignupBonus = withSignupBonus } // Give one demobank a own bank account, mainly to award // customer upon registration. diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -95,7 +95,9 @@ data class CustomerRegistration( val password: String, val isPublic: Boolean = false, // When missing, it's autogenerated. - val iban: String? + val iban: String?, + // When missing, stays null in the DB. + val name: String? ) // Could be used as a general bank account info container. diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -295,6 +295,10 @@ class Serve : CliktCommand("Run sandbox HTTP server") { help = "Bind the Sandbox to the Unix domain socket at PATH. Overrides" + " --port, when both are given", metavar = "PATH" ) + private val withSignupBonus by option( + "--with-signup-bonus", + help = "Award new customers with 100 units of currency!" + ).flag("--without-signup-bonus", default = false) override fun run() { WITH_AUTH = auth @@ -305,7 +309,7 @@ class Serve : CliktCommand("Run sandbox HTTP server") { exitProcess(1) } execThrowableOrTerminate { dbCreateTables(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)) } - maybeCreateDefaultDemobank() + maybeCreateDefaultDemobank(withSignupBonus) if (withUnixSocket != null) { startServer( withUnixSocket ?: throw Exception("Could not use the Unix domain socket path value!"), @@ -590,16 +594,10 @@ val sandboxApp: Application.() -> Unit = { // Book one incoming payment for the requesting account. // The debtor is not required to have an account at this Sandbox. post("/admin/bank-accounts/{label}/simulate-incoming-transaction") { - call.request.basicAuth() + val username = call.request.basicAuth() val body = call.receiveJson<IncomingPaymentInfo>() // FIXME: generate nicer UUID! val accountLabel = ensureNonNull(call.parameters["label"]) - if (!validatePlainAmount(body.amount)) { - throw SandboxError( - HttpStatusCode.BadRequest, - "invalid amount (should be plain amount without currency)" - ) - } val reqDebtorBic = body.debtorBic if (reqDebtorBic != null && !validateBic(reqDebtorBic)) { throw SandboxError( @@ -607,8 +605,16 @@ val sandboxApp: Application.() -> Unit = { "invalid BIC" ) } + val (amount, currency) = parseAmountAsString(body.amount) transaction { val demobank = getDefaultDemobank() + /** + * This API needs compatibility with the currency-less format. + */ + if (currency != null) { + if (currency != demobank.currency) + throw SandboxError(HttpStatusCode.BadRequest, "Currency ${currency} not supported.") + } val account = getBankAccountFromLabel( accountLabel, demobank ) @@ -616,18 +622,18 @@ val sandboxApp: Application.() -> Unit = { BankAccountTransactionEntity.new { creditorIban = account.iban creditorBic = account.bic - creditorName = "Creditor Name" // FIXME: Waits to get this value from the DemobankCustomer type. + creditorName = getPersonNameFromCustomer(username) debtorIban = body.debtorIban debtorBic = reqDebtorBic debtorName = body.debtorName subject = body.subject - amount = body.amount + this.amount = amount date = getUTCnow().toInstant().toEpochMilli() accountServicerReference = "sandbox-$randId" this.account = account direction = "CRDT" this.demobank = demobank - currency = demobank.currency + this.currency = demobank.currency } } call.respond(object {}) @@ -1436,8 +1442,10 @@ val sandboxApp: Application.() -> Unit = { DemobankCustomerEntity.new { username = req.username passwordHash = CryptoUtil.hashpw(req.password) + name = req.name // nullable } - bankAccount.bonus("${demobank.currency}:100") + if (demobank.withSignupBonus) + bankAccount.bonus("${demobank.currency}:100") bankAccount } val balance = balanceForAccount(bankAccount) diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt @@ -28,6 +28,21 @@ fun validatePlainAmount(plainAmount: String): Boolean { return re.matches(plainAmount) } +/** + * Parse an "amount" where the currency is optional. It returns + * a pair where the first item is always the amount, and the second + * is the currency or null (when this one wasn't given in the input) + */ +fun parseAmountAsString(amount: String): Pair<String, String?> { + val match = Regex("^([A-Z]+:)?([0-9]+(\\.[0-9]+)?)$").find(amount) ?: throw + UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount") + var (currency, number) = match.destructured + // Currency given, need to strip the ":". + if (currency.isNotEmpty()) + currency = currency.dropLast(1) + return Pair(number, if (currency.isEmpty()) null else currency) +} + fun parseAmount(amount: String): AmountWithCurrency { val match = Regex("([A-Z]+):([0-9]+(\\.[0-9]+)?)").find(amount) ?: throw UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")