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