libeufin

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

commit 2cd810f110e64c7024ce21866f1c705e1c2f032c
parent 91d31e4d7cfeb8fccc0ea98f047917f83a1afb3e
Author: Florian Dold <florian@dold.me>
Date:   Wed, 13 Jan 2021 22:13:47 +0100

implement per-account transaction listing in sandbox

Diffstat:
Mcli/bin/libeufin-cli | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 4+++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 11+++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 3++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 211 insertions(+), 10 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -334,9 +334,9 @@ def show(obj): print(resp.content.decode("utf-8")) @accounts.command(help="prepare payment debiting 'account-name'") -@click.option("--credit-iban", help="IBAN that will receive the payment", required=True) -@click.option("--credit-bic", help="BIC that will receive the payment", required=False) -@click.option("--credit-name", help="Legal name that will receive the payment", required=True) +@click.option("--creditor-iban", help="IBAN that will receive the payment", required=True) +@click.option("--creditor-bic", help="BIC that will receive the payment", required=False) +@click.option("--creditor-name", help="Legal name that will receive the payment", required=True) @click.option("--payment-amount", help="Amount to be paid (<currency>:X.Y)", required=True) @click.option("--payment-subject", help="Subject of this payment", required=True) @click.argument("account-name") @@ -448,18 +448,30 @@ def new_facade(obj, facade_name, connection_name, account_name): print(resp.content.decode("utf-8")) - @sandbox.group("ebicshost", help="manage EBICS hosts") @click.pass_context def sandbox_ebicshost(ctx): pass +@sandbox.command("check", help="check sandbox status") +@click.pass_obj +def check_sandbox_status(obj): + sandbox_base_url = obj.require_sandbox_base_url() + url = urljoin(sandbox_base_url, "/config") + try: + resp = get(url) + except Exception: + print("Could not reach sandbox") + exit(1) + print(resp.content.decode("utf-8")) + + @sandbox_ebicshost.command("create", help="Create an EBICS host") @click.option("--host-id", help="EBICS host ID", required=True, prompt=True) @click.pass_obj def make_ebics_host(obj, host_id): sandbox_base_url = obj.require_sandbox_base_url() - url = urljoin(sandbox_base_url, "/admin/ebics/host") + url = urljoin(sandbox_base_url, "/admin/ebics/hosts") try: resp = post(url, json=dict(hostID=host_id, ebicsVersion="2.5")) except Exception: @@ -517,6 +529,7 @@ def sandbox_ebicsbankaccount(ctx): pass @sandbox_ebicsbankaccount.command("create", help="Create a bank account associated to an EBICS subscriber.") +@click.option("--currency", help="currency", prompt=True) @click.option("--iban", help="IBAN", required=True) @click.option("--bic", help="BIC", required=True) @click.option("--person-name", help="bank account owner name", required=True) @@ -525,11 +538,12 @@ def sandbox_ebicsbankaccount(ctx): @click.option("--ebics-host-id", help="host ID of the Ebics subscriber", required=True) @click.option("--ebics-partner-id", help="partner ID of the Ebics subscriber", required=True) @click.pass_obj -def associate_bank_account(obj, iban, bic, person_name, account_name, +def associate_bank_account(obj, currency, iban, bic, person_name, account_name, ebics_user_id, ebics_host_id, ebics_partner_id): sandbox_base_url = obj.require_sandbox_base_url() url = urljoin(sandbox_base_url, "/admin/ebics/bank-accounts") body = dict( + currency=currency, subscriber=dict(userID=ebics_user_id, partnerID=ebics_partner_id, hostID=ebics_host_id), iban=iban, bic=bic, name=person_name, label=account_name ) @@ -546,6 +560,45 @@ def associate_bank_account(obj, iban, bic, person_name, account_name, def sandbox_bankaccount(ctx): pass +@sandbox_bankaccount.command("list", help="List accounts") +@click.pass_obj +def bankaccount_list(obj): + sandbox_base_url = obj.require_sandbox_base_url() + url = urljoin(sandbox_base_url, f"/admin/bank-accounts") + try: + resp = get(url) + except Exception: + print("Could not reach sandbox") + exit(1) + print(resp.content.decode("utf-8")) + +@sandbox_bankaccount.command("transactions", help="List transactions") +@click.argument("account-label") +@click.pass_obj +def bankaccount_list(obj, account_label): + sandbox_base_url = obj.require_sandbox_base_url() + url = urljoin(sandbox_base_url, f"/admin/bank-accounts/{account_label}/transactions") + try: + resp = get(url) + except Exception: + print("Could not reach sandbox") + exit(1) + print(resp.content.decode("utf-8")) + +@sandbox_bankaccount.command("generate-transactions", help="Generate test transactions") +@click.argument("account-label") +@click.pass_obj +def bankaccount_generate_transactions(obj, account_label): + sandbox_base_url = obj.require_sandbox_base_url() + url = urljoin(sandbox_base_url, f"/admin/bank-accounts/{account_label}/generate-transactions") + try: + resp = post(url) + except Exception: + print("Could not reach sandbox") + exit(1) + print(resp.content.decode("utf-8")) + + @sandbox_bankaccount.command(help="book a payment in the sandbox") @click.option("--creditor-iban", help="IBAN receiving the payment", prompt=True) @click.option("--creditor-bic", help="BIC receiving the payment", prompt=True) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -273,8 +273,9 @@ object BankAccountsTable : IntIdTable() { val iban = text("iban") val bic = text("bic") val name = text("name") - val label = text("label") + val label = text("label").uniqueIndex("accountLabelIndex") val subscriber = reference("subscriber", EbicsSubscribersTable) + val currency = text("currency") } class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) { @@ -285,6 +286,7 @@ class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) { var name by BankAccountsTable.name var label by BankAccountsTable.label var subscriber by EbicsSubscriberEntity referencedOn BankAccountsTable.subscriber + var currency by BankAccountsTable.currency } object BankAccountStatementsTable : IntIdTable() { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -53,6 +53,17 @@ fun getBankAccountFromIban(iban: String): BankAccountEntity { ) } +fun getBankAccountFromLabel(label: String): BankAccountEntity { + return transaction { + BankAccountEntity.find( + BankAccountsTable.label eq label + ) + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, + "Did not find a bank account for label ${label}" + ) +} + fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity): BankAccountEntity { return transaction { BankAccountEntity.find(BankAccountsTable.subscriber eq subscriber.id) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -69,7 +69,8 @@ data class BankAccountRequest( val iban: String, val bic: String, val name: String, - val label: String + val label: String, + val currency: String ) data class DateRange( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -77,6 +77,7 @@ import tech.libeufin.sandbox.BankAccountTransactionsTable.direction import tech.libeufin.util.* import tech.libeufin.util.ebics_h004.EbicsResponse import tech.libeufin.util.ebics_h004.EbicsTypes +import java.util.* import kotlin.random.Random const val DEFAULT_DB_CONNECTION = "jdbc:sqlite:/tmp/libeufin-sandbox.sqlite3" @@ -86,6 +87,7 @@ class BadInputData(inputData: String?) : Exception("Customer provided invalid in class UnacceptableFractional(badNumber: BigDecimal) : Exception( "Unacceptable fractional part ${badNumber}" ) + lateinit var LOGGER: Logger data class SandboxError(val statusCode: HttpStatusCode, val reason: String) : Exception() @@ -102,6 +104,7 @@ class ResetTables : CliktCommand("Drop all the tables from the database") { helpFormatter = CliktHelpFormatter(showDefaultValues = true) } } + private val dbConnString by option().default(DEFAULT_DB_CONNECTION) override fun run() { execThrowableOrTerminate { @@ -117,6 +120,7 @@ class Serve : CliktCommand("Run sandbox HTTP server") { helpFormatter = CliktHelpFormatter(showDefaultValues = true) } } + private val dbConnString by option().default(DEFAULT_DB_CONNECTION) private val logLevel by option() private val port by option().int().default(5000) @@ -160,6 +164,17 @@ data class EbicsHostPublicInfo( val authenticationPublicKey: RSAPublicKey ) +data class BankAccountInfo( + val label: String, + val name: String, + val iban: String, + val bic: String +) + +data class BankAccountsListReponse( + val accounts: List<BankAccountInfo> +) + inline fun <reified T> Document.toObject(): T { val jc = JAXBContext.newInstance(T::class.java) val m = jc.createUnmarshaller() @@ -171,6 +186,12 @@ fun BigDecimal.signToString(): String { // minus sign is added by default already. } +fun ensureNonNull(param: String?): String { + return param ?: throw SandboxError( + HttpStatusCode.BadRequest, "Bad ID given: $param" + ) +} + fun main(args: Array<String>) { SandboxCommand() .subcommands(Serve(), ResetTables()) @@ -259,6 +280,14 @@ fun serverMain(dbName: String, port: Int) { get("/") { call.respondText("Hello, this is Sandbox\n", ContentType.Text.Plain) } + get("/config") { + call.respond(object { + val name = "libeufin-sandbox" + + // FIXME: use actual version here! + val version = "0.0.0-dev.0" + }) + } // only reason for a post is to hide the iban (to some degree.) post("/admin/payments/camt") { val body = call.receive<CamtParams>() @@ -268,6 +297,7 @@ fun serverMain(dbName: String, port: Int) { call.respondText(camt53, ContentType.Text.Xml, HttpStatusCode.OK) return@post } + // FIXME: This returns *all* payments for all accounts. Is that really useful/required? get("/admin/payments") { val ret = PaymentsResponse() transaction { @@ -342,11 +372,114 @@ fun serverMain(dbName: String, port: Int) { bic = body.bic name = body.name label = body.label + currency = body.currency.toUpperCase(Locale.ROOT) } } call.respondText("Bank account created") return@post } + get("/admin/bank-accounts") { + val accounts = mutableListOf<BankAccountInfo>() + val accountsResp = BankAccountsListReponse( + accounts = accounts + ) + transaction { + BankAccountEntity.all().forEach { + accounts.add( + BankAccountInfo( + label = it.label, + name = it.name, + bic = it.bic, + iban = it.iban + ) + ) + } + } + call.respond(accounts) + } + get("/admin/bank-accounts/{label}/transactions") { + val ret = PaymentsResponse() + transaction { + val accountLabel = ensureNonNull(call.parameters["label"]) + transaction { + val account = getBankAccountFromLabel(accountLabel) + BankAccountTransactionsTable.select { BankAccountTransactionsTable.account eq account.id } + .forEach { + ret.payments.add( + RawPayment( + creditorIban = it[creditorIban], + debitorIban = it[debitorIban], + subject = it[BankAccountTransactionsTable.subject], + date = it[date].toHttpDateString(), + amount = it[amount], + creditorBic = it[creditorBic], + creditorName = it[creditorName], + debitorBic = it[debitorBic], + debitorName = it[debitorName], + currency = it[currency], + direction = it[direction] + ) + ) + } + } + } + call.respond( + object { + val payments = ret + } + ) + } + post("/admin/bank-accounts/{label}/generate-transactions") { + transaction { + val accountLabel = ensureNonNull(call.parameters["label"]) + val account = getBankAccountFromLabel(accountLabel) + + run { + val random = Random.nextLong() + val amount = Random.nextLong(5, 25) + + BankAccountTransactionsTable.insert { + it[creditorIban] = account.iban + it[creditorBic] = account.bic + it[creditorName] = account.name + it[debitorIban] = "DE64500105178797276788" + it[debitorBic] = "FOBADEM001" + it[debitorName] = "Max Mustermann" + it[subject] = "sample transaction $random" + it[BankAccountTransactionsTable.amount] = amount.toString() + it[currency] = account.currency + it[date] = Instant.now().toEpochMilli() + it[pmtInfId] = random.toString() + it[msgId] = random.toString() + it[BankAccountTransactionsTable.account] = account.id + it[direction] = "CRDT" + } + } + + run { + val random = Random.nextLong() + val amount = Random.nextLong(5, 25) + + BankAccountTransactionsTable.insert { + it[debitorIban] = account.iban + it[debitorBic] = account.bic + it[debitorName] = account.name + it[creditorIban] = "DE64500105178797276788" + it[creditorBic] = "FOBADEM001" + it[creditorName] = "Max Mustermann" + it[subject] = "sample transaction $random" + it[BankAccountTransactionsTable.amount] = amount.toString() + it[currency] = account.currency + it[date] = Instant.now().toEpochMilli() + it[pmtInfId] = random.toString() + it[msgId] = random.toString() + it[BankAccountTransactionsTable.account] = account.id + it[direction] = "DBIT" + } + } + } + call.respond(object {}) + } /** * Creates a new Ebics subscriber. */ @@ -372,7 +505,7 @@ fun serverMain(dbName: String, port: Int) { * Shows all the Ebics subscribers' details. */ get("/admin/ebics/subscribers") { - var ret = AdminGetSubscribers() + val ret = AdminGetSubscribers() transaction { EbicsSubscriberEntity.all().forEach { ret.subscribers.add( @@ -431,4 +564,4 @@ fun serverMain(dbName: String, port: Int) { } LOGGER.info("Up and running") server.start(wait = true) -} +} +\ No newline at end of file