Skip to content

Commit

Permalink
APIS-7030 New handler for SubmitApplicationApprovalRequest command (#498
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
petekirby-ee authored Jun 5, 2024
1 parent 2927981 commit 1492243
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -241,7 +241,7 @@ class RequestApprovalsService @Inject() (
}
}

private def sendProdCredsVerificationEmailIfNeeded(
private def sendResponsibleIndividualVerificationEmailIfNeeded(
isRequesterTheResponsibleIndividual: Boolean,
application: StoredApplication,
submission: Submission,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class SubmissionCommandsProcessor @Inject() (
declineApplicationApprovalRequestCommandHandler: DeclineApplicationApprovalRequestCommandHandler,
declineResponsibleIndividualCmdHdlr: DeclineResponsibleIndividualCommandHandler,
declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler,
resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler
resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler,
submitApplicationApprovalRequestCmdHdlr: SubmitApplicationApprovalRequestCommandHandler
) {
import CommandHandler._
import ApplicationCommands._
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
10 changes: 0 additions & 10 deletions conf/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@
</encoder>
</appender>

<appender name="STDOUT_IGNORE_NETTY" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%date{ISO8601} level=[%level] logger=[%logger] thread=[%thread] rid=[not-available] user=[not-available] message=[%message] %replace(exception=[%xException]){'^exception=\[\]$',''}%n</pattern>
</encoder>
</appender>

<appender name="ACCESS_LOG_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/access.log</file>
<encoder>
Expand All @@ -39,10 +33,6 @@
<appender-ref ref="ACCESS_LOG_FILE" />
</logger>

<logger name="com.ning.http.client.providers.netty" additivity="false">
<appender-ref ref="STDOUT_IGNORE_NETTY" />
</logger>

<logger name="uk.gov" level="${logger.uk.gov:-WARN}"/>

<logger name="application" level="INFO"/>
Expand Down
2 changes: 1 addition & 1 deletion project/AppDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.9.7
sbt.version=1.9.9
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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
Expand All @@ -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
)

Expand Down
Loading

0 comments on commit 1492243

Please sign in to comment.