gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]