[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (048e0ada -> 45147878)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (048e0ada -> 45147878) |
Date: |
Fri, 20 Jan 2023 16:49:34 +0100 |
This is an automated email from the git hooks/post-receive script.
ms pushed a change to branch master
in repository libeufin.
from 048e0ada add rule to create TGZ using Git as proposed by Florian
new c32c2a00 CLI.
new 1e146449 204 responses with less code.
new b28f1511 Circuit API.
new 907eb293 testing the previous change
new 57f578dd Circuit API.
new fcca16e8 CLI
new 5b18cb6a adapt test
new 2a45c348 Error management.
new 45147878 revert name change to match the docs
The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
cli/bin/libeufin-cli | 174 ++++++++++++++-------
nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 10 +-
nexus/src/test/kotlin/DownloadAndSubmit.kt | 6 -
nexus/src/test/kotlin/JsonTest.kt | 3 +-
nexus/src/test/kotlin/MakeEnv.kt | 7 +
nexus/src/test/kotlin/SandboxCircuitApiTest.kt | 70 ++++++++-
.../kotlin/tech/libeufin/sandbox/CircuitApi.kt | 104 +++++++++---
.../src/main/kotlin/tech/libeufin/sandbox/DB.kt | 14 +-
.../main/kotlin/tech/libeufin/sandbox/Helpers.kt | 41 +++--
util/src/main/kotlin/HTTP.kt | 1 +
10 files changed, 323 insertions(+), 107 deletions(-)
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli
index de5377f4..68c38e2b 100755
--- a/cli/bin/libeufin-cli
+++ b/cli/bin/libeufin-cli
@@ -25,6 +25,14 @@ def maybe_auth(sandbox_ctx):
)
return dict()
+
+# Gets the account name to use in a request. It gives
+# precedence to the account name passed along the CLI options,
+# and falls back to the account name found in the environment.
+# It returns None if no account was found, or that was 'admin'.
+# Admin is excluded because it isn't modeled like ordinary
+# customers and would therefore very likely hit != 2xx response
+# statuses.
def get_account_name(accountNameCli, usernameEnv):
maybeUsername = accountNameCli
if not maybeUsername:
@@ -45,14 +53,9 @@ def check_response_status(resp, expected_status_code=200):
print("Response: {}".format(resp.text), file=sys.stderr)
sys.exit(1)
-# Prints unexpected responses without exiting
-# and optionally prints expected respones.
-def tell_user(resp, expected_status_code=200, withsuccess=False):
- if resp.status_code != expected_status_code:
- print(resp.content.decode("utf-8"), file=sys.stderr)
- return
- if withsuccess:
- print(resp.content.decode("utf-8"))
+# Prints the response body.
+def tell_user(resp):
+ print(resp.content.decode("utf-8"))
# Normalize the two components to "x/" and "y" and pass them
# to urljoin(). This avoids drop-policies from plain urljoin().
@@ -127,8 +130,8 @@ def users_self(obj):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@users.command("list", help="List users")
@click.pass_obj
@@ -141,8 +144,8 @@ def list_users(obj):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@users.command(help="Change user's password (as superuser)")
@@ -169,8 +172,8 @@ def change_password(obj, username, new_password):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@users.command("create", help="Create a new user without superuser privileges")
@@ -404,14 +407,12 @@ def get_key_letter(obj, connection_name, output_file):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
output = open(output_file, "wb")
output.write(resp.content)
output.close()
-
@connections.command(help="export backup")
@click.option("--passphrase", help="Passphrase for locking the backup",
required=True)
@click.option("--output-file", help="Where to store the backup", required=True)
@@ -459,7 +460,6 @@ def delete_connection(obj, connection_name):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -495,7 +495,6 @@ def restore_backup(obj, backup_file, passphrase,
connection_name):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -528,7 +527,6 @@ def new_ebics_connection(
print(f"Could not reach nexus at {url}")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -545,9 +543,9 @@ def connect(obj, connection_name):
print(e)
print(f"Could not reach nexus at {url}")
exit(1)
- tell_user(resp, withsuccess=True)
- check_response_status(resp)
+ check_response_status(resp)
+ tell_user(resp)
@connections.command(help="Import one bank account, chosen from the downloaded
ones.")
@click.option(
@@ -580,7 +578,6 @@ def import_bank_account(
print(f"Could not reach nexus at {url}: {e}")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -603,7 +600,6 @@ def download_bank_accounts(obj, connection_name):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -620,8 +616,8 @@ def list_connections(obj):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@connections.command(help="Show the status of a bank connection.")
@@ -638,8 +634,8 @@ def show_connection(obj, connection_name):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@connections.command(help="list bank accounts hosted at one connection")
@@ -658,8 +654,8 @@ def list_offered_bank_accounts(obj, connection_name):
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Schedules a new task")
@@ -708,7 +704,6 @@ def task_schedule(
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -728,8 +723,8 @@ def task_status(obj, account_name, task_name):
print("Could not reach nexus " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Delete a task")
@@ -748,7 +743,6 @@ def task_delete(obj, account_name, task_name):
print("Could not reach nexus " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -764,8 +758,8 @@ def tasks_show(obj, account_name):
print("Could not reach nexus " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Show accounts belonging to calling user")
@@ -778,8 +772,8 @@ def show(obj):
print(f"Could not reach nexus at {url}, error: {e}")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Prepare payment initiation debiting the account.")
@@ -825,7 +819,6 @@ def prepare_payment(
print("Could not reach nexus at " + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -857,7 +850,6 @@ def submit_payments(obj, account_name, payment_uuid):
print("Could not reach nexus at" + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -877,8 +869,8 @@ def show_payment(obj, account_name, payment_uuid):
print("Could not reach nexus at" + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="List payment initiations")
@@ -895,10 +887,8 @@ def list_payments(obj, account_name):
print("Could not reach nexus at" + url)
exit(1)
- tell_user(
- resp, withsuccess=True,
- )
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Delete a payment initiation")
@@ -917,7 +907,6 @@ def delete_payment(obj, account_name, payment_uuid):
print("Could not reach nexus at" + url)
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -945,8 +934,8 @@ def fetch_transactions(obj, account_name, range_type,
level):
print("Could not reach nexus " + url)
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@accounts.command(help="Get transactions from the simplified nexus JSON API")
@@ -982,7 +971,7 @@ def transactions(obj, compact, account_name):
)
)
else:
- tell_user(resp, withsuccess=True)
+ tell_user(resp)
check_response_status(resp)
@@ -996,8 +985,8 @@ def list_facades(obj):
print(f"Could not reach nexus (at {obj.nexus_base_url}): {e}")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@facades.command(
@@ -1030,7 +1019,6 @@ def new_anastasis_facade(obj, facade_name,
connection_name, account_name, curren
print(f"Could not reach nexus (at {obj.nexus_base_url}): {e}")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1064,7 +1052,6 @@ def new_twg_facade(obj, facade_name, connection_name,
account_name, currency):
print(f"Could not reach nexus (at {obj.nexus_base_url}): {e}")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1086,8 +1073,8 @@ def check_sandbox_status(obj):
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@sandbox_ebicshost.command("create", help="Create an EBICS host")
@@ -1107,7 +1094,6 @@ def make_ebics_host(obj, host_id):
print("Could not reach sandbox")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1123,8 +1109,8 @@ def list_ebics_host(obj):
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@sandbox.group("ebicssubscriber", help="manage EBICS subscribers")
@@ -1152,7 +1138,6 @@ def create_ebics_subscriber(obj, host_id, partner_id,
user_id):
print("Could not reach sandbox")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1168,8 +1153,8 @@ def list_ebics_subscriber(obj):
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@sandbox.group("ebicsbankaccount", help="manage EBICS bank accounts")
@@ -1223,7 +1208,6 @@ def associate_bank_account(
print("Could not reach sandbox")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1266,7 +1250,8 @@ def sandbox_demobank_list_transactions(obj, bank_account):
print("Could not reach sandbox at " + url)
exit(1)
- tell_user(resp, withsuccess=True)
+ check_response_status(resp)
+ tell_user(resp)
@sandbox_demobank.command("new-transaction", help="Initiate a new
transaction.")
@@ -1320,8 +1305,9 @@ def sandbox_demobank_info(obj, bank_account):
print(e)
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
+ check_response_status(resp)
+ tell_user(resp)
@sandbox_demobank.command(
"debug-url",
@@ -1432,8 +1418,8 @@ def bankaccount_list(obj):
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@sandbox_bankaccount.command("transactions", help="List transactions")
@@ -1451,8 +1437,8 @@ def transactions_list(obj, account_label):
print("Could not reach sandbox")
exit(1)
- tell_user(resp, withsuccess=True)
check_response_status(resp)
+ tell_user(resp)
@sandbox_bankaccount.command("generate-transactions", help="Generate test
transactions")
@@ -1471,7 +1457,6 @@ def bankaccount_generate_transactions(obj, account_label):
print("Could not reach sandbox")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1517,7 +1502,6 @@ def simulate_incoming_transaction(
print("Could not reach sandbox")
exit(1)
- tell_user(resp)
check_response_status(resp)
@@ -1606,7 +1590,7 @@ def circuit_cashout_info(obj, uuid):
exit(1)
check_response_status(resp)
- tell_user(resp, withsuccess=True)
+ tell_user(resp)
@sandbox_demobank.command(
"circuit-delete-account",
@@ -1885,6 +1869,88 @@ def circuit_cashout(obj, subject, amount_debit,
amount_credit, tan_channel):
exit(1)
check_response_status(resp, expected_status_code=202)
- tell_user(resp, 202, withsuccess=True) # Communicates back the operation
UUID.
+ tell_user(resp) # Communicates back the operation UUID.
+
+@sandbox_demobank.command(
+ "circuit-account-info",
+ help="Retrieve Circuit information about one account. Useful to get
cash-out address and contact details."
+)
+@click.option(
+ "--username",
+ help="Username of the account to retrieve. It defaults to
LIBEUFIN_SANDBOX_USERNAME and doesn't accept 'admin'.",
+)
+@click.pass_obj
+def circuit_account_info(obj, username):
+ resource_name = get_account_name(username, obj.username)
+ if not resource_name:
+ print(
+ "Couldn't find the username whose account is being retrieved.",
+ file=sys.stderr
+ )
+ exit(1)
+ # resource_name != admin
+ account_info_endpoint = obj.circuit_api_url(f"accounts/{resource_name}")
+ try:
+ resp = get(
+ account_info_endpoint,
+ **maybe_auth(obj)
+ )
+ except Exception as e:
+ print(e)
+ print("Could not reach the bank at " + account_info_endpoint)
+ exit(1)
+
+ check_response_status(resp)
+ tell_user(resp)
+
+
+@sandbox_demobank.command(
+ "circuit-accounts",
+ help="Gets the list of all the accounts managed by the Circuit. Only
'admin' allowed"
+)
+@click.pass_obj
+def circuit_accounts(obj):
+ # Check admin is requesting.
+ if (obj.username != "admin"):
+ print("Not running as 'admin'. Won't request", file=sys.stderr)
+ exit(1)
+ accounts_endpoint = obj.circuit_api_url(f"accounts")
+ try:
+ resp = get(
+ accounts_endpoint,
+ **maybe_auth(obj)
+ )
+ except Exception as e:
+ print(e)
+ print("Could not reach the bank at " + accounts_endpoint)
+ exit(1)
+
+ check_response_status(resp)
+ tell_user(resp)
+
+
+@sandbox_demobank.command(
+ "circuit-cashouts",
+ help="Gets the list of all the pending and confirmed cash-out operations."
+)
+@click.pass_obj
+def circuit_cashouts(obj):
+ # Check admin is requesting.
+ if (obj.username != "admin"):
+ print("Not running as 'admin'. Won't request", file=sys.stderr)
+ exit(1)
+ cashouts_endpoint = obj.circuit_api_url(f"cashouts")
+ try:
+ resp = get(
+ cashouts_endpoint,
+ **maybe_auth(obj)
+ )
+ except Exception as e:
+ print(e)
+ print("Could not reach the bank at " + cashouts_endpoint)
+ exit(1)
+
+ check_response_status(resp)
+ tell_user(resp)
cli()
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index 66efe910..f14d5552 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -424,10 +424,7 @@ private suspend fun historyOutgoing(call: ApplicationCall)
{
}
}
if (history.outgoing_transactions.size == 0) {
- call.respondBytes(
- bytes = ByteArray(0),
- status = HttpStatusCode.NoContent
- )
+ call.respond(HttpStatusCode.NoContent)
return
}
call.respond(
@@ -476,10 +473,7 @@ private suspend fun historyIncoming(call: ApplicationCall)
{
}
}
if (history.incoming_transactions.size == 0) {
- call.respondBytes(
- bytes = ByteArray(0),
- status = HttpStatusCode.NoContent
- )
+ call.respond(HttpStatusCode.NoContent)
return
}
return call.respond(
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index 928df051..0ac5b0c7 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -10,7 +10,6 @@ import io.ktor.server.routing.*
import io.ktor.server.testing.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
-import org.junit.Ignore
import org.junit.Test
import org.w3c.dom.Document
import tech.libeufin.nexus.*
@@ -87,11 +86,6 @@ fun getCustomEbicsServer(r: EbicsResponses, endpoint: String
= "/ebicsweb"): App
return ret
}
-/**
- * Remove @Ignore, after having put asserts along tests,
- * and having had access to runTask and TaskSchedule, that
- * are now 'private'.
- */
class DownloadAndSubmit {
/**
* Download a C52 report from the bank.
diff --git a/nexus/src/test/kotlin/JsonTest.kt
b/nexus/src/test/kotlin/JsonTest.kt
index a1024f85..138790cb 100644
--- a/nexus/src/test/kotlin/JsonTest.kt
+++ b/nexus/src/test/kotlin/JsonTest.kt
@@ -42,7 +42,8 @@ class JsonTest {
/**
* Ignored because this test was only used to check
- * the logs, as opposed to assert over values.
+ * the logs, as opposed to assert over values. Consider
+ * to remove the Ignore
*/
@Ignore
@Test
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index 40da1d81..bb25a0b6 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -197,12 +197,19 @@ fun prepSandboxDb() {
username = "foo"
passwordHash = CryptoUtil.hashpw("foo")
name = "Foo"
+ cashout_address = "payto://iban/OUTSIDE"
}
DemobankCustomerEntity.new {
username = "bar"
passwordHash = CryptoUtil.hashpw("bar")
name = "Bar"
}
+ DemobankCustomerEntity.new {
+ username = "baz"
+ passwordHash = CryptoUtil.hashpw("foo")
+ name = "Baz"
+ cashout_address = "payto://iban/OTHERBANK"
+ }
}
}
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
index 916400a1..85a41714 100644
--- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
@@ -1,17 +1,15 @@
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.plugins.*
-import io.ktor.client.plugins.auth.*
-import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
-import io.ktor.util.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.Test
import tech.libeufin.sandbox.*
import java.io.File
+import java.util.*
class SandboxCircuitApiTest {
// Get /config, fails if != 200.
@@ -27,6 +25,24 @@ class SandboxCircuitApiTest {
}
}
}
+
+ // Only tests that the calls get a 2xx status code.
+ @Test
+ fun listAccountsTest() {
+ withTestDatabase {
+ prepSandboxDb()
+ testApplication {
+ application(sandboxApp)
+ var R = client.get("/demobanks/default/circuit-api/accounts") {
+ basicAuth("admin", "foo")
+ }
+ println(R.bodyAsText())
+ client.get("/demobanks/default/circuit-api/accounts/baz") {
+ basicAuth("admin", "foo")
+ }
+ }
+ }
+ }
@Test
fun badUuidTest() {
withTestDatabase {
@@ -60,7 +76,53 @@ class SandboxCircuitApiTest {
assert(!checkEmailAddress("foo+bar@example.com"))
}
- // Test the creation and confirmation of a cash-out operation.
+ @Test
+ fun listCashouts() {
+ withTestDatabase {
+ prepSandboxDb()
+ testApplication {
+ application(sandboxApp)
+ var R = client.get("/demobanks/default/circuit-api/cashouts") {
+ expectSuccess = true
+ basicAuth("admin", "foo")
+ }
+ assert(R.status.value == HttpStatusCode.NoContent.value)
+ transaction {
+ CashoutOperationEntity.new {
+ tan = "unused"
+ uuid = UUID.randomUUID()
+ amountDebit = "unused"
+ amountCredit = "unused"
+ subject = "unused"
+ creationTime = 0L
+ tanChannel = SupportedTanChannels.FILE // change type
to enum?
+ account = "unused"
+ status = CashoutOperationStatus.PENDING
+ }
+ }
+ R = client.get("/demobanks/default/circuit-api/cashouts") {
+ expectSuccess = true
+ basicAuth("admin", "foo")
+ }
+ assert(R.status.value == HttpStatusCode.OK.value)
+ // Extract the UUID and check it.
+ val mapper = ObjectMapper()
+ var respJson = mapper.readTree(R.bodyAsText())
+ val uuid = respJson.get("cashouts").get(0).asText()
+ R =
client.get("/demobanks/default/circuit-api/cashouts/$uuid") {
+ expectSuccess = true
+ basicAuth("admin", "foo")
+ }
+ assert(R.status.value == HttpStatusCode.OK.value)
+ respJson = mapper.readTree(R.bodyAsText())
+ val status = respJson.get("status").asText()
+ assert(status.uppercase() == "PENDING")
+ println(R.bodyAsText())
+ }
+ }
+ }
+
+ // Tests the creation and confirmation of a cash-out operation.
@Test
fun cashout() {
withTestDatabase {
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
index 0c43dd29..1ac23785 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -1,5 +1,6 @@
package tech.libeufin.sandbox
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.ktor.server.application.*
import io.ktor.http.*
import io.ktor.server.request.*
@@ -12,7 +13,6 @@ import java.io.File
import java.io.InputStreamReader
import java.math.BigDecimal
import java.math.MathContext
-import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.text.toByteArray
@@ -85,6 +85,17 @@ data class CircuitAccountInfo(
val cashout_address: String
)
+data class CashoutOperationInfo(
+ val status: CashoutOperationStatus,
+ val amount_credit: String,
+ val amount_debit: String,
+ val subject: String,
+ val creation_time: Long, // milliseconds
+ val confirmation_time: Long?, // milliseconds
+ val tan_channel: SupportedTanChannels,
+ val account: String
+)
+
data class CashoutConfirmation(val tan: String)
// Validate phone number
@@ -186,13 +197,13 @@ fun circuitApi(circuitRoute: Route) {
}
if (maybeOperation == null)
throw notFound("Cash-out operation $uuid not found.")
- if (maybeOperation.state == CashoutOperationState.CONFIRMED)
+ if (maybeOperation.status == CashoutOperationStatus.CONFIRMED)
throw SandboxError(
HttpStatusCode.PreconditionFailed,
"Cash-out operation '$uuid' was confirmed already."
)
- if (maybeOperation.state != CashoutOperationState.PENDING)
- throw internalServerError("Found an unsupported cash-out operation
state: ${maybeOperation.state}")
+ if (maybeOperation.status != CashoutOperationStatus.PENDING)
+ throw internalServerError("Found an unsupported cash-out operation
state: ${maybeOperation.status}")
// Operation found and pending: delete from the database.
transaction { maybeOperation.delete() }
call.respond(HttpStatusCode.NoContent)
@@ -215,7 +226,7 @@ fun circuitApi(circuitRoute: Route) {
if (op == null)
throw notFound("Cash-out operation $operationUuid not found")
// 412 if the operation got already confirmed.
- if (op.state == CashoutOperationState.CONFIRMED)
+ if (op.status == CashoutOperationStatus.CONFIRMED)
throw SandboxError(
HttpStatusCode.PreconditionFailed,
"Cash-out operation $operationUuid was already confirmed."
@@ -240,13 +251,16 @@ fun circuitApi(circuitRoute: Route) {
* NOTE: the funds availability got already checked when this operation
* was created. On top of that, the 'wireTransfer()' helper does also
* check for funds availability. */
- wireTransfer(
- debitAccount = op.account,
- creditAccount = "admin",
- subject = op.subject,
- amount = op.amountDebit
- )
- transaction { op.state = CashoutOperationState.CONFIRMED }
+ transaction {
+ wireTransfer(
+ debitAccount = op.account,
+ creditAccount = "admin",
+ subject = op.subject,
+ amount = op.amountDebit
+ )
+ op.status = CashoutOperationStatus.CONFIRMED
+ op.confirmationTime = getUTCnow().toInstant().toEpochMilli()
+ }
call.respond(HttpStatusCode.NoContent)
return@post
}
@@ -262,7 +276,34 @@ fun circuitApi(circuitRoute: Route) {
}
if (maybeOperation == null)
throw notFound("Cash-out operation $operationUuid not found.")
- call.respond(object { val status = maybeOperation.state })
+ val ret = CashoutOperationInfo(
+ amount_credit = maybeOperation.amountCredit,
+ amount_debit = maybeOperation.amountDebit,
+ subject = maybeOperation.subject,
+ status = maybeOperation.status,
+ creation_time = maybeOperation.creationTime,
+ confirmation_time = maybeOperation.confirmationTime,
+ tan_channel = maybeOperation.tanChannel,
+ account = maybeOperation.account
+ )
+ call.respond(ret)
+ return@get
+ }
+ // Gets the list of all the cash-out operations.
+ circuitRoute.get("/cashouts") {
+ call.request.basicAuth(onlyAdmin = true)
+ val node = jacksonObjectMapper().createObjectNode()
+ val maybeArray = node.putArray("cashouts")
+ transaction {
+ CashoutOperationEntity.all().forEach {
+ maybeArray.add(it.uuid.toString())
+ }
+ }
+ if (maybeArray.size() == 0) {
+ call.respond(HttpStatusCode.NoContent)
+ return@get
+ }
+ call.respond(node)
return@get
}
// Create a cash-out operation.
@@ -325,9 +366,10 @@ fun circuitApi(circuitRoute: Route) {
val op = transaction {
CashoutOperationEntity.new {
this.amountDebit = req.amount_debit
+ this.amountCredit = req.amount_credit
this.subject = cashoutSubject
- this.creationTime = getUTCnow().toInstant().epochSecond
- this.tanChannel = tanChannel
+ this.creationTime = getUTCnow().toInstant().toEpochMilli()
+ this.tanChannel = SupportedTanChannels.valueOf(tanChannel)
this.account = user
this.tan = getRandomString(5)
}
@@ -395,17 +437,25 @@ fun circuitApi(circuitRoute: Route) {
throwIfInstitutionalName(resourceName)
allowOwnerOrAdmin(username, resourceName)
val customer = getCustomer(resourceName)
- val bankAccount = getBankAccountFromLabel(resourceName)
+ /**
+ * CUSTOMER AND BANK ACCOUNT INVARIANT.
+ *
+ * After having found a 'customer' associated with the resourceName
+ * - see previous line -, the bank must ensure that a 'bank account'
+ * exist under the same resourceName. If that fails, the bank broke
the
+ * invariant and should respond 500.
+ */
+ val bankAccount = getBankAccountFromLabel(resourceName, withBankFault
= true)
/**
* Throwing when name or cash-out address aren't found ensures
* that the customer was indeed added via the Circuit API, as opposed
* to the Access API.
*/
- val potentialError = "$resourceName not managed by the Circuit API."
+ val maybeError = "$resourceName not managed by the Circuit API."
call.respond(CircuitAccountInfo(
username = customer.username,
- name = customer.name ?: throw notFound(potentialError),
- cashout_address = customer.cashout_address ?: throw
notFound(potentialError),
+ name = customer.name ?: throw notFound(maybeError),
+ cashout_address = customer.cashout_address ?: throw
notFound(maybeError),
contact_data = CircuitContactData(
email = customer.email,
phone = customer.phone
@@ -420,12 +470,23 @@ fun circuitApi(circuitRoute: Route) {
val customers = mutableListOf<Any>()
transaction {
DemobankCustomerEntity.all().forEach {
+ if (it.cashout_address == null) {
+ logger.debug("Not listing account '${it.username}', as
that" +
+ " misses the cash-out address " +
+ "and therefore doesn't belong to the Circuit API"
+ )
+ return@forEach
+ }
customers.add(object {
val username = it.username
val name = it.name
})
}
}
+ if (customers.size == 0) {
+ call.respond(HttpStatusCode.NoContent)
+ return@get
+ }
call.respond(object {val customers = customers})
return@get
}
@@ -543,8 +604,11 @@ fun circuitApi(circuitRoute: Route) {
call.request.basicAuth(onlyAdmin = true)
val resourceName = call.getUriComponent("resourceName")
throwIfInstitutionalName(resourceName)
- val bankAccount = getBankAccountFromLabel(resourceName)
val customer = getCustomer(resourceName)
+ val bankAccount = getBankAccountFromLabel(
+ resourceName,
+ withBankFault = true // See comment "CUSTOMER AND BANK ACCOUNT
INVARIANT".
+ )
val balance = getBalance(bankAccount)
if (balance != BigDecimal.ZERO)
throw SandboxError(
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index f1eba15a..a496cd3e 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -431,7 +431,7 @@ class BankAccountStatementEntity(id: EntityID<Int>) :
IntEntity(id) {
var balanceClbd by BankAccountStatementsTable.balanceClbd
}
-enum class CashoutOperationState { CONFIRMED, PENDING }
+enum class CashoutOperationStatus { CONFIRMED, PENDING }
object CashoutOperationsTable : LongIdTable() {
val uuid = uuid("uuid").autoGenerate()
/**
@@ -440,24 +440,28 @@ object CashoutOperationsTable : LongIdTable() {
* local currency bank account.
*/
val amountDebit = text("amountDebit")
+ val amountCredit = text("amountCredit")
val subject = text("subject")
- val creationTime = long("creationTime") // in seconds.
- val tanChannel = text("tanChannel")
+ val creationTime = long("creationTime") // in milliseconds.
+ val confirmationTime = long("confirmationTime").nullable() // in
milliseconds.
+ val tanChannel = enumeration("tanChannel", SupportedTanChannels::class)
val account = text("account")
val tan = text("tan")
- val state = enumeration("state",
CashoutOperationState::class).default(CashoutOperationState.PENDING)
+ val status = enumeration("status",
CashoutOperationStatus::class).default(CashoutOperationStatus.PENDING)
}
class CashoutOperationEntity(id: EntityID<Long>) : LongEntity(id) {
companion object :
LongEntityClass<CashoutOperationEntity>(CashoutOperationsTable)
var uuid by CashoutOperationsTable.uuid
var amountDebit by CashoutOperationsTable.amountDebit
+ var amountCredit by CashoutOperationsTable.amountCredit
var subject by CashoutOperationsTable.subject
var creationTime by CashoutOperationsTable.creationTime
+ var confirmationTime by CashoutOperationsTable.confirmationTime
var tanChannel by CashoutOperationsTable.tanChannel
var account by CashoutOperationsTable.account
var tan by CashoutOperationsTable.tan
- var state by CashoutOperationsTable.state
+ var status by CashoutOperationsTable.status
}
object TalerWithdrawalsTable : LongIdTable() {
val wopid = uuid("wopid").autoGenerate()
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index 693dc885..de22bf34 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -302,7 +302,18 @@ fun getBankAccountFromIban(iban: String):
BankAccountEntity {
)
}
-fun getBankAccountFromLabel(label: String, demobank: String = "default"):
BankAccountEntity {
+/**
+ * The argument 'withBankFault' represents the case where
+ * _the bank_ must ensure that a resource (in this case a bank
+ * account) exists. For example, every 'customer' should have
+ * a 'bank account', and if a customer is found without a bank
+ * account, then the bank broke such condition.
+ */
+fun getBankAccountFromLabel(
+ label: String,
+ demobank: String = "default",
+ withBankFault: Boolean = false
+): BankAccountEntity {
val maybeDemobank = getDemobank(demobank)
if (maybeDemobank == null) {
logger.error("Demobank '$demobank' not found")
@@ -311,21 +322,33 @@ fun getBankAccountFromLabel(label: String, demobank:
String = "default"): BankAc
"Demobank '$demobank' not found"
)
}
- return getBankAccountFromLabel(label, maybeDemobank)
+ return getBankAccountFromLabel(
+ label,
+ maybeDemobank,
+ withBankFault
+ )
}
-fun getBankAccountFromLabel(label: String,
- demobank: DemobankConfigEntity
+fun getBankAccountFromLabel(
+ label: String,
+ demobank: DemobankConfigEntity,
+ withBankFault: Boolean = false
): BankAccountEntity {
- return transaction {
+ val maybeBankAccount = transaction {
BankAccountEntity.find(
BankAccountsTable.label eq label and (
BankAccountsTable.demoBank eq demobank.id
)
- ).firstOrNull() ?: throw SandboxError(
- HttpStatusCode.NotFound,
- "Did not find a bank account for label $label"
- )
+ ).firstOrNull()
}
+ if (maybeBankAccount == null && withBankFault)
+ throw internalServerError(
+ "Bank account $label was not found, but it should."
+ )
+ if (maybeBankAccount == null)
+ throw notFound(
+ "Bank account $label was not found."
+ )
+ return maybeBankAccount
}
fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity):
BankAccountEntity {
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 06884a04..0f70c7e4 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -4,6 +4,7 @@ import UtilError
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
+import io.ktor.server.response.*
import io.ktor.server.util.*
import io.ktor.util.*
import logger
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [libeufin] branch master updated (048e0ada -> 45147878),
gnunet <=
- [libeufin] 03/09: Circuit API., gnunet, 2023/01/20
- [libeufin] 02/09: 204 responses with less code., gnunet, 2023/01/20
- [libeufin] 04/09: testing the previous change, gnunet, 2023/01/20
- [libeufin] 08/09: Error management., gnunet, 2023/01/20
- [libeufin] 06/09: CLI, gnunet, 2023/01/20
- [libeufin] 07/09: adapt test, gnunet, 2023/01/20
- [libeufin] 01/09: CLI., gnunet, 2023/01/20
- [libeufin] 09/09: revert name change to match the docs, gnunet, 2023/01/20
- [libeufin] 05/09: Circuit API., gnunet, 2023/01/20