Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions obp-api/src/main/scala/deletion/DeleteAccountCascade.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import code.counterpartyattribute.CounterpartyAttribute
import code.counterpartylimit.CounterpartyLimit
import code.entitlement.MappedEntitlement
import code.metadata.counterparties.{MappedCounterparty, MappedCounterpartyBespoke, MappedCounterpartyMetadata, MappedCounterpartyWhereTag}
import code.model.dataAccess.{BankAccountRouting, MappedBankAccount, MappedBankAccountData}
import code.model.dataAccess.{BankAccountRouting, DoubleEntryBookTransaction, MappedBankAccount, MappedBankAccountData}
import code.transactionChallenge.MappedExpectedChallengeAnswer
import code.transactionrequests.MappedTransactionRequest
import code.views.system.{AccountAccess, ViewDefinition}
import code.webhook.MappedAccountWebhook
import com.openbankproject.commons.model.{AccountId, BankId}
Expand All @@ -52,8 +54,13 @@ import scala.collection.immutable.List
object DeleteAccountCascade {

def delete(bankId: BankId, accountId: AccountId): Boolean = {
// The tasks run in list order: the challenges are found through the account's transaction
// requests, so they must go before deleteTransactionRequests and deleteTransactions remove them.
val doneTasks =
deleteTransactions(bankId, accountId) ::
deleteTransactionRequestChallenges(bankId, accountId) ::
deleteTransactionRequests(bankId, accountId) ::
deleteDoubleEntryBookTransactions(bankId, accountId) ::
deleteTransactions(bankId, accountId) ::
deleteEntitlements(bankId, accountId) ::
deleteAccountAccess(bankId, accountId) ::
deleteCustomViews(bankId, accountId) ::
Expand Down Expand Up @@ -202,4 +209,31 @@ object DeleteAccountCascade {
false
}
}

private def deleteTransactionRequestChallenges(bankId: BankId, accountId: AccountId): Boolean = {
val transactionRequestIds = MappedTransactionRequest.findAll(
By(MappedTransactionRequest.mFrom_BankId, bankId.value),
By(MappedTransactionRequest.mFrom_AccountId, accountId.value)
).map(_.mTransactionRequestId.get)
MappedExpectedChallengeAnswer.bulkDelete_!!(
ByList(MappedExpectedChallengeAnswer.TransactionRequestId, transactionRequestIds)
)
}
private def deleteTransactionRequests(bankId: BankId, accountId: AccountId): Boolean = {
MappedTransactionRequest.bulkDelete_!!(
By(MappedTransactionRequest.mFrom_BankId, bankId.value),
By(MappedTransactionRequest.mFrom_AccountId, accountId.value)
)
}
private def deleteDoubleEntryBookTransactions(bankId: BankId, accountId: AccountId): Boolean = {
val debitSide = DoubleEntryBookTransaction.bulkDelete_!!(
By(DoubleEntryBookTransaction.DebitTransactionBankId, bankId.value),
By(DoubleEntryBookTransaction.DebitTransactionAccountId, accountId.value)
)
val creditSide = DoubleEntryBookTransaction.bulkDelete_!!(
By(DoubleEntryBookTransaction.CreditTransactionBankId, bankId.value),
By(DoubleEntryBookTransaction.CreditTransactionAccountId, accountId.value)
)
debitSide && creditSide
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.createViewJsonV300
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole
import code.api.util.{APIUtil, ApiRole}
import code.api.util.ApiRole.CanDeleteAccountCascade
import code.api.util.ErrorMessages.{UserHasMissingRoles, AuthenticatedUserIsRequired}
import code.api.v3_1_0.CreateAccountResponseJsonV310
Expand Down Expand Up @@ -93,7 +93,7 @@
}
feature(s"test $ApiEndpoint1 - Authorized access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) {
When("We grant the role")

Check failure on line 96 in obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "We grant the role" 3 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AaGhA8OStCxZ9fQ6kKV_&open=AaGhA8OStCxZ9fQ6kKV_&pullRequest=2958
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canCreateAccount.toString)
And("We make a request v4.0.0")
val request400 = (v4_0_0_Request / "banks" / bankId / "accounts" ).POST <@(user1)
Expand Down Expand Up @@ -142,6 +142,29 @@
When("We try to delete one more time")
makeDeleteRequest(request400).code should equal(404)
}
scenario("We delete an account that has paid by transaction requests", ApiEndpoint1, VersionOfApi) {
val paymentsBankId = createBank(APIUtil.generateUUID()).bankId.value
val (fromAccountId, toAccountId, transactionRequestIds) = createPaymentsForDeleteCascade(paymentsBankId)
doubleEntryBookTransactionsOfAccount(paymentsBankId, fromAccountId) should not be empty
val (transactionRequestsBefore, challengesBefore) = transactionRequestsAndChallenges(transactionRequestIds)
transactionRequestsBefore.size should equal(2)
challengesBefore should not be empty

When("We grant the role")
Entitlement.entitlement.vend.addEntitlement(paymentsBankId, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString)
And("We make a delete cascade request v4.0.0")
val deleteRequest400 = (v4_0_0_Request / "management" / "cascading" / "banks" / paymentsBankId /
"accounts" / fromAccountId).DELETE <@(user1)
makeDeleteRequest(deleteRequest400).code should equal(200)

Then("The double-entry book rows, transaction requests and challenges of the account are deleted")
doubleEntryBookTransactionsOfAccount(paymentsBankId, fromAccountId) shouldBe empty
val (transactionRequestsAfter, challengesAfter) = transactionRequestsAndChallenges(transactionRequestIds)
transactionRequestsAfter shouldBe empty
challengesAfter shouldBe empty
And("The double-entry book row is gone for the receiving account as well, since it was one row for both sides")
doubleEntryBookTransactionsOfAccount(paymentsBankId, toAccountId) shouldBe empty
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - cards") {
Expand Down
19 changes: 19 additions & 0 deletions obp-api/src/test/scala/code/api/v4_0_0/DeleteBankCascadeTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
}
feature(s"test $ApiEndpoint1 - Authorized access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) {
When("We grant the role")

Check failure on line 90 in obp-api/src/test/scala/code/api/v4_0_0/DeleteBankCascadeTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "We grant the role" 3 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AaGhA8EgtCxZ9fQ6kKV-&open=AaGhA8EgtCxZ9fQ6kKV-&pullRequest=2958
val bankId = createBank(APIUtil.generateUUID()).bankId.value
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canCreateAccount.toString)
And("We make a request v4.0.0")
Expand Down Expand Up @@ -158,6 +158,25 @@
val responseBody = response.body.extract[CustomersMinimalJsonV400]
responseBody.customers.length equals 0
}
scenario("We delete a bank whose accounts have paid by transaction requests", ApiEndpoint1, VersionOfApi) {
val bankId = createBank(APIUtil.generateUUID()).bankId.value
val (fromAccountId, toAccountId, transactionRequestIds) = createPaymentsForDeleteCascade(bankId)
doubleEntryBookTransactionsOfAccount(bankId, fromAccountId) should not be empty
transactionRequestsAndChallenges(transactionRequestIds)._2 should not be empty

When("We grant the role")
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canDeleteBankCascade.toString)
And("We make a delete cascade request v4.0.0")
val deleteRequest400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId ).DELETE <@(user1)
makeDeleteRequest(deleteRequest400).code should equal(200)

Then("The double-entry book rows, transaction requests and challenges of its accounts are deleted")
doubleEntryBookTransactionsOfAccount(bankId, fromAccountId) shouldBe empty
doubleEntryBookTransactionsOfAccount(bankId, toAccountId) shouldBe empty
val (transactionRequestsAfter, challengesAfter) = transactionRequestsAndChallenges(transactionRequestIds)
transactionRequestsAfter shouldBe empty
challengesAfter shouldBe empty
}
}


Expand Down
61 changes: 60 additions & 1 deletion obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ import code.metadata.transactionimages.MappedTransactionImage
import code.metadata.wheretags.MappedWhereTag
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UserHasMissingRoles}
import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData}
import code.model.dataAccess.DoubleEntryBookTransaction
import code.transactionChallenge.MappedExpectedChallengeAnswer
import code.transactionattribute.MappedTransactionAttribute
import code.transactionrequests.MappedTransactionRequest
import com.openbankproject.commons.model.enums.TransactionRequestStatus
import com.openbankproject.commons.model.{AccountId, AccountRoutingJsonV121, AmountOfMoneyJsonV121, BankId, CreateViewJson, ErrorMessage, UpdateViewJSON}
import com.openbankproject.commons.util.ApiShortVersions
import code.setup.OBPReq
import org.json4s.native.Serialization.write
import net.liftweb.mapper.By
import net.liftweb.mapper.{By, ByList}
import net.liftweb.util.Helpers.randomString

import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -457,6 +461,61 @@ trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers {
(bank.bankId.value, fromAccount.account_id, transactionId)
}

/**
* Creates two accounts at the bank and two SANDBOX_TAN transaction requests from the first to the second:
* one below the challenge threshold, which is paid at once and booked in the double-entry book,
* and one above it, which stays INITIATED with an expected challenge answer and no transaction.
* Returns (fromAccountId, toAccountId, transactionRequestIds).
*/
def createPaymentsForDeleteCascade(bankId: String): (String, String, List[String]) = {
def addAccountJson = SwaggerDefinitionsJSON.createAccountRequestJsonV310
.copy(user_id = resourceUser1.userId, balance = AmountOfMoneyJsonV121("EUR","0"),
account_routings = List(AccountRoutingJsonV121(Random.nextString(10), Random.nextString(10))))
val fromAccount = createAccountViaEndpoint(bankId, addAccountJson, user1)
val toAccount = createAccountViaEndpoint(bankId, addAccountJson, user1)
val customViewJson = createViewJsonV300.copy(name = "_cascade_delete", metadata_view = "_cascade_delete", is_public = false).toCreateViewJson
val customView = createViewViaEndpoint(bankId, fromAccount.account_id, customViewJson, user1)
grantUserAccessToViewViaEndpoint(
bankId,
fromAccount.account_id,
resourceUser1.userId,
user1,
PostViewJsonV400(view_id = customView.id, is_system = false)
)
def pay(amount: String) = createTransactionRequestViaEndpoint(
fromBankId = bankId,
fromAccountId = fromAccount.account_id,
fromCurrency = fromAccount.balance.currency,
fromViewId = customView.id,
amount = amount,
toBankId = bankId,
toAccountId = toAccount.account_id,
user1
)
val completed = pay("10")
completed.status should equal(TransactionRequestStatus.COMPLETED.toString)
val initiated = pay("5000")
initiated.status should equal(TransactionRequestStatus.INITIATED.toString)
(fromAccount.account_id, toAccount.account_id, List(completed.id, initiated.id))
}

def doubleEntryBookTransactionsOfAccount(bankId: String, accountId: String): List[DoubleEntryBookTransaction] = {
DoubleEntryBookTransaction.findAll(
By(DoubleEntryBookTransaction.DebitTransactionBankId, bankId),
By(DoubleEntryBookTransaction.DebitTransactionAccountId, accountId)
) ++ DoubleEntryBookTransaction.findAll(
By(DoubleEntryBookTransaction.CreditTransactionBankId, bankId),
By(DoubleEntryBookTransaction.CreditTransactionAccountId, accountId)
)
}

def transactionRequestsAndChallenges(transactionRequestIds: List[String]): (List[MappedTransactionRequest], List[MappedExpectedChallengeAnswer]) = {
(
MappedTransactionRequest.findAll(ByList(MappedTransactionRequest.mTransactionRequestId, transactionRequestIds)),
MappedExpectedChallengeAnswer.findAll(ByList(MappedExpectedChallengeAnswer.TransactionRequestId, transactionRequestIds))
)
}

def saveHistoricalTransactionViaEndpoint(fromBankId: BankId,
fromAccountId: AccountId,
toBankId: BankId,
Expand Down
Loading