gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] 02/02: Implementing accounts deletion.


From: gnunet
Subject: [libeufin] 02/02: Implementing accounts deletion.
Date: Mon, 02 Oct 2023 12:44:51 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

commit 3bfd71615f61f17708ab97757c8fde428519eeb6
Author: MS <ms@taler.net>
AuthorDate: Mon Oct 2 12:44:17 2023 +0200

    Implementing accounts deletion.
---
 .../main/kotlin/tech/libeufin/bank/BankMessages.kt | 21 ++++++-
 .../tech/libeufin/bank/CorebankApiHandlers.kt      | 49 ++++++++++++----
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 30 +++++++---
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    |  5 ++
 .../tech/libeufin/bank/WireGatewayApiHandlers.kt   |  6 +-
 bank/src/test/kotlin/Common.kt                     |  1 +
 bank/src/test/kotlin/DatabaseTest.kt               | 42 +++++++++++---
 bank/src/test/kotlin/LibeuFinApiTest.kt            | 67 +++++++++++++++++++++-
 bank/src/test/kotlin/TalerApiTest.kt               | 13 +++--
 contrib/wallet-core                                |  2 +-
 database-versioning/libeufin-bank-0001.sql         |  1 +
 database-versioning/procedures.sql                 | 51 ++++++++++++++++
 util/src/main/kotlin/Encoding.kt                   |  4 ++
 13 files changed, 254 insertions(+), 38 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
index 787a99a5..75869426 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -19,7 +19,6 @@
 
 package tech.libeufin.bank
 
-import CreditDebitIndicator
 import io.ktor.http.*
 import io.ktor.server.application.*
 import kotlinx.serialization.Serializable
@@ -475,6 +474,16 @@ fun ResourceName.canI(c: Customer, withAdmin: Boolean = 
true): Boolean {
 fun ApplicationCall.getResourceName(param: String): ResourceName =
     this.expectUriComponent(param)
 
+/**
+ * This type communicates the result of deleting an account
+ * from the database.
+ */
+enum class CustomerDeletionResult {
+    SUCCESS,
+    CUSTOMER_NOT_FOUND,
+    BALANCE_NOT_ZERO
+}
+
 /**
  * This type communicates the result of a database operation
  * to confirm one withdrawal operation.
@@ -496,6 +505,16 @@ enum class WithdrawalConfirmationResult {
     CONFLICT
 }
 
+/**
+ * Communicates the result of creating a bank transaction in the database.
+ */
+enum class BankTransactionResult {
+    NO_CREDITOR,
+    NO_DEBTOR,
+    SUCCESS,
+    CONFLICT // balance insufficient
+}
+
 // GET /config response from the Taler Integration API.
 @Serializable
 data class TalerIntegrationConfigResponse(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index b9a53fab..489b71e0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -185,13 +185,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
                 transactionDate = Instant.now()
             )
             when (db.bankTransactionCreate(adminPaysBonus)) {
-                Database.BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Bonus impossible: creditor not found, despite its recent 
creation.")
-
-                Database.BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
-
-                Database.BankTransactionResult.CONFLICT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
-
-                Database.BankTransactionResult.SUCCESS -> {/* continue the 
execution */
+                BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Bonus impossible: creditor not found, despite its recent 
creation.")
+                BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
+                BankTransactionResult.CONFLICT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
+                BankTransactionResult.SUCCESS -> {/* continue the execution */
                 }
             }
         }
@@ -257,6 +254,36 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         return@get
     }
+    delete("/accounts/{USERNAME}") {
+        val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
+        val resourceName = call.expectUriComponent("USERNAME")
+        // Checking rights.
+        if (c.login != "admin" && ctx.restrictAccountDeletion)
+            throw forbidden("Only admin allowed.")
+        if (!resourceName.canI(c, withAdmin = true))
+            throw forbidden("Insufficient rights on this account.")
+        // Not deleting reserved names.
+        if (resourceName == "bank" || resourceName == "admin")
+            throw forbidden("Cannot delete reserved accounts.")
+        val res = db.customerDeleteIfBalanceIsZero(resourceName)
+        when (res) {
+            CustomerDeletionResult.CUSTOMER_NOT_FOUND ->
+                throw notFound(
+                    "Customer '$resourceName' not found",
+                    talerEc = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+                    )
+            CustomerDeletionResult.BALANCE_NOT_ZERO ->
+                throw LibeufinBankException(
+                    httpStatus = HttpStatusCode.PreconditionFailed,
+                    talerError = TalerError(
+                        hint = "Balance is not zero.",
+                        code = TalerErrorCode.TALER_EC_NONE.code // FIXME: 
need EC.
+                    )
+                )
+            CustomerDeletionResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
+        }
+        return@delete
+    }
 
     post("/accounts/{USERNAME}/withdrawals") {
         val c = call.authenticateBankRequest(db, TokenScope.readwrite)
@@ -435,13 +462,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         val res = db.bankTransactionCreate(dbInstructions)
         when (res) {
-            Database.BankTransactionResult.CONFLICT -> throw conflict(
+            BankTransactionResult.CONFLICT -> throw conflict(
                 "Insufficient funds",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
-            Database.BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Creditor not found despite previous checks.")
-            Database.BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Debtor not found despite the request was authenticated.")
-            Database.BankTransactionResult.SUCCESS -> 
call.respond(HttpStatusCode.OK)
+            BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Creditor not found despite previous checks.")
+            BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Debtor not found despite the request was authenticated.")
+            BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
         }
         return@post
     }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 717239de..a18f5bd2 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -21,7 +21,6 @@
 package tech.libeufin.bank
 
 import org.postgresql.jdbc.PgConnection
-import org.postgresql.util.PSQLException
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import tech.libeufin.util.getJdbcConnectionFromPg
@@ -231,6 +230,27 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
         }
     }
 
+    /**
+     * Deletes a customer (including its bank account row) from
+     * the database.  The bank account gets deleted by the cascade.
+     */
+    fun customerDeleteIfBalanceIsZero(login: String): CustomerDeletionResult {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              out_nx_customer,
+              out_balance_not_zero
+              FROM customer_delete(?);
+        """)
+        stmt.setString(1, login)
+        stmt.executeQuery().apply {
+            if (!this.next()) throw internalServerError("Deletion returned 
nothing.")
+            if (this.getBoolean("out_nx_customer")) return 
CustomerDeletionResult.CUSTOMER_NOT_FOUND
+            if (this.getBoolean("out_balance_not_zero")) return 
CustomerDeletionResult.BALANCE_NOT_ZERO
+            return CustomerDeletionResult.SUCCESS
+        }
+    }
+
     // Mostly used to get customers out of bearer tokens.
     fun customerGetFromRowId(customer_id: Long): Customer? {
         reconnect()
@@ -304,6 +324,7 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
             )
         }
     }
+
     // Possibly more "customerGetFrom*()" to come.
 
     // BEARER TOKEN
@@ -566,12 +587,7 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
     }
 
     // BANK ACCOUNT TRANSACTIONS
-    enum class BankTransactionResult {
-        NO_CREDITOR,
-        NO_DEBTOR,
-        SUCCESS,
-        CONFLICT // balance insufficient
-    }
+
     fun bankTransactionCreate(
         tx: BankInternalTransaction
     ): BankTransactionResult {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 29dd7e9d..2db84c3c 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -82,6 +82,10 @@ data class BankApplicationContext(
      * Restrict account registration to the administrator.
      */
     val restrictRegistration: Boolean,
+    /**
+     * Restrict account deletion to the administrator.
+     */
+    val restrictAccountDeletion: Boolean,
     /**
      * Cashout currency, if cashouts are supported.
      */
@@ -480,6 +484,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig): 
BankApplicationConte
         suggestedWithdrawalExchange = cfg.lookupValueString("libeufin-bank", 
"suggested_withdrawal_exchange"),
         defaultAdminDebtLimit = cfg.requireValueAmount("libeufin-bank", 
"default_admin_debt_limit", currency),
         spaCaptchaURL = cfg.lookupValueString("libeufin-bank", 
"spa_captcha_url"),
+        restrictAccountDeletion = 
cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_account_deletion", 
true)
     )
 }
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 95307b31..71aa2039 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -123,12 +123,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
             exchangeBankAccountId = exchangeBankAccount.expectRowId(),
             timestamp = transferTimestamp
         )
-        if (dbRes.txResult == Database.BankTransactionResult.CONFLICT)
+        if (dbRes.txResult == BankTransactionResult.CONFLICT)
             throw conflict(
                 "Insufficient balance for exchange",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
-        if (dbRes.txResult == Database.BankTransactionResult.NO_CREDITOR)
+        if (dbRes.txResult == BankTransactionResult.NO_CREDITOR)
             throw notFound(
                 "Creditor account was not found",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
@@ -179,7 +179,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
          * Other possible errors are highly unlikely, because of the
          * previous checks on the existence of the involved bank accounts.
          */
-        if (res == Database.BankTransactionResult.CONFLICT)
+        if (res == BankTransactionResult.CONFLICT)
             throw conflict(
                 "Insufficient balance",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt
index 425cff3a..8d9721ab 100644
--- a/bank/src/test/kotlin/Common.kt
+++ b/bank/src/test/kotlin/Common.kt
@@ -47,6 +47,7 @@ fun getTestContext(
         registrationBonus = null,
         suggestedWithdrawalExchange = suggestedExchange,
         spaCaptchaURL = null,
+        restrictAccountDeletion = true
     )
 }
 
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index b0a9128a..89415b3a 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -18,8 +18,10 @@
  */
 
 import org.junit.Test
+import org.postgresql.jdbc.PgConnection
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
 import java.time.Instant
 import java.util.Random
 import java.util.UUID
@@ -125,7 +127,7 @@ class DatabaseTest {
             exchangeBankAccountId = 1L,
             timestamp = Instant.now()
         )
-        assert(res.txResult == Database.BankTransactionResult.SUCCESS)
+        assert(res.txResult == BankTransactionResult.SUCCESS)
     }
 
     @Test
@@ -167,13 +169,13 @@ class DatabaseTest {
             TalerAmount(50, 0, currency)
         )
         val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays 
Bar and goes debit.
-        assert(firstSpending == Database.BankTransactionResult.SUCCESS)
+        assert(firstSpending == BankTransactionResult.SUCCESS)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Foo: credit -> debit
         assert(fooAccount?.hasDebt == true) // Asserting Foo's debit.
         // Now checking that more spending doesn't get Foo out of debit.
         val secondSpending = db.bankTransactionCreate(fooPaysBar)
-        assert(secondSpending == Database.BankTransactionResult.SUCCESS)
+        assert(secondSpending == BankTransactionResult.SUCCESS)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Checking that Foo's debit is two times the paid amount
         // Foo: debit -> debit
@@ -200,14 +202,14 @@ class DatabaseTest {
             transactionDate = Instant.now()
         )
         val barPays = db.bankTransactionCreate(barPaysFoo)
-        assert(barPays == Database.BankTransactionResult.SUCCESS)
+        assert(barPays == BankTransactionResult.SUCCESS)
         barAccount = db.bankAccountGetFromOwnerId(barId)
         val barBalanceTen: TalerAmount? = barAccount?.balance
         // Bar: credit -> credit
         assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && 
barBalanceTen.frac == 0)
         // Bar pays again to let Foo return in credit.
         val barPaysAgain = db.bankTransactionCreate(barPaysFoo)
-        assert(barPaysAgain == Database.BankTransactionResult.SUCCESS)
+        assert(barPaysAgain == BankTransactionResult.SUCCESS)
         // Refreshing the two accounts.
         barAccount = db.bankAccountGetFromOwnerId(barId)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
@@ -218,7 +220,7 @@ class DatabaseTest {
         assert(barAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true)
         // Bringing Bar to debit.
         val barPaysMore = db.bankTransactionCreate(barPaysFoo)
-        assert(barPaysMore == Database.BankTransactionResult.SUCCESS)
+        assert(barPaysMore == BankTransactionResult.SUCCESS)
         barAccount = db.bankAccountGetFromOwnerId(barId)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Bar: credit -> debit
@@ -226,6 +228,32 @@ class DatabaseTest {
         assert(fooAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == 
true)
         assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == 
true)
     }
+
+    // Testing customer(+bank account) deletion logic.
+    @Test
+    fun customerDeletionTest() {
+        val db = initDb()
+        // asserting false, as foo doesn't exist yet.
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.CUSTOMER_NOT_FOUND)
+        // Creating foo.
+        db.customerCreate(customerFoo).apply {
+            assert(this != null)
+            assert(db.bankAccountCreate(bankAccountFoo) != null)
+        }
+        // foo has zero balance, deletion should succeed.
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.SUCCESS)
+        val db2 = initDb()
+        // Creating foo again, artificially setting its balance != zero.
+        db2.customerCreate(customerFoo).apply {
+            assert(this != null)
+            db2.bankAccountCreate(bankAccountFoo).apply {
+                assert(this != null)
+                val conn = 
DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+                conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET 
balance.frac = 1 WHERE bank_account_id = $this")
+            }
+        }
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.BALANCE_NOT_ZERO)
+    }
     @Test
     fun customerCreationTest() {
         val db = initDb()
@@ -346,7 +374,7 @@ class DatabaseTest {
             paymentInformationId = "pmtinfid",
             transactionDate = Instant.now()
         )
-        ) == Database.BankTransactionResult.SUCCESS)
+        ) == BankTransactionResult.SUCCESS)
         // Confirming the cash-out
         assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L))
         // Checking the confirmation took place.
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 107d277c..d9a5a3c9 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -2,13 +2,16 @@ import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.client.statement.*
 import io.ktor.http.*
+import io.ktor.server.engine.*
 import io.ktor.server.testing.*
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
+import org.postgresql.jdbc.PgConnection
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
 import java.time.Duration
 import java.time.Instant
 import java.time.temporal.ChronoUnit
@@ -517,6 +520,66 @@ class LibeuFinApiTest {
         }
     }
 
+    /**
+     * Tests DELETE /accounts/foo
+     */
+    @Test
+    fun deleteAccount() {
+        val db = initDb()
+        val ctx = getTestContext()
+        val adminCustomer = Customer(
+            "admin",
+            CryptoUtil.hashpw("pass"),
+            "CFO"
+        )
+        db.customerCreate(adminCustomer)
+        testApplication {
+            application {
+                corebankWebApp(db, ctx)
+            }
+            // account to delete doesn't exist.
+            client.delete("/accounts/foo") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.NotFound)
+            }
+            // account to delete is reserved.
+            client.delete("/accounts/admin") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.Forbidden)
+            }
+            // successful deletion
+            db.customerCreate(customerFoo).apply {
+                assert(this != null)
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
+            }
+            client.delete("/accounts/foo") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.NoContent)
+            }
+            // fail to delete, due to a non-zero balance.
+            db.customerCreate(customerBar).apply {
+                assert(this != null)
+                db.bankAccountCreate(genBankAccount(this!!)).apply {
+                    assert(this != null)
+                    val conn = 
DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+                    conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET 
balance.val = 1 WHERE bank_account_id = $this")
+                }
+            }
+            client.delete("/accounts/bar") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.PreconditionFailed)
+            }
+        }
+    }
+
     /**
      * Tests the GET /accounts endpoint.
      */
@@ -544,12 +607,12 @@ class LibeuFinApiTest {
             // foo account
             db.customerCreate(customerFoo).apply {
                 assert(this != null)
-                db.bankAccountCreate(genBankAccount(this!!)) != null
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
             }
             // bar account
             db.customerCreate(customerBar).apply {
                 assert(this != null)
-                db.bankAccountCreate(genBankAccount(this!!)) != null
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
             }
             // Two users registered, requesting all of them.
             client.get("/accounts") {
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index 158b8734..9be3de8b 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,6 +4,7 @@ import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
 import kotlinx.serialization.json.Json
+import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
@@ -151,14 +152,14 @@ class TalerApiTest {
         // Foo pays Bar (the exchange) twice.
         val reservePubOne = 
"5ZFS98S1K4Y083W95GVZK638TSRE44RABVASB3AFA3R95VCW17V0"
         val reservePubTwo = 
"TFBT5NEVT8D2GETZ4DRF7C69XZHKHJ15296HRGB1R5ARNK0SP8A0"
-        assert(db.bankTransactionCreate(genTx(reservePubOne)) == 
Database.BankTransactionResult.SUCCESS)
-        assert(db.bankTransactionCreate(genTx(reservePubTwo)) == 
Database.BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx(reservePubOne)) == 
BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx(reservePubTwo)) == 
BankTransactionResult.SUCCESS)
         // Should not show up in the taler wire gateway API history
-        assert(db.bankTransactionCreate(genTx("bogus foobar")) == 
Database.BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx("bogus foobar")) == 
BankTransactionResult.SUCCESS)
         // Bar pays Foo once, but that should not appear in the result.
         assert(
             db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId 
= 2)) ==
-                    Database.BankTransactionResult.SUCCESS
+                    BankTransactionResult.SUCCESS
         )
         // Bar expects two entries in the incoming history
         testApplication {
@@ -172,9 +173,9 @@ class TalerApiTest {
             val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText())
             assert(j.incoming_transactions.size == 2)
             // Testing ranges.
-            val mockReservePub = "X".repeat(52)
+            val mockReservePub = Base32Crockford.encode(ByteArray(32))
             for (i in 1..400)
-                assert(db.bankTransactionCreate(genTx(mockReservePub)) == 
Database.BankTransactionResult.SUCCESS)
+                assert(db.bankTransactionCreate(genTx(mockReservePub)) == 
BankTransactionResult.SUCCESS)
             // forward range:
             val range = 
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30")
 {
                 basicAuth("bar", "secret")
diff --git a/contrib/wallet-core b/contrib/wallet-core
index 1b5df6f0..c5a3cd4c 160000
--- a/contrib/wallet-core
+++ b/contrib/wallet-core
@@ -1 +1 @@
-Subproject commit 1b5df6f037a8d9e93a96e4c83f7e628023e4cd90
+Subproject commit c5a3cd4c50676c49fa6c67cbdeb609101c38e764
diff --git a/database-versioning/libeufin-bank-0001.sql 
b/database-versioning/libeufin-bank-0001.sql
index acd4f174..94475c99 100644
--- a/database-versioning/libeufin-bank-0001.sql
+++ b/database-versioning/libeufin-bank-0001.sql
@@ -91,6 +91,7 @@ CREATE TABLE IF NOT EXISTS bank_accounts
   ,internal_payto_uri TEXT NOT NULL UNIQUE
   ,owning_customer_id BIGINT NOT NULL UNIQUE -- UNIQUE enforces 1-1 map with 
customers
     REFERENCES customers(customer_id)
+    ON DELETE CASCADE
   ,is_public BOOLEAN DEFAULT FALSE NOT NULL -- privacy by default
   ,is_taler_exchange BOOLEAN DEFAULT FALSE NOT NULL
   ,last_nexus_fetch_row_id BIGINT
diff --git a/database-versioning/procedures.sql 
b/database-versioning/procedures.sql
index 652219ba..ad36cc2a 100644
--- a/database-versioning/procedures.sql
+++ b/database-versioning/procedures.sql
@@ -86,6 +86,57 @@ END $$;
 COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT)
   IS 'Update or insert configuration values';
 
+CREATE OR REPLACE FUNCTION customer_delete(
+  IN in_login TEXT,
+  OUT out_nx_customer BOOLEAN,
+  OUT out_balance_not_zero BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+my_customer_id BIGINT;
+my_balance_val INT8;
+my_balance_frac INT4;
+BEGIN
+-- check if login exists
+SELECT customer_id
+  INTO my_customer_id
+  FROM customers
+  WHERE login = in_login;
+IF NOT FOUND
+THEN
+  out_nx_customer=TRUE;
+  RETURN;
+END IF;
+out_nx_customer=FALSE;
+
+-- get the balance
+SELECT
+  (balance).val as balance_val,
+  (balance).frac as balance_frac
+  INTO
+    my_balance_val,
+    my_balance_frac
+  FROM bank_accounts
+  WHERE owning_customer_id = my_customer_id;
+IF NOT FOUND
+THEN
+  RAISE EXCEPTION 'Invariant failed: customer lacks bank account';
+END IF;
+-- check that balance is zero.
+IF my_balance_val != 0 OR my_balance_frac != 0
+THEN
+  out_balance_not_zero=TRUE;
+  RETURN;
+END IF;
+out_balance_not_zero=FALSE;
+
+-- actual deletion
+DELETE FROM customers WHERE login = in_login;
+END $$;
+COMMENT ON FUNCTION customer_delete(TEXT)
+  IS 'Deletes a customer (and its bank account via cascade) if the balance is 
zero';
+
 CREATE OR REPLACE FUNCTION taler_transfer(
   IN in_request_uid TEXT,
   IN in_wtid TEXT,
diff --git a/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt
index db0c269e..4f3dcabf 100644
--- a/util/src/main/kotlin/Encoding.kt
+++ b/util/src/main/kotlin/Encoding.kt
@@ -53,6 +53,10 @@ object Base32Crockford {
         return sb.toString()
     }
 
+    /**
+     * Decodes the input to its binary representation, throws
+     * net.taler.wallet.crypto.EncodingException on invalid encodings.
+     */
     fun decode(encoded: String, out: ByteArrayOutputStream) {
         val size = encoded.length
         var bitpos = 0

-- 
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]