commit 6024130ab85171528bff4f57d8ac31a1a06994d2
parent 0920cb07545501e54659f6a7ae94afbab86633f5
Author: Antoine A <>
Date: Thu, 26 Oct 2023 15:45:35 +0000
Remove unused columns, fix account creation and other improvements
Diffstat:
11 files changed, 433 insertions(+), 495 deletions(-)
diff --git a/bank/conf/test_bonus.conf b/bank/conf/test_bonus.conf
@@ -0,0 +1,9 @@
+[libeufin-bank]
+CURRENCY = KUDOS
+DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10
+REGISTRATION_BONUS_ENABLED = yes
+REGISTRATION_BONUS = KUDOS:10
+
+[libeufin-bankdb-postgres]
+SQL_DIR = $DATADIR/sql/
+CONFIG = postgresql:///libeufincheck
+\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -98,27 +98,21 @@ private fun Routing.coreBankTokenApi(db: Database) {
throw badRequest("Bad token duration: ${e.message}")
}
}
- val customerDbRow =
- db.customerGetFromLogin(login)?.customerId
- ?: throw internalServerError(
- "Could not get customer '$login' database row ID"
- )
- val token =
- BearerToken(
- bankCustomer = customerDbRow,
- content = tokenBytes,
- creationTime = creationTime,
- expirationTime = expirationTimestamp,
- scope = req.scope,
- isRefreshable = req.refreshable
- )
- if (!db.bearerTokenCreate(token))
- throw internalServerError("Failed at inserting new token in the database")
+ if (!db.bearerTokenCreate(
+ login = login,
+ content = tokenBytes,
+ creationTime = creationTime,
+ expirationTime = expirationTimestamp,
+ scope = req.scope,
+ isRefreshable = req.refreshable
+ )) {
+ throw internalServerError("Failed at inserting new token in the database")
+ }
call.respond(
- TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
- expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
- )
+ TokenSuccessResponse(
+ access_token = Base32Crockford.encode(tokenBytes),
+ expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
+ )
)
}
delete("/accounts/{USERNAME}/token") {
@@ -148,101 +142,42 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) {
val req = call.receive<RegisterAccountRequest>()
// Prohibit reserved usernames:
if (reservedAccounts.contains(req.username))
- throw forbidden(
- "Username '${req.username}' is reserved.",
- TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
- )
- // Checking idempotency.
- val maybeCustomerExists =
- db.customerGetFromLogin(
- req.username
- ) // Can be null if previous call crashed before completion.
- val maybeHasBankAccount =
- maybeCustomerExists.run {
- if (this == null) return@run null
- db.bankAccountGetFromOwnerId(this.customerId)
- }
- val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri())
- if (maybeCustomerExists != null && maybeHasBankAccount != null) {
- logger.debug(
- "Registering username was found: ${maybeCustomerExists.login}"
- ) // Checking _all_ the details are the same.
- val isIdentic =
- maybeCustomerExists.name == req.name &&
- maybeCustomerExists.email == req.challenge_contact_data?.email &&
- maybeCustomerExists.phone == req.challenge_contact_data?.phone &&
- maybeCustomerExists.cashoutPayto == req.cashout_payto_uri &&
- CryptoUtil.checkpw(req.password, maybeCustomerExists.passwordHash) &&
- maybeHasBankAccount.isPublic == req.is_public &&
- maybeHasBankAccount.isTalerExchange == req.is_taler_exchange &&
- maybeHasBankAccount.internalPaytoUri.canonical ==
- internalPayto.canonical
- if (isIdentic) {
- call.respond(HttpStatusCode.Created)
- return@post
- }
- throw conflict(
- "Idempotency check failed.",
- TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
+ throw forbidden(
+ "Username '${req.username}' is reserved.",
+ TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
)
- }
- // From here: fresh user being added.
- val (_, newBankAccountId) = db.accountCreate(
- login = req.username,
- name = req.name,
- email = req.challenge_contact_data?.email,
- phone = req.challenge_contact_data?.phone,
- cashoutPayto =
- req.cashout_payto_uri, // Following could be gone, if included in
- // cashout_payto_uri
- cashoutCurrency = ctx.fiatCurrency,
- passwordHash = CryptoUtil.hashpw(req.password),
- internalPaytoUri = internalPayto,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = ctx.defaultCustomerDebtLimit
+ val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri())
+ val result = db.accountCreate(
+ login = req.username,
+ name = req.name,
+ email = req.challenge_contact_data?.email,
+ phone = req.challenge_contact_data?.phone,
+ cashoutPayto = req.cashout_payto_uri,
+ password = req.password,
+ internalPaytoUri = internalPayto,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = ctx.defaultCustomerDebtLimit,
+ bonus = if (ctx.registrationBonusEnabled && !req.is_taler_exchange) ctx.registrationBonus
+ else null
)
- // The new account got created, now optionally award the registration
- // bonus to it.
- val bonusAmount =
- if (ctx.registrationBonusEnabled && !req.is_taler_exchange) ctx.registrationBonus
- else null
- if (bonusAmount != null) {
- val adminCustomer =
- db.customerGetFromLogin("admin")
- ?: throw internalServerError("Admin customer not found")
- val adminBankAccount =
- db.bankAccountGetFromOwnerId(adminCustomer.customerId)
- ?: throw internalServerError("Admin bank account not found")
- val adminPaysBonus =
- BankInternalTransaction(
- creditorAccountId = newBankAccountId,
- debtorAccountId = adminBankAccount.bankAccountId,
- amount = bonusAmount,
- subject = "Registration bonus.",
- transactionDate = Instant.now()
- )
- when (db.bankTransactionCreate(adminPaysBonus)) {
- 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.BALANCE_INSUFFICIENT ->
- throw internalServerError(
- "Bonus impossible: admin has insufficient balance."
- )
- BankTransactionResult.SAME_ACCOUNT ->
- throw internalServerError("Bonus impossible: admin should not be creditor.")
- BankTransactionResult.SUCCESS -> {
- /* continue the execution */
- }
- }
+ when (result) {
+ CustomerCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ CustomerCreationResult.CONFLICT_LOGIN -> throw conflict(
+ "Customer username reuse '${req.username}'",
+ TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
+ )
+ CustomerCreationResult.CONFLICT_PAY_TO -> throw conflict(
+ "Bank internalPayToUri reuse '${internalPayto.canonical}'",
+ TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
+ )
+ CustomerCreationResult.SUCCESS -> call.respond(HttpStatusCode.Created)
}
- call.respond(HttpStatusCode.Created)
}
delete("/accounts/{USERNAME}") {
val (login, _) =
@@ -277,57 +212,25 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) {
val (login, isAdmin) = call.authCheck(db, TokenScope.readwrite, withAdmin = true)
// admin is not allowed itself to change its own details.
if (login == "admin") throw forbidden("admin account not patchable")
- // authentication OK, go on.
+
val req = call.receive<AccountReconfiguration>()
- /**
- * This object holds the details of the customer that's affected by this operation, as it
- * MAY differ from the one being authenticated. This typically happens when admin did the
- * request.
- */
- val accountCustomer =
- db.customerGetFromLogin(login)
- ?: throw notFound(
- "Account $login not found",
- talerEc = TalerErrorCode.TALER_EC_END // FIXME, define EC.
- )
- // Check if a non-admin user tried to change their legal name
- if (!isAdmin && (req.name != null) && (req.name != accountCustomer.name))
+ val res = db.accountReconfig(
+ login = login,
+ name = req.name,
+ cashoutPayto = req.cashout_address,
+ emailAddress = req.challenge_contact_data?.email,
+ isTalerExchange = req.is_exchange,
+ phoneNumber = req.challenge_contact_data?.phone,
+ isAdmin = isAdmin
+ )
+ when (res) {
+ CustomerPatchResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ CustomerPatchResult.ACCOUNT_NOT_FOUND -> throw notFound(
+ "Customer '$login' not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ CustomerPatchResult.CONFLICT_LEGAL_NAME ->
throw forbidden("non-admin user cannot change their legal name")
- // Preventing identical data to be overridden.
- val bankAccount =
- db.bankAccountGetFromOwnerId(accountCustomer.customerId)
- ?: throw internalServerError(
- "Customer '${accountCustomer.login}' lacks bank account."
- )
- if ((req.is_exchange == bankAccount.isTalerExchange) &&
- (req.cashout_address == accountCustomer.cashoutPayto) &&
- (req.name == accountCustomer.name) &&
- (req.challenge_contact_data?.phone == accountCustomer.phone) &&
- (req.challenge_contact_data?.email == accountCustomer.email)
- ) {
- call.respond(HttpStatusCode.NoContent)
- return@patch
- }
- val dbRes =
- db.accountReconfig(
- login = accountCustomer.login,
- name = req.name,
- cashoutPayto = req.cashout_address,
- emailAddress = req.challenge_contact_data?.email,
- isTalerExchange = req.is_exchange,
- phoneNumber = req.challenge_contact_data?.phone
- )
- when (dbRes) {
- AccountReconfigDBResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
- AccountReconfigDBResult.CUSTOMER_NOT_FOUND -> {
- // Rare case. Only possible if a deletion happened before the flow reaches here.
- logger.warn("Authenticated customer wasn't found any more in the database")
- throw notFound("Customer not found", TalerErrorCode.TALER_EC_END) // FIXME: needs EC
- }
- AccountReconfigDBResult.BANK_ACCOUNT_NOT_FOUND -> {
- // Bank's fault: no customer should lack a bank account.
- throw internalServerError("Customer '${accountCustomer.login}' lacks bank account")
- }
}
}
patch("/accounts/{USERNAME}/auth") {
@@ -370,44 +273,11 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) {
}
get("/accounts/{USERNAME}") {
val (login, _) = call.authCheck(db, TokenScope.readonly, withAdmin = true)
- val customerData =
- db.customerGetFromLogin(login)
- ?: throw notFound(
- "Customer '$login' not found in the database.",
- talerEc = TalerErrorCode.TALER_EC_END
- )
- val bankAccountData =
- db.bankAccountGetFromOwnerId(customerData.customerId)
- ?: throw internalServerError(
- "Customer '$login' had no bank account despite they are customer.'"
- )
- val balance =
- Balance(
- amount = bankAccountData.balance
- ?: throw internalServerError(
- "Account '${customerData.login}' lacks balance!"
- ),
- credit_debit_indicator =
- if (bankAccountData.hasDebt) {
- CorebankCreditDebitInfo.debit
- } else {
- CorebankCreditDebitInfo.credit
- }
- )
- call.respond(
- AccountData(
- name = customerData.name,
- balance = balance,
- debit_threshold = bankAccountData.maxDebt,
- payto_uri = bankAccountData.internalPaytoUri,
- contact_data =
- ChallengeContactData(
- email = customerData.email,
- phone = customerData.phone
- ),
- cashout_payto_uri = customerData.cashoutPayto,
- )
+ val account = db.accountDataFromLogin(login) ?: throw notFound(
+ "Customer '$login' not found in the database.",
+ talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
+ call.respond(account)
}
}
@@ -418,7 +288,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
val bankAccount = call.bankAccount(db)
val history: List<BankAccountTransactionInfo> =
- db.bankPoolHistory(params, bankAccount.bankAccountId!!)
+ db.bankPoolHistory(params, bankAccount.bankAccountId)
call.respond(BankAccountTransactionsResponse(history))
}
get("/accounts/{USERNAME}/transactions/{T_ID}") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -172,57 +172,40 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
}
}
- suspend fun customerGetFromLogin(login: String): Customer? = conn { conn ->
- val stmt = conn.prepareStatement("""
- SELECT
- customer_id,
- password_hash,
- name,
- email,
- phone,
- cashout_payto,
- cashout_currency
- FROM customers
- WHERE login=?
- """)
- stmt.setString(1, login)
- stmt.oneOrNull {
- Customer(
- login = login,
- passwordHash = it.getString("password_hash"),
- name = it.getString("name"),
- phone = it.getString("phone"),
- email = it.getString("email"),
- cashoutCurrency = it.getString("cashout_currency"),
- cashoutPayto = it.getString("cashout_payto"),
- customerId = it.getLong("customer_id")
- )
- }
- }
-
- // Possibly more "customerGetFrom*()" to come.
-
// BEARER TOKEN
- suspend fun bearerTokenCreate(token: BearerToken): Boolean = conn { conn ->
+ suspend fun bearerTokenCreate(
+ login: String,
+ content: ByteArray,
+ creationTime: Instant,
+ expirationTime: Instant,
+ scope: TokenScope,
+ isRefreshable: Boolean
+ ): Boolean = conn { conn ->
+ val bankCustomer = conn.prepareStatement("""
+ SELECT customer_id FROM customers WHERE login=?
+ """).run {
+ setString(1, login)
+ oneOrNull { it.getLong(1) }!!
+ }
val stmt = conn.prepareStatement("""
- INSERT INTO bearer_tokens
- (content,
+ INSERT INTO bearer_tokens (
+ content,
creation_time,
expiration_time,
scope,
bank_customer,
is_refreshable
- ) VALUES
- (?, ?, ?, ?::token_scope_enum, ?, ?)
+ ) VALUES (?, ?, ?, ?::token_scope_enum, ?, ?)
""")
- stmt.setBytes(1, token.content)
- stmt.setLong(2, token.creationTime.toDbMicros() ?: throw faultyTimestampByBank())
- stmt.setLong(3, token.expirationTime.toDbMicros() ?: throw faultyDurationByClient())
- stmt.setString(4, token.scope.name)
- stmt.setLong(5, token.bankCustomer)
- stmt.setBoolean(6, token.isRefreshable)
+ stmt.setBytes(1, content)
+ stmt.setLong(2, creationTime.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(3, expirationTime.toDbMicros() ?: throw faultyDurationByClient())
+ stmt.setString(4, scope.name)
+ stmt.setLong(5, bankCustomer)
+ stmt.setBoolean(6, isRefreshable)
stmt.executeUpdateViolation()
}
+
suspend fun bearerTokenGet(token: ByteArray): BearerToken? = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT
@@ -265,63 +248,171 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
suspend fun accountCreate(
login: String,
- passwordHash: String,
+ password: String,
name: String,
email: String? = null,
phone: String? = null,
- cashoutPayto: String? = null,
- cashoutCurrency: String? = null,
+ cashoutPayto: IbanPayTo? = null,
internalPaytoUri: IbanPayTo,
isPublic: Boolean,
isTalerExchange: Boolean,
- maxDebt: TalerAmount
- ): Pair<Long, Long> = conn { it ->
+ maxDebt: TalerAmount,
+ bonus: TalerAmount?
+ ): CustomerCreationResult = conn { it ->
it.transaction { conn ->
- val customerId = conn.prepareStatement("""
- INSERT INTO customers (
- login
- ,password_hash
- ,name
- ,email
- ,phone
- ,cashout_payto
- ,cashout_currency
- )
- VALUES (?, ?, ?, ?, ?, ?, ?)
- RETURNING customer_id
- """
- ).run {
- setString(1, login)
- setString(2, passwordHash)
- setString(3, name)
- setString(4, email)
- setString(5, phone)
- setString(6, cashoutPayto)
- setString(7, cashoutCurrency)
- oneOrNull { it.getLong("customer_id") }
- ?: throw internalServerError("SQL RETURNING gave no customer_id.")
+ val idempotent = conn.prepareStatement("""
+ SELECT password_hash, name=?
+ AND email IS NOT DISTINCT FROM ?
+ AND phone IS NOT DISTINCT FROM ?
+ AND cashout_payto IS NOT DISTINCT FROM ?
+ AND internal_payto_uri=?
+ AND is_public=?
+ AND is_taler_exchange=?
+ FROM customers
+ JOIN bank_accounts
+ ON customer_id=owning_customer_id
+ WHERE login=?
+ """).run {
+ setString(1, name)
+ setString(2, email)
+ setString(3, phone)
+ setString(4, cashoutPayto?.canonical)
+ setString(5, internalPaytoUri.canonical)
+ setBoolean(6, isPublic)
+ setBoolean(7, isTalerExchange)
+ setString(8, login)
+ oneOrNull {
+ CryptoUtil.checkpw(password, it.getString(1)) && it.getBoolean(2)
+ }
+ }
+ if (idempotent != null) {
+ if (idempotent) {
+ CustomerCreationResult.SUCCESS
+ } else {
+ CustomerCreationResult.CONFLICT_LOGIN
+ }
+ } else {
+ val customerId = conn.prepareStatement("""
+ INSERT INTO customers (
+ login
+ ,password_hash
+ ,name
+ ,email
+ ,phone
+ ,cashout_payto
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ RETURNING customer_id
+ """
+ ).run {
+ setString(1, login)
+ setString(2, CryptoUtil.hashpw(password))
+ setString(3, name)
+ setString(4, email)
+ setString(5, phone)
+ setString(6, cashoutPayto?.canonical)
+ oneOrNull { it.getLong("customer_id") }!!
+ }
+
+ conn.prepareStatement("""
+ INSERT INTO bank_accounts(
+ internal_payto_uri
+ ,owning_customer_id
+ ,is_public
+ ,is_taler_exchange
+ ,max_debt
+ ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount)
+ """).run {
+ setString(1, internalPaytoUri.canonical)
+ setLong(2, customerId)
+ setBoolean(3, isPublic)
+ setBoolean(4, isTalerExchange)
+ setLong(5, maxDebt.value)
+ setInt(6, maxDebt.frac)
+ if (!executeUpdateViolation()) {
+ conn.rollback()
+ return@transaction CustomerCreationResult.CONFLICT_PAY_TO
+ }
+ }
+
+ if (bonus != null) {
+ conn.prepareStatement("""
+ SELECT out_balance_insufficient
+ FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,?,?,?)
+ """).run {
+ setString(1, internalPaytoUri.canonical)
+ setLong(2, bonus.value)
+ setInt(3, bonus.frac)
+ setLong(4, Instant.now().toDbMicros() ?: throw faultyTimestampByBank())
+ setString(5, "not used") // ISO20022
+ setString(6, "not used") // ISO20022
+ setString(7, "not used") // ISO20022
+ executeQuery().use {
+ when {
+ !it.next() -> throw internalServerError("Bank transaction didn't properly return")
+ it.getBoolean("out_balance_insufficient") -> {
+ conn.rollback()
+ CustomerCreationResult.BALANCE_INSUFFICIENT
+ }
+ else -> CustomerCreationResult.SUCCESS
+ }
+ }
+ }
+ } else {
+ CustomerCreationResult.SUCCESS
+ }
}
-
- val stmt = conn.prepareStatement("""
- INSERT INTO bank_accounts
- (internal_payto_uri
- ,owning_customer_id
- ,is_public
- ,is_taler_exchange
- ,max_debt
- )
- VALUES (?, ?, ?, ?, (?, ?)::taler_amount)
- RETURNING bank_account_id;
- """)
- stmt.setString(1, internalPaytoUri.canonical)
- stmt.setLong(2, customerId)
- stmt.setBoolean(3, isPublic)
- stmt.setBoolean(4, isTalerExchange)
- stmt.setLong(5, maxDebt.value)
- stmt.setInt(6, maxDebt.frac)
- val bankId = stmt.oneOrNull { it.getLong("bank_account_id") }
- ?: throw internalServerError("SQL RETURNING gave no bank_account_id.")
- Pair(customerId, bankId)
+ }
+ }
+
+ suspend fun accountDataFromLogin(
+ login: String
+ ): AccountData? = conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ name
+ ,email
+ ,phone
+ ,cashout_payto
+ ,internal_payto_uri
+ ,(balance).val AS balance_val
+ ,(balance).frac AS balance_frac
+ ,has_debt
+ ,(max_debt).val AS max_debt_val
+ ,(max_debt).frac AS max_debt_frac
+ FROM customers
+ JOIN bank_accounts
+ ON customer_id=owning_customer_id
+ WHERE login=?
+ """)
+ stmt.setString(1, login)
+ stmt.oneOrNull {
+ AccountData(
+ name = it.getString("name"),
+ contact_data = ChallengeContactData(
+ email = it.getString("email"),
+ phone = it.getString("phone")
+ ),
+ cashout_payto_uri = it.getString("cashout_payto")?.run(::IbanPayTo),
+ payto_uri = IbanPayTo(it.getString("internal_payto_uri")),
+ balance = Balance(
+ amount = TalerAmount(
+ it.getLong("balance_val"),
+ it.getInt("balance_frac"),
+ getCurrency()
+ ),
+ credit_debit_indicator =
+ if (it.getBoolean("has_debt")) {
+ CorebankCreditDebitInfo.debit
+ } else {
+ CorebankCreditDebitInfo.credit
+ }
+ ),
+ debit_threshold = TalerAmount(
+ value = it.getLong("max_debt_val"),
+ frac = it.getInt("max_debt_frac"),
+ getCurrency()
+ )
+ )
}
}
@@ -342,33 +433,34 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
suspend fun accountReconfig(
login: String,
name: String?,
- cashoutPayto: String?,
+ cashoutPayto: IbanPayTo?,
phoneNumber: String?,
emailAddress: String?,
- isTalerExchange: Boolean?
- ): AccountReconfigDBResult = conn { conn ->
+ isTalerExchange: Boolean?,
+ isAdmin: Boolean
+ ): CustomerPatchResult = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT
- out_nx_customer,
- out_nx_bank_account
- FROM account_reconfig(?, ?, ?, ?, ?, ?)
+ out_not_found,
+ out_legal_name_change
+ FROM account_reconfig(?, ?, ?, ?, ?, ?, ?)
""")
stmt.setString(1, login)
stmt.setString(2, name)
stmt.setString(3, phoneNumber)
stmt.setString(4, emailAddress)
- stmt.setString(5, cashoutPayto)
-
+ stmt.setString(5, cashoutPayto?.canonical)
if (isTalerExchange == null)
stmt.setNull(6, Types.NULL)
else stmt.setBoolean(6, isTalerExchange)
+ stmt.setBoolean(7, isAdmin)
stmt.executeQuery().use {
when {
!it.next() -> throw internalServerError("accountReconfig() returned nothing")
- it.getBoolean("out_nx_customer") -> AccountReconfigDBResult.CUSTOMER_NOT_FOUND
- it.getBoolean("out_nx_bank_account") -> AccountReconfigDBResult.BANK_ACCOUNT_NOT_FOUND
- else -> AccountReconfigDBResult.SUCCESS
+ it.getBoolean("out_not_found") -> CustomerPatchResult.ACCOUNT_NOT_FOUND
+ it.getBoolean("out_legal_name_change") -> CustomerPatchResult.CONFLICT_LEGAL_NAME
+ else -> CustomerPatchResult.SUCCESS
}
}
}
@@ -483,11 +575,10 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
suspend fun bankAccountGetFromOwnerId(ownerId: Long): BankAccount? = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT
- internal_payto_uri
+ internal_payto_uri
,owning_customer_id
,is_public
,is_taler_exchange
- ,last_nexus_fetch_row_id
,(balance).val AS balance_val
,(balance).frac AS balance_frac
,has_debt
@@ -507,7 +598,6 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
it.getInt("balance_frac"),
getCurrency()
),
- lastNexusFetchRowId = it.getLong("last_nexus_fetch_row_id"),
owningCustomerId = it.getLong("owning_customer_id"),
hasDebt = it.getBoolean("has_debt"),
isTalerExchange = it.getBoolean("is_taler_exchange"),
@@ -530,7 +620,6 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
,internal_payto_uri
,is_public
,is_taler_exchange
- ,last_nexus_fetch_row_id
,(balance).val AS balance_val
,(balance).frac AS balance_frac
,has_debt
@@ -551,7 +640,6 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
it.getInt("balance_frac"),
getCurrency()
),
- lastNexusFetchRowId = it.getLong("last_nexus_fetch_row_id"),
owningCustomerId = it.getLong("owning_customer_id"),
hasDebt = it.getBoolean("has_debt"),
isTalerExchange = it.getBoolean("is_taler_exchange"),
@@ -560,6 +648,7 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
frac = it.getInt("max_debt_frac"),
getCurrency()
),
+ isPublic = it.getBoolean("is_public"),
bankAccountId = it.getLong("bank_account_id")
)
}
@@ -741,6 +830,7 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
,end_to_end_id
,direction
,bank_account_id
+ ,bank_transaction_id
FROM bank_account_transactions
WHERE bank_transaction_id=?
""")
@@ -762,7 +852,8 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
bankAccountId = it.getLong("bank_account_id"),
paymentInformationId = it.getString("payment_information_id"),
subject = it.getString("subject"),
- transactionDate = it.getLong("transaction_date").microsToJavaInstant() ?: throw faultyTimestampByBank()
+ transactionDate = it.getLong("transaction_date").microsToJavaInstant() ?: throw faultyTimestampByBank(),
+ dbRowId = it.getLong("bank_transaction_id")
)
}
}
@@ -1530,6 +1621,21 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
}
}
+/** Result status of customer account creation */
+enum class CustomerCreationResult {
+ SUCCESS,
+ CONFLICT_LOGIN,
+ CONFLICT_PAY_TO,
+ BALANCE_INSUFFICIENT,
+}
+
+/** Result status of customer account patch */
+enum class CustomerPatchResult {
+ ACCOUNT_NOT_FOUND,
+ CONFLICT_LEGAL_NAME,
+ SUCCESS
+}
+
/** Result status of customer account deletion */
enum class CustomerDeletionResult {
SUCCESS,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -410,14 +410,21 @@ class ExchangeUrl {
}
}
+sealed class PaytoUri {
+ abstract val amount: TalerAmount?
+ abstract val message: String?
+ abstract val receiverName: String?
+}
+
+// TODO x-taler-bank Payto
@Serializable(with = IbanPayTo.Serializer::class)
-class IbanPayTo {
+class IbanPayTo: PaytoUri {
val parsed: URI
val canonical: String
- val amount: TalerAmount?
- val message: String?
- val receiverName: String?
+ override val amount: TalerAmount?
+ override val message: String?
+ override val receiverName: String?
constructor(raw: String) {
parsed = URI(raw)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -82,7 +82,7 @@ data class RegisterAccountRequest(
val is_taler_exchange: Boolean = false,
val challenge_contact_data: ChallengeContactData? = null,
// External bank account where to send cashout amounts.
- val cashout_payto_uri: String? = null,
+ val cashout_payto_uri: IbanPayTo? = null,
// Bank account internal to Libeufin-Bank.
val internal_payto_uri: IbanPayTo? = null
)
@@ -124,30 +124,6 @@ data class MonitorWithCashout(
) : MonitorResponse()
/**
- * Convenience type to hold customer data, typically after such
- * data gets fetched from the database. It is also used to _insert_
- * customer data to the database.
- */
-data class Customer(
- val login: String,
- val passwordHash: String,
- val name: String,
- val customerId: Long,
- val email: String? = null,
- val phone: String? = null,
- /**
- * External bank account where customers send
- * their cashout amounts.
- */
- val cashoutPayto: String? = null,
- /**
- * Currency of the external bank account where
- * customers send their cashout amounts.
- */
- val cashoutCurrency: String? = null
-)
-
-/**
* Convenience type to get and set bank account information
* from/to the database.
*/
@@ -156,20 +132,9 @@ data class BankAccount(
// Database row ID of the customer that owns this bank account.
val owningCustomerId: Long,
val bankAccountId: Long,
- val isPublic: Boolean = false,
- val isTalerExchange: Boolean = false,
- /**
- * Because bank accounts MAY be funded by an external currency,
- * local bank accounts need to query Nexus, in order to find this
- * out. This field is a pointer to the latest incoming payment that
- * was contained in a Nexus history response.
- *
- * Typically, the 'admin' bank account uses this field, in order
- * to initiate Taler withdrawals that depend on an external currency
- * being wired by wallet owners.
- */
- val lastNexusFetchRowId: Long = 0L,
- val balance: TalerAmount? = null, // null when a new bank account gets created.
+ val isPublic: Boolean,
+ val isTalerExchange: Boolean,
+ val balance: TalerAmount,
val hasDebt: Boolean,
val maxDebt: TalerAmount
)
@@ -253,7 +218,7 @@ data class BankAccountTransaction(
*/
val bankAccountId: Long,
// Null if this type is used to _create_ one transaction.
- val dbRowId: Long? = null,
+ val dbRowId: Long,
// Following are ISO20022 specific.
val accountServicerReference: String,
val paymentInformationId: String,
@@ -267,9 +232,9 @@ data class BankAccountTransaction(
data class TalerWithdrawalOperation(
val withdrawalUuid: UUID,
val amount: TalerAmount,
- val selectionDone: Boolean = false,
- val aborted: Boolean = false,
- val confirmationDone: Boolean = false,
+ val selectionDone: Boolean,
+ val aborted: Boolean,
+ val confirmationDone: Boolean,
val reservePub: EddsaPublicKey?,
val selectedExchangePayto: IbanPayTo?,
val walletBankAccount: Long
@@ -349,7 +314,7 @@ data class AccountData(
val payto_uri: IbanPayTo,
val debit_threshold: TalerAmount,
val contact_data: ChallengeContactData? = null,
- val cashout_payto_uri: String? = null,
+ val cashout_payto_uri: IbanPayTo? = null,
)
/**
@@ -660,34 +625,7 @@ data class AccountPasswordChange(
@Serializable
data class AccountReconfiguration(
val challenge_contact_data: ChallengeContactData?,
- val cashout_address: String?,
+ val cashout_address: IbanPayTo?,
val name: String?,
val is_exchange: Boolean?
-)
-
-/**
- * This type expresses the outcome of updating the account
- * data in the database.
- */
-enum class AccountReconfigDBResult {
- /**
- * This indicates that despite the customer row was
- * found in the database, its related bank account was not.
- * This condition is a hard failure of the bank, since
- * every customer must have one (and only one) bank account.
- */
- BANK_ACCOUNT_NOT_FOUND,
-
- /**
- * The customer row wasn't found in the database. This error
- * should be rare, as the client got authenticated in the first
- * place, before the handler could try the reconfiguration in
- * the database.
- */
- CUSTOMER_NOT_FOUND,
-
- /**
- * Reconfiguration successful.
- */
- SUCCESS
-}
-\ No newline at end of file
+)
+\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -221,30 +221,28 @@ data class CashoutRateParams(
* It returns false in case of problems, true otherwise.
*/
suspend fun maybeCreateAdminAccount(db: Database, ctx: BankConfig, pw: String? = null): Boolean {
- val maybeAdminCustomer = db.customerGetFromLogin("admin")
- if (maybeAdminCustomer == null) {
- logger.debug("Creating admin's account")
- var pwStr = pw;
- if (pwStr == null) {
- val pwBuf = ByteArray(32)
- Random().nextBytes(pwBuf)
- pwStr = String(pwBuf, Charsets.UTF_8)
- }
-
-
- db.accountCreate(
- login = "admin",
- /**
- * Hashing the password helps to avoid the "password not hashed"
- * error, in case the admin tries to authenticate.
- */
- passwordHash = CryptoUtil.hashpw(pwStr),
- name = "Bank administrator",
- internalPaytoUri = IbanPayTo(genIbanPaytoUri()),
- isPublic = false,
- isTalerExchange = false,
- maxDebt = ctx.defaultAdminDebtLimit
- )
+ logger.debug("Creating admin's account")
+ var pwStr = pw;
+ if (pwStr == null) {
+ val pwBuf = ByteArray(32)
+ Random().nextBytes(pwBuf)
+ pwStr = String(pwBuf, Charsets.UTF_8)
+ }
+
+ val res = db.accountCreate(
+ login = "admin",
+ password = pwStr,
+ name = "Bank administrator",
+ internalPaytoUri = IbanPayTo(genIbanPaytoUri()),
+ isPublic = false,
+ isTalerExchange = false,
+ maxDebt = ctx.defaultAdminDebtLimit,
+ bonus = null
+ )
+ return when (res) {
+ CustomerCreationResult.BALANCE_INSUFFICIENT -> false
+ CustomerCreationResult.CONFLICT_LOGIN -> true
+ CustomerCreationResult.CONFLICT_PAY_TO -> false
+ CustomerCreationResult.SUCCESS -> true
}
- return true
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -178,7 +178,7 @@ class CoreBankAccountsMgmtApiTest {
client.post("/accounts") {
jsonBody(req)
}.assertCreated()
- // Testing idempotency.
+ // Testing idempotency
client.post("/accounts") {
jsonBody(req)
}.assertCreated()
@@ -202,6 +202,55 @@ class CoreBankAccountsMgmtApiTest {
})
}.assertForbidden().assertErr(TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT)
}
+
+ // Testing login conflict
+ client.post("/accounts") {
+ jsonBody(json(req) {
+ "name" to "Foo"
+ })
+ }.assertConflict()
+ // Testing payto conflict
+ client.post("/accounts") {
+ jsonBody(json(req) {
+ "username" to "bar"
+ })
+ }.assertConflict()
+ client.get("/accounts/bar") {
+ basicAuth("admin", "admin-password")
+ }.assertNotFound().assertErr(TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT)
+ }
+
+ // Test account created with bonus
+ @Test
+ fun createAccountBonusTest() = bankSetup(conf = "test_bonus.conf") { _ ->
+ val req = json {
+ "username" to "foo"
+ "password" to "xyz"
+ "name" to "Mallory"
+ }
+
+ // Check ok
+ client.post("/accounts") {
+ basicAuth("admin", "admin-password")
+ jsonBody(req)
+ }.assertCreated()
+ client.get("/accounts/foo") {
+ basicAuth("admin", "admin-password")
+ }.assertOk().run {
+ val obj: AccountData = Json.decodeFromString(bodyAsText())
+ assertEquals(TalerAmount("KUDOS:10"), obj.balance.amount)
+ }
+
+ // Check unsufficient funs
+ client.post("/accounts") {
+ basicAuth("admin", "admin-password")
+ jsonBody(json(req) {
+ "username" to "bar"
+ })
+ }.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT)
+ client.get("/accounts/bar") {
+ basicAuth("admin", "admin-password")
+ }.assertNotFound().assertErr(TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT)
}
// Test admin-only account creation
@@ -280,10 +329,10 @@ class CoreBankAccountsMgmtApiTest {
// PATCH /accounts/USERNAME
@Test
- fun accountReconfig() = bankSetup { db ->
+ fun accountReconfig() = bankSetup { _ ->
// Successful attempt now.
val req = json {
- "cashout_address" to "payto://new-cashout-address"
+ "cashout_address" to IbanPayTo(genIbanPaytoUri()).canonical
"challenge_contact_data" to json {
"email" to "new@example.com"
"phone" to "+987"
@@ -300,10 +349,11 @@ class CoreBankAccountsMgmtApiTest {
jsonBody(req)
}.assertNoContent()
+ val cashout = IbanPayTo(genIbanPaytoUri())
val nameReq = json {
"login" to "foo"
"name" to "Another Foo"
- "cashout_address" to "payto://cashout"
+ "cashout_address" to cashout.canonical
"challenge_contact_data" to json {
"phone" to "+99"
"email" to "foo@example.com"
@@ -326,11 +376,10 @@ class CoreBankAccountsMgmtApiTest {
}.assertOk().run {
val obj: AccountData = Json.decodeFromString(bodyAsText())
assertEquals("Another Foo", obj.name)
- assertEquals("payto://cashout", obj.cashout_payto_uri)
+ assertEquals(cashout.canonical, obj.cashout_payto_uri?.canonical)
assertEquals("+99", obj.contact_data?.phone)
assertEquals("foo@example.com", obj.contact_data?.email)
}
-
}
// PATCH /accounts/USERNAME/auth
@@ -780,7 +829,6 @@ class CoreBankWithdrawalApiTest {
basicAuth("merchant", "merchant-password")
jsonBody(json { "amount" to "KUDOS:90" })
}.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT)
-
}
// GET /withdrawals/withdrawal_id
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
@@ -50,23 +50,10 @@ class DatabaseTest {
// Testing the helper that creates the admin account.
@Test
fun createAdminTest() = setup { db, ctx ->
- // No admin accounts is expected.
- val noAdminCustomer = db.customerGetFromLogin("admin")
- assert(noAdminCustomer == null)
- // Now creating one.
+ // Create admin account
assert(maybeCreateAdminAccount(db, ctx))
- // Now expecting one.
- val yesAdminCustomer = db.customerGetFromLogin("admin")
- assert(yesAdminCustomer != null)
- // Expecting also its _bank_ account.
- assert(db.bankAccountGetFromOwnerId(yesAdminCustomer!!.customerId) != null)
- // Checking idempotency.
+ // Checking idempotency
assert(maybeCreateAdminAccount(db, ctx))
- // Checking that the random password blocks a login.
- assert(!CryptoUtil.checkpw(
- "likely-wrong",
- yesAdminCustomer.passwordHash
- ))
}
}
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
@@ -38,32 +38,35 @@ fun bankSetup(
) {
setup(conf) { db, ctx ->
// Creating the exchange and merchant accounts first.
- assertNotNull(db.accountCreate(
+ assertEquals(CustomerCreationResult.SUCCESS, db.accountCreate(
login = "merchant",
- passwordHash = CryptoUtil.hashpw("merchant-password"),
+ password = "merchant-password",
name = "Merchant",
internalPaytoUri = IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"),
maxDebt = TalerAmount(10, 1, "KUDOS"),
isTalerExchange = false,
- isPublic = false
+ isPublic = false,
+ bonus = null
))
- assertNotNull(db.accountCreate(
+ assertEquals(CustomerCreationResult.SUCCESS, db.accountCreate(
login = "exchange",
- passwordHash = CryptoUtil.hashpw("exchange-password"),
+ password = "exchange-password",
name = "Exchange",
internalPaytoUri = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"),
maxDebt = TalerAmount(10, 1, "KUDOS"),
isTalerExchange = true,
- isPublic = false
+ isPublic = false,
+ bonus = null
))
- assertNotNull(db.accountCreate(
+ assertEquals(CustomerCreationResult.SUCCESS, db.accountCreate(
login = "customer",
- passwordHash = CryptoUtil.hashpw("customer-password"),
+ password = "customer-password",
name = "Customer",
internalPaytoUri = IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ"),
maxDebt = TalerAmount(10, 1, "KUDOS"),
isTalerExchange = false,
- isPublic = false
+ isPublic = false,
+ bonus = null
))
// Create admin account
assert(maybeCreateAdminAccount(db, ctx, "admin-password"))
diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql
@@ -61,8 +61,7 @@ CREATE TABLE IF NOT EXISTS customers
,name TEXT
,email TEXT
,phone TEXT
- ,cashout_payto TEXT -- here because has no business meaning inside libeufin-bank
- ,cashout_currency TEXT
+ ,cashout_payto TEXT
);
COMMENT ON COLUMN customers.cashout_payto
@@ -97,7 +96,6 @@ CREATE TABLE IF NOT EXISTS bank_accounts
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
,balance taler_amount DEFAULT (0, 0)
,max_debt taler_amount DEFAULT (0, 0)
,has_debt BOOLEAN NOT NULL DEFAULT FALSE
@@ -112,11 +110,6 @@ one bank account for one user, and additionally the bank
account label matches always the login.';
COMMENT ON COLUMN bank_accounts.has_debt
IS 'When true, the balance is negative';
-COMMENT ON COLUMN bank_accounts.last_nexus_fetch_row_id
- IS 'Keeps the ID of the last incoming payment that was learnt
-from Nexus. For that reason, this ID is stored verbatim as
-it was returned by Nexus. It helps to build queries to Nexus
-that needs this value as a parameter.';
COMMENT ON COLUMN bank_accounts.is_public
IS 'Indicates whether the bank account history
@@ -161,29 +154,22 @@ COMMENT ON COLUMN bank_account_transactions.bank_account_id
-- start of: cashout management
CREATE TABLE IF NOT EXISTS cashout_operations
- (cashout_operation_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
- ,cashout_uuid uuid PRIMARY KEY
- ,local_transaction BIGINT UNIQUE -- FIXME: Comment that the transaction only gets created after the TAN confirmation
- REFERENCES bank_account_transactions(bank_transaction_id)
- ON DELETE RESTRICT
- ON UPDATE RESTRICT
- ,amount_debit taler_amount NOT NULL -- FIXME: comment on column how to derive the currency
- ,amount_credit taler_amount NOT NULL -- FIXME: comment on column how to derive the currency
- ,buy_at_ratio INT4 NOT NULL -- FIXME: document format (fractional base)
- ,buy_in_fee taler_amount NOT NULL -- FIXME: comment on column how to derive the currency
- ,sell_at_ratio INT4 NOT NULL -- FIXME: document format (fractional base)
- ,sell_out_fee taler_amount NOT NULL -- FIXME: comment on column how to derive the currency
+ (cashout_uuid uuid NOT NULL PRIMARY KEY
+ ,amount_debit taler_amount NOT NULL
+ ,amount_credit taler_amount NOT NULL
,subject TEXT NOT NULL
,creation_time BIGINT NOT NULL
- ,tan_confirmation_time BIGINT
- ,tan_channel tan_enum NOT NULL
- ,tan_code TEXT NOT NULL
- ,bank_account BIGINT DEFAULT(NULL)
+ ,bank_account BIGINT NOT NULL
REFERENCES bank_accounts(bank_account_id)
ON DELETE CASCADE
ON UPDATE RESTRICT
- ,credit_payto_uri TEXT NOT NULL
- ,cashout_currency TEXT NOT NULL -- need, or include in credit_payto_uri?
+ ,tan_channel tan_enum NOT NULL
+ ,tan_code TEXT NOT NULL
+ ,tan_confirmation_time BIGINT
+ ,local_transaction BIGINT UNIQUE -- FIXME: Comment that the transaction only gets created after the TAN confirmation
+ REFERENCES bank_account_transactions(bank_transaction_id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
);
-- FIXME: table comment missing
@@ -193,23 +179,6 @@ COMMENT ON COLUMN cashout_operations.tan_confirmation_time
COMMENT ON COLUMN cashout_operations.tan_code
IS 'text that the customer must send to confirm the cash-out operation';
--- FIXME: check in the code if this really only has pending or failed submissions!
-CREATE TABLE IF NOT EXISTS pending_cashout_submissions
- (cashout_submission_id BIGINT GENERATED BY DEFAULT AS IDENTITY
- ,cashout_operation_id BIGINT NOT NULL
- REFERENCES cashout_operations(cashout_operation_id)
- ON DELETE CASCADE
- ON UPDATE RESTRICT
- ,nexus_response TEXT
- ,submission_time BIGINT
- );
-
-COMMENT ON TABLE pending_cashout_submissions
- IS 'Tracks payment requests made from Sandbox to Nexus to trigger fiat transactions that finalize cash-outs.';
-COMMENT ON COLUMN pending_cashout_submissions.nexus_response
- IS 'Keeps the Nexus response to the payment submission on failure';
-
-
-- end of: cashout management
-- start of: EBICS management
@@ -379,8 +348,7 @@ CREATE TABLE IF NOT EXISTS taler_exchange_incoming
);
CREATE TABLE IF NOT EXISTS taler_withdrawal_operations
- (taler_withdrawal_id BIGINT GENERATED BY DEFAULT AS IDENTITY
- ,withdrawal_uuid uuid NOT NULL
+ (withdrawal_uuid uuid NOT NULL PRIMARY KEY
,amount taler_amount NOT NULL
,selection_done BOOLEAN DEFAULT FALSE NOT NULL
,aborted BOOLEAN DEFAULT FALSE NOT NULL
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -101,35 +101,38 @@ CREATE OR REPLACE FUNCTION account_reconfig(
IN in_email TEXT,
IN in_cashout_payto TEXT,
IN in_is_taler_exchange BOOLEAN,
- OUT out_nx_customer BOOLEAN,
- OUT out_nx_bank_account BOOLEAN
+ IN in_is_admin BOOLEAN,
+ OUT out_not_found BOOLEAN,
+ OUT out_legal_name_change BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
my_customer_id INT8;
BEGIN
SELECT
- customer_id
- INTO my_customer_id
+ customer_id,
+ in_name IS NOT NULL AND name != in_name AND NOT in_is_admin
+ INTO my_customer_id, out_legal_name_change
FROM customers
WHERE login=in_login;
IF NOT FOUND THEN
- out_nx_customer=TRUE;
+ out_not_found=TRUE;
+ RETURN;
+ELSIF out_legal_name_change THEN
RETURN;
END IF;
-out_nx_customer=FALSE;
-- optionally updating the Taler exchange flag
IF in_is_taler_exchange IS NOT NULL THEN
UPDATE bank_accounts
SET is_taler_exchange = in_is_taler_exchange
WHERE owning_customer_id = my_customer_id;
+ IF NOT FOUND THEN
+ out_not_found=TRUE;
+ RETURN;
+ END IF;
END IF;
-IF in_is_taler_exchange IS NOT NULL AND NOT FOUND THEN
- out_nx_bank_account=TRUE;
- RETURN;
-END IF;
-out_nx_bank_account=FALSE;
+
-- bank account patching worked, custom must as well
-- since this runs in a DB transaction and the customer
@@ -145,7 +148,7 @@ IF in_name IS NOT NULL THEN
UPDATE customers SET name=in_name WHERE customer_id = my_customer_id;
END IF;
END $$;
-COMMENT ON FUNCTION account_reconfig(TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN)
+COMMENT ON FUNCTION account_reconfig(TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN, BOOLEAN)
IS 'Updates values on customer and bank account rows based on the input data.';
CREATE OR REPLACE FUNCTION customer_delete(