From 939fcbf1595ba8fdcca1cb7cedddb12069c09278 Mon Sep 17 00:00:00 2001 From: Neil Frow <675806+worthydolt@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:51:38 +0100 Subject: [PATCH] API-7700: add ActorType responsible for last action (#517) * API-7700: add ActorType responsible for last action to Applications response * API-7700: pr-bot comment * API-7700: shuffling code to make it more compliant with what's already there --- .../repository/StateHistoryRepository.scala | 11 +++- .../services/ApplicationService.scala | 29 ++++++--- conf/application-json-logger.xml | 13 ---- .../StateHistoryRepositoryISpec.scala | 61 +++++++++++++++++++ project/AppDependencies.scala | 2 +- .../StateHistoryRepositoryMockModule.scala | 6 ++ .../services/ApplicationServiceSpec.scala | 6 ++ 7 files changed, 104 insertions(+), 24 deletions(-) delete mode 100644 conf/application-json-logger.xml diff --git a/app/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepository.scala b/app/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepository.scala index e575a39b8..cf5a3ce85 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepository.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepository.scala @@ -20,7 +20,7 @@ import java.time.Instant import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} -import org.mongodb.scala.model.Filters.{and, equal} +import org.mongodb.scala.model.Filters.{and, equal, in} import org.mongodb.scala.model.Indexes.{ascending, descending} import org.mongodb.scala.model.{IndexModel, IndexOptions} @@ -94,6 +94,15 @@ class StateHistoryRepository @Inject() (mongo: MongoComponent)(implicit val ec: .map(x => x.toList) } + def fetchDeletedByApplicationIds(applicationIds: List[ApplicationId]): Future[List[StateHistory]] = { + collection.find(and( + in("applicationId", applicationIds.map(i => Codecs.toBson(i)): _*), + equal("state", Codecs.toBson(State.DELETED.toString)) + )) + .toFuture() + .map(x => x.toList) + } + def fetchLatestByStateForApplication(applicationId: ApplicationId, state: State): Future[Option[StateHistory]] = { collection.find(and( equal("applicationId", Codecs.toBson(applicationId)), diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/ApplicationService.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/ApplicationService.scala index a41a8948f..a7faf0636 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/ApplicationService.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/ApplicationService.scala @@ -44,7 +44,7 @@ import uk.gov.hmrc.thirdpartyapplication.connector._ import uk.gov.hmrc.thirdpartyapplication.controllers.{DeleteApplicationRequest, FixCollaboratorRequest} import uk.gov.hmrc.thirdpartyapplication.domain.models.{ApplicationStateChange, Deleted} import uk.gov.hmrc.thirdpartyapplication.models._ -import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication +import uk.gov.hmrc.thirdpartyapplication.models.db.{PaginatedApplicationData, StoredApplication} import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, NotificationRepository, StateHistoryRepository, SubscriptionRepository, TermsOfUseInvitationRepository} import uk.gov.hmrc.thirdpartyapplication.services.AuditAction._ import uk.gov.hmrc.thirdpartyapplication.util.http.HeaderCarrierUtils._ @@ -260,14 +260,25 @@ class ApplicationService @Inject() ( .map(application => Application(data = application)) def searchApplications(applicationSearch: ApplicationSearch): Future[PaginatedApplicationResponse] = { - applicationRepository.searchApplications("applicationSearch")(applicationSearch).map { data => - PaginatedApplicationResponse( - page = applicationSearch.pageNumber, - pageSize = applicationSearch.pageSize, - total = data.totals.foldLeft(0)(_ + _.total), - matching = data.matching.foldLeft(0)(_ + _.total), - applications = data.applications.map(application => Application(data = application)) - ) + + def buildApplication(storedApplication: StoredApplication, stateHistory: Option[StateHistory]) = { + val partApp = Application(data = storedApplication) + partApp.copy(moreApplication = partApp.moreApplication.copy(lastActionActor = stateHistory.map(sh => ActorType.actorType(sh.actor)).getOrElse(ActorType.UNKNOWN))) + } + + val applicationsResponse: Future[PaginatedApplicationData] = applicationRepository.searchApplications("applicationSearch")(applicationSearch) + val appHistory: Future[List[StateHistory]] = { + applicationsResponse.map(data => data.applications.map(app => app.id)).flatMap(ar => stateHistoryRepository.fetchDeletedByApplicationIds(ar)) + } + + applicationsResponse.zipWith(appHistory) { + case (data, appHistory) => PaginatedApplicationResponse( + page = applicationSearch.pageNumber, + pageSize = applicationSearch.pageSize, + total = data.totals.foldLeft(0)(_ + _.total), + matching = data.matching.foldLeft(0)(_ + _.total), + applications = data.applications.map(app => buildApplication(app, appHistory.find(ah => ah.applicationId == app.id))) + ) } } diff --git a/conf/application-json-logger.xml b/conf/application-json-logger.xml deleted file mode 100644 index 6628cd691..000000000 --- a/conf/application-json-logger.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepositoryISpec.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepositoryISpec.scala index 149065cff..391bcb7b8 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepositoryISpec.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/StateHistoryRepositoryISpec.scala @@ -192,5 +192,66 @@ class StateHistoryRepositoryISpec savedStateHistories shouldBe List(stateHistory) } } + "fetchDeletedByApplicationIds" should { + "fetch a single deleted application" in { + val requesterEmail = "bill.badger@rupert.com".toLaxEmail + val appId = ApplicationId.random + val ts = instant + val actor: Actor = Actors.AppCollaborator(requesterEmail) + + val stateHistory = StateHistory(appId, State.DELETED, actor, Some(State.PENDING_RESPONSIBLE_INDIVIDUAL_VERIFICATION), changedAt = ts) + + val result = await(repository.insert(stateHistory)) + result shouldBe stateHistory + + val retrieved = await(repository.fetchDeletedByApplicationIds(List(appId, ApplicationId.random))) + retrieved.size shouldBe 1 + retrieved.head shouldBe stateHistory + } + "fetch multiple deleted applications" in { + val requesterEmail = "bill.badger@rupert.com".toLaxEmail + val appId = ApplicationId.random + val appId2 = ApplicationId.random + val appId3 = ApplicationId.random + val ts = instant + val actor: Actor = Actors.AppCollaborator(requesterEmail) + val stateHistory = StateHistory(appId, State.DELETED, actor, Some(State.PENDING_RESPONSIBLE_INDIVIDUAL_VERIFICATION), changedAt = ts) + val stateHistory2 = stateHistory.copy(applicationId = appId2) + val stateHistory3 = stateHistory.copy(applicationId = appId3) + + await(repository.insert(stateHistory)) + await(repository.insert(stateHistory2)) + await(repository.insert(stateHistory3)) + + val retrieved = await(repository.fetchDeletedByApplicationIds(List(appId, appId2, appId3))) + retrieved.size shouldBe 3 + retrieved should contain(stateHistory) + retrieved should contain(stateHistory2) + retrieved should contain(stateHistory3) + } + + "fetch multiple deleted applications and none in other states" in { + val requesterEmail = "bill.badger@rupert.com".toLaxEmail + val appId = ApplicationId.random + val appId2 = ApplicationId.random + val appId3 = ApplicationId.random + val ts = instant + val actor: Actor = Actors.AppCollaborator(requesterEmail) + + val stateHistory = StateHistory(appId, State.DELETED, actor, Some(State.PENDING_RESPONSIBLE_INDIVIDUAL_VERIFICATION), changedAt = ts) + val stateHistory2 = stateHistory.copy(applicationId = appId2) + val stateHistory3 = stateHistory.copy(applicationId = appId3, state = State.PENDING_GATEKEEPER_APPROVAL) + + await(repository.insert(stateHistory)) + await(repository.insert(stateHistory2)) + await(repository.insert(stateHistory3)) + + val retrieved = await(repository.fetchDeletedByApplicationIds(List(appId, appId2, appId3))) + retrieved.size shouldBe 2 + retrieved should contain(stateHistory) + retrieved should contain(stateHistory2) + retrieved should not contain stateHistory3 + } + } } diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index d27135782..3205f0717 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -7,7 +7,7 @@ object AppDependencies { lazy val bootstrapVersion = "9.2.0" lazy val hmrcMongoVersion = "1.7.0" lazy val commonDomainVersion = "0.15.0" - lazy val applicationEventVersion = "0.63.0" + lazy val applicationEventVersion = "0.64.0" private lazy val compileDeps = Seq( "uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion, diff --git a/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/StateHistoryRepositoryMockModule.scala b/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/StateHistoryRepositoryMockModule.scala index 6cbfc0b70..928bc0451 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/StateHistoryRepositoryMockModule.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/StateHistoryRepositoryMockModule.scala @@ -85,6 +85,12 @@ trait StateHistoryRepositoryMockModule extends MockitoSugar with ArgumentMatcher def thenFailWith(ex: Exception) = when(aMock.fetchByApplicationId(*[ApplicationId])).thenReturn(failed(ex)) } + + object FetchDeletedByApplicationIds { + + def thenReturnWhen(applicationIds: List[ApplicationId])(values: StateHistory*) = + when(aMock.fetchDeletedByApplicationIds(eqTo(applicationIds))).thenReturn(successful(values.toList)) + } } object StateHistoryRepoMock extends BaseStateHistoryRepoMock { diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationServiceSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationServiceSpec.scala index b8c683516..f4ce61f2b 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationServiceSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationServiceSpec.scala @@ -918,6 +918,8 @@ class ApplicationServiceSpec ) ) ) + val histories = List(aHistory(standardApplicationData.id), aHistory(privilegedApplicationData.id), aHistory(ropcApplicationData.id)) + StateHistoryRepoMock.FetchDeletedByApplicationIds.thenReturnWhen(List(standardApplicationData.id, privilegedApplicationData.id, ropcApplicationData.id))(histories: _*) val result: PaginatedApplicationResponse = await(underTest.searchApplications(search)) @@ -980,4 +982,8 @@ class ApplicationServiceSpec ApplicationId.random ) } + + private def aHistory(appId: ApplicationId, state: State = State.DELETED): StateHistory = { + StateHistory(appId, state, Actors.AppCollaborator("anEmail".toLaxEmail), Some(State.TESTING), changedAt = instant) + } }