libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 2a45c348a3506259e835bd0877fad9c3c5f55c4a
parent 5b18cb6a663394d6c29c87c62dafbee584964e9e
Author: MS <ms@taler.net>
Date:   Fri, 20 Jan 2023 16:26:34 +0100

Error management.

Blaming the bank when a 'bank account' is not
found for an existing 'customer'.

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 29++++++++++++++++++++---------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 41++++++++++++++++++++++++++++++++---------
2 files changed, 52 insertions(+), 18 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -6,6 +6,7 @@ import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.netty.handler.codec.http.HttpResponseStatus import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.sandbox.CashoutOperationsTable.uuid import tech.libeufin.util.* @@ -437,22 +438,25 @@ fun circuitApi(circuitRoute: Route) { throwIfInstitutionalName(resourceName) allowOwnerOrAdmin(username, resourceName) val customer = getCustomer(resourceName) - /** FIXME: the following query can 404, but should 500. - * The reason is that that's the bank's fault if an existing - * customer misses the bank account. Check other calls too, - * for the same error. + /** + * CUSTOMER AND BANK ACCOUNT INVARIANT. + * + * After having found a 'customer' associated with the resourceName + * - see previous line -, the bank must ensure that a 'bank account' + * exist under the same resourceName. If that fails, the bank broke the + * invariant and should respond 500. */ - val bankAccount = getBankAccountFromLabel(resourceName) + val bankAccount = getBankAccountFromLabel(resourceName, withBankFault = true) /** * Throwing when name or cash-out address aren't found ensures * that the customer was indeed added via the Circuit API, as opposed * to the Access API. */ - val potentialError = "$resourceName not managed by the Circuit API." + val maybeError = "$resourceName not managed by the Circuit API." call.respond(CircuitAccountInfo( username = customer.username, - name = customer.name ?: throw notFound(potentialError), - cashout_address = customer.cashout_address ?: throw notFound(potentialError), + name = customer.name ?: throw notFound(maybeError), + cashout_address = customer.cashout_address ?: throw notFound(maybeError), contact_data = CircuitContactData( email = customer.email, phone = customer.phone @@ -480,6 +484,10 @@ fun circuitApi(circuitRoute: Route) { }) } } + if (customers.size == 0) { + call.respond(HttpStatusCode.NoContent) + return@get + } call.respond(object {val customers = customers}) return@get } @@ -597,8 +605,11 @@ fun circuitApi(circuitRoute: Route) { call.request.basicAuth(onlyAdmin = true) val resourceName = call.getUriComponent("resourceName") throwIfInstitutionalName(resourceName) - val bankAccount = getBankAccountFromLabel(resourceName) val customer = getCustomer(resourceName) + val bankAccount = getBankAccountFromLabel( + resourceName, + withBankFault = true // See comment "CUSTOMER AND BANK ACCOUNT INVARIANT". + ) val balance = getBalance(bankAccount) if (balance != BigDecimal.ZERO) throw SandboxError( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -302,7 +302,18 @@ fun getBankAccountFromIban(iban: String): BankAccountEntity { ) } -fun getBankAccountFromLabel(label: String, demobank: String = "default"): BankAccountEntity { +/** + * The argument 'withBankFault' represents the case where + * _the bank_ must ensure that a resource (in this case a bank + * account) exists. For example, every 'customer' should have + * a 'bank account', and if a customer is found without a bank + * account, then the bank broke such condition. + */ +fun getBankAccountFromLabel( + label: String, + demobank: String = "default", + withBankFault: Boolean = false +): BankAccountEntity { val maybeDemobank = getDemobank(demobank) if (maybeDemobank == null) { logger.error("Demobank '$demobank' not found") @@ -311,21 +322,33 @@ fun getBankAccountFromLabel(label: String, demobank: String = "default"): BankAc "Demobank '$demobank' not found" ) } - return getBankAccountFromLabel(label, maybeDemobank) + return getBankAccountFromLabel( + label, + maybeDemobank, + withBankFault + ) } -fun getBankAccountFromLabel(label: String, - demobank: DemobankConfigEntity +fun getBankAccountFromLabel( + label: String, + demobank: DemobankConfigEntity, + withBankFault: Boolean = false ): BankAccountEntity { - return transaction { + val maybeBankAccount = transaction { BankAccountEntity.find( BankAccountsTable.label eq label and ( BankAccountsTable.demoBank eq demobank.id ) - ).firstOrNull() ?: throw SandboxError( - HttpStatusCode.NotFound, - "Did not find a bank account for label $label" - ) + ).firstOrNull() } + if (maybeBankAccount == null && withBankFault) + throw internalServerError( + "Bank account $label was not found, but it should." + ) + if (maybeBankAccount == null) + throw notFound( + "Bank account $label was not found." + ) + return maybeBankAccount } fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity): BankAccountEntity {