From 1492243d1a3bb42c08b3204bf3c56e3a8960e650 Mon Sep 17 00:00:00 2001 From: petekirby-ee Date: Wed, 5 Jun 2024 10:28:21 +0100 Subject: [PATCH] APIS-7030 New handler for SubmitApplicationApprovalRequest command (#498) * APIS-7030 Replace action 'submit application approval' with command/event * APIS-7030 New handler for SubmitApplicationApprovalRequest command * APIS-7030 New handler for SubmitApplicationApprovalRequest command * APIS-7030 PR bot actions * APIS-7030 PR bot actions --- .../services/RequestApprovalsService.scala | 4 +- ...onsibleIndividualVerificationService.scala | 9 +- .../SubmissionCommandsProcessor.scala | 4 +- ...icationApprovalRequestCommandHandler.scala | 230 ++++++++++++++++++ conf/logback.xml | 10 - project/AppDependencies.scala | 2 +- project/build.properties | 2 +- project/plugins.sbt | 2 +- ...bleIndividualVerificationServiceSpec.scala | 15 +- ...ionApprovalRequestCommandHandlerSpec.scala | 212 ++++++++++++++++ .../ApplicationCommandDispatcherUtils.scala | 4 +- 11 files changed, 456 insertions(+), 38 deletions(-) create mode 100644 app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandler.scala create mode 100644 test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandlerSpec.scala diff --git a/app/uk/gov/hmrc/apiplatform/modules/approvals/services/RequestApprovalsService.scala b/app/uk/gov/hmrc/apiplatform/modules/approvals/services/RequestApprovalsService.scala index 041511c88..7173449c0 100644 --- a/app/uk/gov/hmrc/apiplatform/modules/approvals/services/RequestApprovalsService.scala +++ b/app/uk/gov/hmrc/apiplatform/modules/approvals/services/RequestApprovalsService.scala @@ -138,7 +138,7 @@ class RequestApprovalsService @Inject() ( _ <- ET.liftF(writeStateHistory(updatedApp, requestedByEmailAddress)) updatedSubmission = Submission.submit(Instant.now(clock), requestedByEmailAddress)(submission) savedSubmission <- ET.liftF(submissionService.store(updatedSubmission)) - _ <- ET.liftF(sendProdCredsVerificationEmailIfNeeded(isRequesterTheResponsibleIndividual, savedApp, submission, importantSubmissionData, requestedByName)) + _ <- ET.liftF(sendResponsibleIndividualVerificationEmailIfNeeded(isRequesterTheResponsibleIndividual, savedApp, submission, importantSubmissionData, requestedByName)) _ = logCompletedApprovalRequest(savedApp) _ <- ET.liftF(auditCompletedApprovalRequest(originalApp.id, savedApp)) } yield ApprovalAccepted(savedApp) @@ -241,7 +241,7 @@ class RequestApprovalsService @Inject() ( } } - private def sendProdCredsVerificationEmailIfNeeded( + private def sendResponsibleIndividualVerificationEmailIfNeeded( isRequesterTheResponsibleIndividual: Boolean, application: StoredApplication, submission: Submission, diff --git a/app/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationService.scala b/app/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationService.scala index 111797274..87986066d 100644 --- a/app/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationService.scala +++ b/app/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationService.scala @@ -30,19 +30,12 @@ import uk.gov.hmrc.apiplatform.modules.approvals.domain.models.{ ResponsibleIndividualVerificationId } import uk.gov.hmrc.apiplatform.modules.approvals.repositories.ResponsibleIndividualVerificationRepository -import uk.gov.hmrc.apiplatform.modules.submissions.services.SubmissionsService -import uk.gov.hmrc.thirdpartyapplication.connector.EmailConnector import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication -import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, StateHistoryRepository} -import uk.gov.hmrc.thirdpartyapplication.services.ApplicationService +import uk.gov.hmrc.thirdpartyapplication.repository.StateHistoryRepository class ResponsibleIndividualVerificationService @Inject() ( responsibleIndividualVerificationRepository: ResponsibleIndividualVerificationRepository, - applicationRepository: ApplicationRepository, stateHistoryRepository: StateHistoryRepository, - applicationService: ApplicationService, - submissionService: SubmissionsService, - emailConnector: EmailConnector, clock: Clock )(implicit ec: ExecutionContext ) extends BaseService(stateHistoryRepository, clock) with ApplicationLogger { diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala index a37147d97..3240ad692 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala @@ -30,7 +30,8 @@ class SubmissionCommandsProcessor @Inject() ( declineApplicationApprovalRequestCommandHandler: DeclineApplicationApprovalRequestCommandHandler, declineResponsibleIndividualCmdHdlr: DeclineResponsibleIndividualCommandHandler, declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler, - resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler + resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler, + submitApplicationApprovalRequestCmdHdlr: SubmitApplicationApprovalRequestCommandHandler ) { import CommandHandler._ import ApplicationCommands._ @@ -43,5 +44,6 @@ class SubmissionCommandsProcessor @Inject() ( case cmd: DeclineResponsibleIndividual => declineResponsibleIndividualCmdHdlr.process(app, cmd) case cmd: DeclineResponsibleIndividualDidNotVerify => declineResponsibleIndividualDidNotVerifyCmdHdlr.process(app, cmd) case cmd: ResendRequesterEmailVerification => resendRequesterEmailVerificationCmdHdlr.process(app, cmd) + case cmd: SubmitApplicationApprovalRequest => submitApplicationApprovalRequestCmdHdlr.process(app, cmd) } } diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandler.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandler.scala new file mode 100644 index 000000000..bee01803c --- /dev/null +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandler.scala @@ -0,0 +1,230 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.thirdpartyapplication.services.commands.submission + +import javax.inject.{Inject, Singleton} +import scala.concurrent.{ExecutionContext, Future} + +import cats.Apply +import cats.data.{NonEmptyList, OptionT, Validated} +import cats.syntax.validated._ + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, ApplicationId} +import uk.gov.hmrc.apiplatform.modules.common.services.ApplicationLogger +import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access +import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.{State, StateHistory} +import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models.{ImportantSubmissionData, ResponsibleIndividual, TermsOfUseAcceptance} +import uk.gov.hmrc.apiplatform.modules.approvals.domain.models.ResponsibleIndividualVerificationId +import uk.gov.hmrc.apiplatform.modules.approvals.services.{ApprovalsNamingService, ResponsibleIndividualVerificationService} +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.SubmitApplicationApprovalRequest +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models._ +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents._ +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._ +import uk.gov.hmrc.apiplatform.modules.submissions.domain.models._ +import uk.gov.hmrc.apiplatform.modules.submissions.domain.services.SubmissionDataExtracter +import uk.gov.hmrc.apiplatform.modules.submissions.services.SubmissionsService +import uk.gov.hmrc.thirdpartyapplication.models._ +import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication +import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, StateHistoryRepository, TermsOfUseInvitationRepository} +import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandler + +@Singleton +class SubmitApplicationApprovalRequestCommandHandler @Inject() ( + submissionService: SubmissionsService, + applicationRepository: ApplicationRepository, + stateHistoryRepository: StateHistoryRepository, + termsOfUseInvitationRepository: TermsOfUseInvitationRepository, + approvalsNamingService: ApprovalsNamingService, + responsibleIndividualVerificationService: ResponsibleIndividualVerificationService + )(implicit val ec: ExecutionContext + ) extends CommandHandler with ApplicationLogger { + + import CommandHandler._ + import CommandFailures._ + + private def validate(app: StoredApplication): Future[Validated[Failures, (Submission, String)]] = { + ( + for { + submission <- OptionT(submissionService.fetchLatest(app.id)) + appName <- OptionT.fromOption[Future](SubmissionDataExtracter.getApplicationName(submission)) + nameValidation <- OptionT.liftF[Future, ApplicationNameValidationResult](validateApplicationName(appName, app.id)) + } yield (submission, appName, nameValidation) + ) + .fold[Validated[Failures, (Submission, String)]]( + GenericFailure(s"No submission found for application ${app.id.value}").invalidNel[(Submission, String)] + ) { + case (submission, appName, nameValidationResult) => { + Apply[Validated[Failures, *]].map5( + isStandardNewJourneyApp(app), + isInTesting(app), + cond(submission.status.isAnsweredCompletely, "Submission has not been answered completely"), + cond(nameValidationResult != DuplicateName, "New name is a duplicate"), + cond(nameValidationResult != InvalidName, "New name is invalid") + ) { case _ => (submission, appName) } + } + } + } + + private def asEvents( + app: StoredApplication, + cmd: SubmitApplicationApprovalRequest, + submission: Submission, + isRequesterTheResponsibleIndividual: Boolean, + verificationId: Option[ResponsibleIndividualVerificationId], + importantSubmissionData: ImportantSubmissionData + ): NonEmptyList[ApplicationEvent] = { + val submittedEvent = ApplicationApprovalRequestSubmitted( + id = EventId.random, + applicationId = app.id, + eventDateTime = cmd.timestamp, + actor = cmd.actor, + submissionId = submission.id, + submissionIndex = submission.latestInstance.index, + requestingAdminName = cmd.requesterName, + requestingAdminEmail = cmd.requesterEmail + ) + + if (isRequesterTheResponsibleIndividual) { + NonEmptyList.one( + submittedEvent + ) + } else { + NonEmptyList.of( + submittedEvent, + ResponsibleIndividualVerificationRequired( + id = EventId.random, + applicationId = app.id, + eventDateTime = cmd.timestamp, + actor = cmd.actor, + applicationName = app.name, + requestingAdminName = cmd.requesterName, + requestingAdminEmail = cmd.requesterEmail, + responsibleIndividualName = importantSubmissionData.responsibleIndividual.fullName.value, + responsibleIndividualEmail = importantSubmissionData.responsibleIndividual.emailAddress, + submissionId = submission.id, + submissionIndex = submission.latestInstance.index, + verificationId = verificationId.get.value + ) + ) + } + } + + def process(app: StoredApplication, cmd: SubmitApplicationApprovalRequest): AppCmdResultT = { + import SubmissionDataExtracter._ + + logStartingApprovalRequestProcessing(app.id) + + for { + validated <- E.fromValidatedF(validate(app)) + (submission, appName) = validated + isRequesterTheResponsibleIndividual = SubmissionDataExtracter.isRequesterTheResponsibleIndividual(submission) + importantSubmissionData = getImportantSubmissionData(submission, cmd.requesterName, cmd.requesterEmail.text).get // Safe at this point + updatedApp = deriveNewAppDetails(app, isRequesterTheResponsibleIndividual, appName, importantSubmissionData, cmd) + savedApp <- E.liftF(applicationRepository.save(updatedApp)) + _ <- E.liftF(addTouAcceptanceIfNeeded(isRequesterTheResponsibleIndividual, updatedApp, submission, cmd)) + _ <- E.liftF(stateHistoryRepository.insert(createStateHistory(savedApp, cmd))) + updatedSubmission = Submission.submit(cmd.timestamp, cmd.requesterEmail.text)(submission) + savedSubmission <- E.liftF(submissionService.store(updatedSubmission)) + verificationId <- E.liftF(createTouUpliftVerificationRecordIfNeeded(isRequesterTheResponsibleIndividual, savedApp, submission, cmd)) + _ = logCompletedApprovalRequest(savedApp) + events = asEvents(app, cmd, submission, isRequesterTheResponsibleIndividual, verificationId, importantSubmissionData) + } yield (app, events) + } + + private def logStartingApprovalRequestProcessing(applicationId: ApplicationId) = { + logger.info(s"Approval-01: approval request made for appId:${applicationId}") + } + + private def logCompletedApprovalRequest(app: StoredApplication) = + logger.info(s"Approval-02: approval request (pending) application:${app.name} appId:${app.id} appState:${app.state.name}") + + private def validateApplicationName(appName: String, appId: ApplicationId): Future[ApplicationNameValidationResult] = + approvalsNamingService.validateApplicationName(appName, appId) + + private def deriveNewAppDetails( + existing: StoredApplication, + isRequesterTheResponsibleIndividual: Boolean, + applicationName: String, + importantSubmissionData: ImportantSubmissionData, + cmd: SubmitApplicationApprovalRequest + ): StoredApplication = + existing.copy( + name = applicationName, + normalisedName = applicationName.toLowerCase, + access = updateStandardData(existing.access, importantSubmissionData), + state = if (isRequesterTheResponsibleIndividual) { + existing.state.toPendingGatekeeperApproval(cmd.requesterEmail.text, cmd.requesterName, cmd.timestamp) + } else { + existing.state.toPendingResponsibleIndividualVerification(cmd.requesterEmail.text, cmd.requesterName, cmd.timestamp) + } + ) + + private def updateStandardData(existingAccess: Access, importantSubmissionData: ImportantSubmissionData): Access = { + existingAccess match { + case s: Access.Standard => s.copy(importantSubmissionData = Some(importantSubmissionData)) + case _ => existingAccess + } + } + + private def createStateHistory(snapshotApp: StoredApplication, cmd: SubmitApplicationApprovalRequest): StateHistory = + StateHistory( + snapshotApp.id, + snapshotApp.state.name, + Actors.AppCollaborator(cmd.requesterEmail), + Some(State.TESTING), + None, + cmd.timestamp + ) + + private def addTouAcceptanceIfNeeded( + addTouAcceptance: Boolean, + appWithoutTouAcceptance: StoredApplication, + submission: Submission, + cmd: SubmitApplicationApprovalRequest + ): Future[StoredApplication] = { + if (addTouAcceptance) { + val responsibleIndividual = ResponsibleIndividual.build(cmd.requesterName, cmd.requesterEmail.text) + val acceptance = TermsOfUseAcceptance(responsibleIndividual, cmd.timestamp, submission.id, submission.latestInstance.index) + applicationRepository.addApplicationTermsOfUseAcceptance(appWithoutTouAcceptance.id, acceptance) + } else { + Future.successful(appWithoutTouAcceptance) + } + } + + private def createTouUpliftVerificationRecordIfNeeded( + isRequesterTheResponsibleIndividual: Boolean, + application: StoredApplication, + submission: Submission, + cmd: SubmitApplicationApprovalRequest + ): Future[Option[ResponsibleIndividualVerificationId]] = { + if (!isRequesterTheResponsibleIndividual) { + for { + verification <- responsibleIndividualVerificationService.createNewTouUpliftVerification( + application, + submission.id, + submission.latestInstance.index, + cmd.requesterName, + cmd.requesterEmail + ) + } yield Some(verification.id) + + } else { + Future.successful(None) + } + } + +} diff --git a/conf/logback.xml b/conf/logback.xml index 2f58ab25f..100edff0a 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -14,12 +14,6 @@ - - - %date{ISO8601} level=[%level] logger=[%logger] thread=[%thread] rid=[not-available] user=[not-available] message=[%message] %replace(exception=[%xException]){'^exception=\[\]$',''}%n - - - logs/access.log @@ -39,10 +33,6 @@ - - - - diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index a42258440..e45c8728d 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -7,7 +7,7 @@ object AppDependencies { lazy val bootstrapVersion = "8.4.0" lazy val hmrcMongoVersion = "1.7.0" lazy val commonDomainVersion = "0.13.0" - lazy val applicationEventVersion = "0.51.0" + lazy val applicationEventVersion = "0.53.0" private lazy val compileDeps = Seq( "uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion, diff --git a/project/build.properties b/project/build.properties index e8a1e246e..04267b14a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.9 diff --git a/project/plugins.sbt b/project/plugins.sbt index a51b82b57..5281f72b2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ resolvers += Resolver.url("HMRC-open-artefacts-ivy", url("https://open.artefacts resolvers += "Typesafe Releases" at "https://repo.typesafe.com/typesafe/releases/" -addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.21.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.22.0") addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "2.5.0") addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") diff --git a/test/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationServiceSpec.scala b/test/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationServiceSpec.scala index 7d1346af6..b914b4467 100644 --- a/test/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationServiceSpec.scala +++ b/test/uk/gov/hmrc/apiplatform/modules/approvals/services/ResponsibleIndividualVerificationServiceSpec.scala @@ -27,10 +27,7 @@ import uk.gov.hmrc.apiplatform.modules.approvals.domain.models.{ ResponsibleIndividualVerificationWithDetails } import uk.gov.hmrc.apiplatform.modules.submissions.SubmissionsTestData -import uk.gov.hmrc.apiplatform.modules.submissions.mocks.SubmissionsServiceMockModule -import uk.gov.hmrc.thirdpartyapplication.mocks.ApplicationServiceMockModule -import uk.gov.hmrc.thirdpartyapplication.mocks.connectors.EmailConnectorMockModule -import uk.gov.hmrc.thirdpartyapplication.mocks.repository.{ApplicationRepositoryMockModule, ResponsibleIndividualVerificationRepositoryMockModule, StateHistoryRepositoryMockModule} +import uk.gov.hmrc.thirdpartyapplication.mocks.repository.{ResponsibleIndividualVerificationRepositoryMockModule, StateHistoryRepositoryMockModule} import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication import uk.gov.hmrc.thirdpartyapplication.util.{ApplicationTestData, AsyncHmrcSpec} @@ -39,12 +36,8 @@ class ResponsibleIndividualVerificationServiceSpec extends AsyncHmrcSpec { trait Setup extends ApplicationTestData with SubmissionsTestData - with ApplicationRepositoryMockModule with StateHistoryRepositoryMockModule - with ResponsibleIndividualVerificationRepositoryMockModule - with ApplicationServiceMockModule - with SubmissionsServiceMockModule - with EmailConnectorMockModule { + with ResponsibleIndividualVerificationRepositoryMockModule { val appName = "my shiny app" val submissionInstanceIndex = 0 @@ -69,11 +62,7 @@ class ResponsibleIndividualVerificationServiceSpec extends AsyncHmrcSpec { val underTest = new ResponsibleIndividualVerificationService( ResponsibleIndividualVerificationRepositoryMock.aMock, - ApplicationRepoMock.aMock, StateHistoryRepoMock.aMock, - ApplicationServiceMock.aMock, - SubmissionsServiceMock.aMock, - EmailConnectorMock.aMock, clock ) diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandlerSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandlerSpec.scala new file mode 100644 index 000000000..9aba0e1d7 --- /dev/null +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmitApplicationApprovalRequestCommandHandlerSpec.scala @@ -0,0 +1,212 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.thirdpartyapplication.services.commands.submission + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future.successful + +import uk.gov.hmrc.http.HeaderCarrier + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax +import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, ApplicationId, UserId} +import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock +import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access +import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models._ +import uk.gov.hmrc.apiplatform.modules.approvals.domain.models.ResponsibleIndividualVerificationId +import uk.gov.hmrc.apiplatform.modules.approvals.mocks.ResponsibleIndividualVerificationServiceMockModule +import uk.gov.hmrc.apiplatform.modules.approvals.services.ApprovalsNamingService +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.SubmitApplicationApprovalRequest +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents.{ApplicationApprovalRequestSubmitted, ResponsibleIndividualVerificationRequired} +import uk.gov.hmrc.apiplatform.modules.submissions.SubmissionsTestData +import uk.gov.hmrc.apiplatform.modules.submissions.domain.services.AnswerQuestion +import uk.gov.hmrc.apiplatform.modules.submissions.mocks.SubmissionsServiceMockModule +import uk.gov.hmrc.thirdpartyapplication.domain.models._ +import uk.gov.hmrc.thirdpartyapplication.mocks.repository.{ApplicationRepositoryMockModule, StateHistoryRepositoryMockModule, TermsOfUseInvitationRepositoryMockModule} +import uk.gov.hmrc.thirdpartyapplication.models.{ApplicationNameValidationResult, DuplicateName, InvalidName, ValidName} +import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandlerBaseSpec + +class SubmitApplicationApprovalRequestCommandHandlerSpec extends CommandHandlerBaseSpec with SubmissionsTestData { + + trait Setup extends SubmissionsServiceMockModule + with ApplicationRepositoryMockModule + with StateHistoryRepositoryMockModule + with TermsOfUseInvitationRepositoryMockModule + with ResponsibleIndividualVerificationServiceMockModule { + + implicit val hc: HeaderCarrier = HeaderCarrier() + + val submission = buildFullyAnsweredSubmission(aSubmission) + val submissionRequesterNotRI = AnswerQuestion.recordAnswer(submission, submission.questionIdsOfInterest.responsibleIndividualIsRequesterId, List("No")).value.submission + + val appAdminUserId = UserId.random + val appAdminEmail = "admin@example.com".toLaxEmail + val appAdminName = "Ms Admin" + + val importantSubmissionData = ImportantSubmissionData( + None, + ResponsibleIndividual.build("Bob", "bob@example.com"), + Set.empty, + TermsAndConditionsLocations.InDesktopSoftware, + PrivacyPolicyLocations.InDesktopSoftware, + List.empty + ) + + val app = anApplicationData(applicationId).copy( + state = ApplicationStateExamples.testing, + access = Access.Standard(List.empty, None, None, Set.empty, None, Some(importantSubmissionData)) + ) + + val ts = FixedClock.instant + val mockApprovalsNamingService: ApprovalsNamingService = mock[ApprovalsNamingService] + + def namingServiceReturns(result: ApplicationNameValidationResult) = + when(mockApprovalsNamingService.validateApplicationName(*, *[ApplicationId])).thenReturn(successful(result)) + + val underTest = new SubmitApplicationApprovalRequestCommandHandler( + SubmissionsServiceMock.aMock, + ApplicationRepoMock.aMock, + StateHistoryRepoMock.aMock, + TermsOfUseInvitationRepositoryMock.aMock, + mockApprovalsNamingService, + ResponsibleIndividualVerificationServiceMock.aMock + ) + } + + "process" should { + "create correct event for a valid request with a standard app and submission where requester is the responsible individual" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(ValidName) + ApplicationRepoMock.Save.thenReturn(app) + StateHistoryRepoMock.Insert.succeeds() + SubmissionsServiceMock.Store.thenReturn() + ApplicationRepoMock.AddApplicationTermsOfUseAcceptance.thenReturn(app) + + val result = await(underTest.process(app, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)).value).value + + inside(result) { case (app, events) => + events should have size 1 + + inside(events.head) { + case event: ApplicationApprovalRequestSubmitted => + event.applicationId shouldBe applicationId + event.eventDateTime shouldBe ts + event.actor shouldBe Actors.AppCollaborator(appAdminEmail) + event.submissionIndex shouldBe submission.latestInstance.index + event.submissionId.value shouldBe submission.id.value + event.requestingAdminEmail shouldBe appAdminEmail + event.requestingAdminName shouldBe appAdminName + } + } + } + + "create correct event for a valid request with a standard app and submission where requester is NOT the responsible individual" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submissionRequesterNotRI) + namingServiceReturns(ValidName) + ApplicationRepoMock.Save.thenReturn(app) + StateHistoryRepoMock.Insert.succeeds() + SubmissionsServiceMock.Store.thenReturn() + ApplicationRepoMock.AddApplicationTermsOfUseAcceptance.thenReturn(app) + val code = ResponsibleIndividualVerificationId.random + ResponsibleIndividualVerificationServiceMock.CreateNewTouUpliftVerification.thenCreateNewTouUpliftVerification(code) + + val result = await(underTest.process(app, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)).value).value + + inside(result) { case (app, events) => + events should have size 2 + + inside(events.head) { + case event: ApplicationApprovalRequestSubmitted => + event.applicationId shouldBe applicationId + event.eventDateTime shouldBe ts + event.actor shouldBe Actors.AppCollaborator(appAdminEmail) + event.submissionIndex shouldBe submission.latestInstance.index + event.submissionId.value shouldBe submission.id.value + event.requestingAdminEmail shouldBe appAdminEmail + event.requestingAdminName shouldBe appAdminName + } + + inside(events.tail.head) { + case event: ResponsibleIndividualVerificationRequired => + event.applicationId shouldBe applicationId + event.eventDateTime shouldBe ts + event.actor shouldBe Actors.AppCollaborator(appAdminEmail) + event.submissionIndex shouldBe submission.latestInstance.index + event.submissionId.value shouldBe submission.id.value + event.requestingAdminEmail shouldBe appAdminEmail + event.requestingAdminName shouldBe appAdminName + event.verificationId shouldBe code.value + } + } + } + + "return an error if no submission is found for the application" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturnNone() + + checkFailsWith(s"No submission found for application $applicationId") { + underTest.process(app, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + + "return an error if the application is non-standard" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(ValidName) + val nonStandardApp = app.copy(access = Access.Ropc(Set.empty)) + + checkFailsWith("Must be a standard new journey application") { + underTest.process(nonStandardApp, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + + "return an error if the application is old journey" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(ValidName) + val oldJourneyApp = app.copy(access = Access.Standard(List.empty, None, None, Set.empty, None, None)) + + checkFailsWith("Must be a standard new journey application") { + underTest.process(oldJourneyApp, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + + "return an error if the application name is not unique" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(DuplicateName) + + checkFailsWith("New name is a duplicate") { + underTest.process(app, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + + "return an error if the application name is invalid" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(InvalidName) + + checkFailsWith("New name is invalid") { + underTest.process(app, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + + "return an error if the application is not in TESTING" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + namingServiceReturns(ValidName) + val notTestingApp = app.copy(state = ApplicationStateExamples.pendingGatekeeperApproval("someone@example.com", "Someone")) + + checkFailsWith("App is not in TESTING state") { + underTest.process(notTestingApp, SubmitApplicationApprovalRequest(Actors.AppCollaborator(appAdminEmail), instant, appAdminName, appAdminEmail)) + } + } + } +} diff --git a/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala b/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala index ddb1edbcf..52ebabe73 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala @@ -83,6 +83,7 @@ abstract class ApplicationCommandDispatcherUtils extends AsyncHmrcSpec val mockDeclineResponsibleIndividualDidNotVerifyCommandHandler: DeclineResponsibleIndividualDidNotVerifyCommandHandler = mock[DeclineResponsibleIndividualDidNotVerifyCommandHandler] + val mockSubmitApplicationApprovalRequestCommandHandler: SubmitApplicationApprovalRequestCommandHandler = mock[SubmitApplicationApprovalRequestCommandHandler] val mockDeclineApplicationApprovalRequestCommandHandler: DeclineApplicationApprovalRequestCommandHandler = mock[DeclineApplicationApprovalRequestCommandHandler] val mockDeleteApplicationByCollaboratorCommandHandler: DeleteApplicationByCollaboratorCommandHandler = mock[DeleteApplicationByCollaboratorCommandHandler] val mockDeleteApplicationByGatekeeperCommandHandler: DeleteApplicationByGatekeeperCommandHandler = mock[DeleteApplicationByGatekeeperCommandHandler] @@ -181,7 +182,8 @@ abstract class ApplicationCommandDispatcherUtils extends AsyncHmrcSpec mockDeclineApplicationApprovalRequestCommandHandler, mockDeclineResponsibleIndividualCommandHandler, mockDeclineResponsibleIndividualDidNotVerifyCommandHandler, - mockResendRequesterEmailVerificationCommandHandler + mockResendRequesterEmailVerificationCommandHandler, + mockSubmitApplicationApprovalRequestCommandHandler ) val subscriptionCommandsProcessor = new SubscriptionCommandsProcessor(