Skip to content

Commit

Permalink
APIS-7016 Add command and event for Resend Verification email (#494)
Browse files Browse the repository at this point in the history
* APIS-7016 Add new command ResendRequesterEmailVerification handler etc

* APIS-7016 Add new command ResendRequesterEmailVerification handler etc

* APIS-7016 Add new command ResendRequesterEmailVerification handler etc

* APIS-7016 Add new command ResendRequesterEmailVerification handler etc

* APIS-7016 Add new command ResendRequesterEmailVerification handler etc

* APIS-7016 Add new command ResendRequesterEmailVerification handler etc
  • Loading branch information
petekirby-ee authored May 16, 2024
1 parent e4f66af commit 0f48391
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class GatekeeperController @Inject() (
JsErrorResponse(INVALID_STATE_TRANSITION, "Application is not in state 'PENDING_REQUESTER_VERIFICATION'")
)

@deprecated
def resendVerification(id: ApplicationId) = requiresAuthentication().async(parse.json) {
implicit request =>
withJsonBody[ResendVerificationRequest] { resendVerificationPayload =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class AuditService @Inject() (val auditConnector: AuditConnector, val submission
case evt: SandboxApplicationPrivacyPolicyUrlRemoved => auditSandboxApplicationPrivacyPolicyUrlRemoved(app, evt)
case evt: SandboxApplicationTermsAndConditionsUrlChanged => auditSandboxApplicationTermsAndConditionsUrlChanged(app, evt)
case evt: SandboxApplicationTermsAndConditionsUrlRemoved => auditSandboxApplicationTermsAndConditionsUrlRemoved(app, evt)
case evt: RequesterEmailVerificationResent => auditRequesterEmailVerificationResent(app, evt)
case _ => Future.successful(None)
}
}
Expand Down Expand Up @@ -228,6 +229,16 @@ class AuditService @Inject() (val auditConnector: AuditConnector, val submission
))
.toOption
.value

private def auditRequesterEmailVerificationResent(app: StoredApplication, evt: RequesterEmailVerificationResent)(implicit hc: HeaderCarrier): Future[Option[AuditResult]] =
E.liftF(auditGatekeeperAction(
evt.actor.user,
app,
ApplicationVerificationResent
))
.toOption
.value

}

sealed trait AuditAction {
Expand Down Expand Up @@ -317,7 +328,7 @@ object AuditAction {
val auditType = "ApplicationNameDeclinedByGatekeeper"
}

case object ApplicationVerficationResent extends AuditAction {
case object ApplicationVerificationResent extends AuditAction {
val name = "verification email has been resent"
val auditType = "VerificationEmailResentByGatekeeper"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class GatekeeperService @Inject() (
} yield history
}

@deprecated
def resendVerification(applicationId: ApplicationId, gatekeeperUserId: String)(implicit hc: HeaderCarrier): Future[ApplicationStateChange] = {
def rejectIfNotPendingVerification(existing: StoredApplication) = {
existing.state.requireState(State.PENDING_REQUESTER_VERIFICATION, State.PENDING_REQUESTER_VERIFICATION)
Expand All @@ -103,7 +104,7 @@ class GatekeeperService @Inject() (
for {
app <- fetchApp(applicationId)
_ = rejectIfNotPendingVerification(app)
_ = auditService.auditGatekeeperAction(gatekeeperUserId, app, ApplicationVerficationResent)
_ = auditService.auditGatekeeperAction(gatekeeperUserId, app, ApplicationVerificationResent)
_ = recoverAll(sendEmails(app))
} yield UpliftApproved

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ object CommandHandler extends BaseCommandHandler[(StoredApplication, NonEmptyLis
GenericFailure("App is not in PENDING_RESPONSIBLE_INDIVIDUAL_VERIFICATION state")
)

def isPendingRequesterVerification(app: StoredApplication) =
cond(
app.isPendingRequesterVerification,
GenericFailure("App is not in PENDING_REQUESTER_VERIFICATION state")
)

def isInTesting(app: StoredApplication) =
cond(
app.isInTesting,
Expand Down Expand Up @@ -201,4 +207,10 @@ object CommandHandler extends BaseCommandHandler[(StoredApplication, NonEmptyLis

def appHasLessThanLimitOfSecrets(app: StoredApplication, clientSecretLimit: Int): Validated[Failures, Unit] =
cond(app.tokens.production.clientSecrets.size < clientSecretLimit, GenericFailure("Client secret limit has been exceeded"))

def getVerificationCode(app: StoredApplication): Option[String] =
app.state.verificationCode

def ensureVerificationCodeDefined(app: StoredApplication) =
mustBeDefined(getVerificationCode(app), "The verificationCode has not been set for this application")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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, Validated}
import cats.syntax.validated._

import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, LaxEmailAddress}
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.ResendRequesterEmailVerification
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.{CommandFailure, CommandFailures}
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.services.SubmissionsService
import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication
import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandler

@Singleton
class ResendRequesterEmailVerificationCommandHandler @Inject() (
submissionService: SubmissionsService
)(implicit val ec: ExecutionContext
) extends CommandHandler {

import CommandHandler._
import CommandFailures._

private def validate(app: StoredApplication): Future[Validated[Failures, (LaxEmailAddress, String, Submission)]] = {

def checkSubmission(maybeSubmission: Option[Submission]): Validated[Failures, Submission] = {
lazy val fails: CommandFailure = GenericFailure(s"No submission found for application ${app.id.value}")
maybeSubmission.fold(fails.invalidNel[Submission])(_.validNel[CommandFailure])
}

submissionService.fetchLatest(app.id).map { maybeSubmission =>
Apply[Validated[Failures, *]].map6(
isStandardNewJourneyApp(app),
isPendingRequesterVerification(app),
ensureRequesterEmailDefined(app),
ensureRequesterNameDefined(app),
ensureVerificationCodeDefined(app),
checkSubmission(maybeSubmission)
) { case (_, _, requesterEmail, requesterName, _, submission) => (requesterEmail, requesterName, submission) }
}
}

private def asEvents(
app: StoredApplication,
cmd: ResendRequesterEmailVerification,
submission: Submission,
requesterEmail: LaxEmailAddress,
requesterName: String
): (RequesterEmailVerificationResent) = {
(
RequesterEmailVerificationResent(
id = EventId.random,
applicationId = app.id,
eventDateTime = cmd.timestamp,
actor = Actors.GatekeeperUser(cmd.gatekeeperUser),
submissionId = submission.id,
submissionIndex = submission.latestInstance.index,
requestingAdminName = requesterName,
requestingAdminEmail = requesterEmail
)
)
}

def process(app: StoredApplication, cmd: ResendRequesterEmailVerification): AppCmdResultT = {
for {
validated <- E.fromValidatedF(validate(app))
(requesterEmail, requesterName, submission) = validated
emailResent = asEvents(app, cmd, submission, requesterEmail, requesterName)
} yield (app, NonEmptyList.one(emailResent))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class SubmissionCommandsProcessor @Inject() (
verifyResponsibleIndividualCmdHdlr: VerifyResponsibleIndividualCommandHandler,
declineApplicationApprovalRequestCommandHandler: DeclineApplicationApprovalRequestCommandHandler,
declineResponsibleIndividualCmdHdlr: DeclineResponsibleIndividualCommandHandler,
declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler
declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler,
resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler
) {
import CommandHandler._
import ApplicationCommands._
Expand All @@ -41,5 +42,6 @@ class SubmissionCommandsProcessor @Inject() (
case cmd: DeclineApplicationApprovalRequest => declineApplicationApprovalRequestCommandHandler.process(app, cmd)
case cmd: DeclineResponsibleIndividual => declineResponsibleIndividualCmdHdlr.process(app, cmd)
case cmd: DeclineResponsibleIndividualDidNotVerify => declineResponsibleIndividualDidNotVerifyCmdHdlr.process(app, cmd)
case cmd: ResendRequesterEmailVerification => resendRequesterEmailVerificationCmdHdlr.process(app, cmd)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class NotificationService @Inject() (emailConnector: EmailConnector)(implicit va
case evt: TermsOfUsePassed => TermsOfUsePassedNotification.sendAdviceEmail(emailConnector, app, evt)
case evt: ProductionCredentialsApplicationDeleted => ProductionCredentialsApplicationDeletedNotification.sendAdviceEmail(emailConnector, app, evt)
case evt: ApplicationDeletedByGatekeeper => ApplicationDeletedByGatekeeperNotification.sendAdviceEmail(emailConnector, app, evt)
case evt: RequesterEmailVerificationResent => VerifyRequesterEmailNotification.sendAdviceEmail(emailConnector, app, evt)
case _ => Future.successful(HasSucceeded)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.notifications

import scala.concurrent.Future

import uk.gov.hmrc.http.HeaderCarrier

import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents.RequesterEmailVerificationResent
import uk.gov.hmrc.thirdpartyapplication.connector.EmailConnector
import uk.gov.hmrc.thirdpartyapplication.models.HasSucceeded
import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication

object VerifyRequesterEmailNotification {

def sendAdviceEmail(emailConnector: EmailConnector, app: StoredApplication, event: RequesterEmailVerificationResent)(implicit hc: HeaderCarrier): Future[HasSucceeded] = {
val verificationCode: String = app.state.verificationCode.getOrElse("") // Note verificationCode has already been validated
emailConnector.sendApplicationApprovedAdminConfirmation(
app.name,
verificationCode,
Set(event.requestingAdminEmail)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ trait EmailConnectorMockModule extends MockitoSugar with ArgumentMatchersSugar {
def thenReturnSuccess() = {
when(aMock.sendApplicationApprovedAdminConfirmation(*, *, *)(*)).thenReturn(successful(HasSucceeded))
}

def verifyCalledWith(applicationName: String, code: String, recipients: Set[LaxEmailAddress]): Future[HasSucceeded] = {
verify.sendApplicationApprovedAdminConfirmation(eqTo(applicationName), eqTo(code), eqTo(recipients))(*)
}
}

object SendVerifyResponsibleIndividualNotification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ApplicationCommandDispatcherSpec
mockChangeResponsibleIndividualToOtherCommandHandler,
mockVerifyResponsibleIndividualCommandHandler,
mockDeclineResponsibleIndividualCommandHandler,
mockResendRequesterEmailVerificationCommandHandler,
mockDeclineResponsibleIndividualDidNotVerifyCommandHandler,
mockChangeSandboxApplicationNameCommandHandler,
mockChangeSandboxApplicationDescriptionCommandHandler,
Expand Down Expand Up @@ -465,6 +466,40 @@ class ApplicationCommandDispatcherSpec

}

"ResendRequesterEmailVerification is received" should {
val timestamp = instant
val actor = Actors.GatekeeperUser(gatekeeperUser)

val cmd = ResendRequesterEmailVerification(actor.user, timestamp)
val evt = ApplicationEvents.RequesterEmailVerificationResent(
EventId.random,
applicationId,
instant,
actor,
SubmissionId(SubmissionId.random.value),
1,
"adminName",
"anAdminEmail".toLaxEmail
)

"call ResendRequesterEmailVerification Handler and relevant common services if application exists" in new Setup {
primeCommonServiceSuccess()

when(mockResendRequesterEmailVerificationCommandHandler.process(*[StoredApplication], *[ResendRequesterEmailVerification])).thenReturn(E.pure((
applicationData,
NonEmptyList.one(evt)
)))

await(underTest.dispatch(applicationId, cmd, Set.empty).value)
verifyServicesCalledWithEvent(evt)
verifyNoneButGivenCmmandHandlerCalled[ResendRequesterEmailVerificationCommandHandler]()
}

"bubble up exception when application fetch fails" in new Setup {
testFailure(cmd)
}
}

"DeleteApplicationByCollaborator is received" should {

val cmd = DeleteApplicationByCollaborator(UserId.random, reasons, timestamp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class GatekeeperServiceSpec
await(underTest.resendVerification(applicationId, gatekeeperUserId))

AuditServiceMock.AuditGatekeeperAction.verifyUserName() shouldBe gatekeeperUserId
AuditServiceMock.AuditGatekeeperAction.verifyAction() shouldBe AuditAction.ApplicationVerficationResent
AuditServiceMock.AuditGatekeeperAction.verifyAction() shouldBe AuditAction.ApplicationVerificationResent
}

"fail with InvalidStateTransition when the application is not in PENDING_REQUESTER_VERIFICATION state" in new Setup {
Expand Down
Loading

0 comments on commit 0f48391

Please sign in to comment.