Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7c02966
[APB-11100] model
paweldigital Apr 16, 2026
4477295
[APB-11100] model
paweldigital Apr 16, 2026
232a634
[WG][APB-11100] Do actions per Pav description
gitwojciech Apr 21, 2026
1c4e007
Merge branch 'main' into APB-11100_2
gitwojciech Apr 21, 2026
d9d3647
[WG][APB-11100] Do actions per Pav description, merge main
gitwojciech Apr 21, 2026
f35b4f4
[WG][APB-11100] Do actions per Pav description, RiskingFile repo
gitwojciech Apr 21, 2026
853c591
[WG][APB-11100] lastUpdatedAt - update when risking responce
gitwojciech Apr 22, 2026
c016314
[WG][APB-11100] lastUpdatedAt - update when risking responce 2
gitwojciech Apr 22, 2026
86b63ef
[WG][APB-11100] lastUpdatedAt - update when risking responce PR comme…
gitwojciech Apr 23, 2026
d9be6f4
PP WG [APB-11100] alignement
paweldigital Apr 23, 2026
c5f5830
[WG][APB-11100] make code compile plus tests
gitwojciech Apr 24, 2026
d169df3
PP WG [APB-11100] readme
paweldigital Apr 24, 2026
fe20e1a
PP WG [APB-11100] comments
paweldigital Apr 24, 2026
0a0b6f6
Merge branch 'main' into APB-11100
paweldigital Apr 24, 2026
6c1d1a3
PP WG [APB-11100] fixes after merge
paweldigital Apr 24, 2026
7dd4bc5
PP WG [APB-11100] fixes in tests
paweldigital Apr 24, 2026
6059914
[WG][APB-11045] Risking outcomes - emails - success
gitwojciech Apr 29, 2026
e646367
[APB-11100] fixes after sync from FE
paweldigital Apr 29, 2026
d4099ae
[WG][APB-11015] Risking outcomes - emails - FailedNonFixable
gitwojciech Apr 29, 2026
16b4cf0
[APB-11100] refactored production cod
paweldigital Apr 29, 2026
b05ea41
[WG][APB-11015] Risking outcomes - emails - FailedNonFixable, SoleTra…
gitwojciech May 1, 2026
08ad1f1
Merge branch 'APB-11100_2' into APB-11045
gitwojciech May 1, 2026
05e90e0
[DC-8799][APB-11045][APB-11015] agent registartion emails - success, …
gitwojciech May 1, 2026
04d21ea
[APB-11100] tests commented out, wip
paweldigital May 1, 2026
5f2e1de
[APB-11100] ProcessInSequenceSpec
paweldigital May 1, 2026
33b9ad9
[APB-11100] Test Data for Risking POC
paweldigital May 5, 2026
92720e9
Merge branch 'main' into APB-11100
paweldigital May 5, 2026
cb1cf32
[APB-11100] Test Data fixes
paweldigital May 5, 2026
9de4d92
Merge branch 'APB-11100_2' into APB-11045
gitwojciech May 5, 2026
cb938ac
[APB-11045] agent registartion emails - remove testing
gitwojciech May 5, 2026
a0eb410
[APB-11100] More TD Fixes and compiling first test
paweldigital May 5, 2026
ff2a7e7
[APB-11100] first working test
paweldigital May 5, 2026
246c12b
[APB-11100] small cleanup
paweldigital May 5, 2026
c3f7ba1
[APB-11100] getRiskingProgressForApplicant test
paweldigital May 5, 2026
c9b5789
[APB-11100] risking results for applicant
paweldigital May 5, 2026
aa08cd3
[APB-11100] sync
paweldigital May 5, 2026
45cad5a
Merge branch 'APB-11100_2' into APB-11045
gitwojciech May 6, 2026
dd34816
Merge branch 'main' into APB-11045
gitwojciech May 6, 2026
357f824
[APB-11045] agent registartion emails - after merge fix
gitwojciech May 6, 2026
8023e25
[APB-11045] agent registartion emails - after merge fix2
gitwojciech May 6, 2026
c28f356
Merge branch 'main' into APB-11045
gitwojciech May 7, 2026
aa8850b
Merge branch 'main' into APB-11045
gitwojciech May 8, 2026
63e98e9
[APB-11045] agent registartion emails - another merge fixes
gitwojciech May 8, 2026
6cd0888
[APB-11045] agent registartion emails - PR comments
gitwojciech May 8, 2026
f4839cb
[APB-11045] agent registartion emails - PR comments 2
gitwojciech May 8, 2026
d2dcab6
[APB-11045] agent registartion emails - better emails for indyviduals
gitwojciech May 9, 2026
cb08a60
[APB-11045] agent registartion emails - more filters
gitwojciech May 9, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class AppConfig @Inject() (
def getConfString(key: String): String = servicesConfig.getConfString(key, throw new RuntimeException(s"config '$key' not found"))

val appName: String = config.get[String]("appName")
val emailBaseUrl: String = servicesConfig.baseUrl("email")
val enrolmentStoreProxyBaseUrl: String = servicesConfig.baseUrl("enrolment-store-proxy")
val hmrcAsAgentEnrolment: Enrolment = Enrolment(key = "HMRC-AS-AGENT")
val hipBaseUrl: String = servicesConfig.baseUrl("hip")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2026 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.agentregistrationrisking.connectors

import play.api.http.Status.ACCEPTED
import uk.gov.hmrc.agentregistration.shared.util.Errors
import uk.gov.hmrc.agentregistrationrisking.config.AppConfig
import uk.gov.hmrc.agentregistrationrisking.model.EmailInformation
import uk.gov.hmrc.agentregistrationrisking.util.FutureUtil.andLogOnFailure
import uk.gov.hmrc.http.client.HttpClientV2

import javax.inject.Inject
import javax.inject.Singleton
import scala.concurrent.ExecutionContext

@Singleton
class EmailConnector @Inject() (
appConfig: AppConfig,
httpClient: HttpClientV2
)(using ExecutionContext)
extends Connector:

def sendEmail(emailInformation: EmailInformation)(using RequestHeader): Future[Unit] =
val url: URL = url"$baseUrl/hmrc/email"
httpClient
.post(url)
.withBody(Json.toJson(emailInformation))
.execute[HttpResponse]
.map: response =>
response.status match
case ACCEPTED => ()
case status =>
Errors.throwUpstreamErrorResponse(
httpMethod = "POST",
url = url,
status = status,
response = response,
info = s"Failed to send email for template ${emailInformation.templateId}"
)
.andLogOnFailure(s"Failed to send email for template ${emailInformation.templateId}")

private val baseUrl: String = appConfig.emailBaseUrl
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ import com.google.inject.Inject
import play.api.mvc.Action
import play.api.mvc.ControllerComponents
import play.api.mvc.RequestHeader

import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import uk.gov.hmrc.agentregistrationrisking.action.Actions
import uk.gov.hmrc.agentregistrationrisking.model.sdes.*
import uk.gov.hmrc.agentregistrationrisking.services.EmailService
import uk.gov.hmrc.agentregistrationrisking.services.RiskingResultsService
import uk.gov.hmrc.agentregistrationrisking.services.SdesProxyService
import uk.gov.hmrc.agentregistrationrisking.services.SubscriptionService
import uk.gov.hmrc.agentregistrationrisking.util.ProcessInSequence

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

class SdesNotificationController @Inject() (
cc: ControllerComponents,
actions: Actions,
riskingResultsService: RiskingResultsService,
subscriptionService: SubscriptionService
subscriptionService: SubscriptionService,
emailService: EmailService
)(using ExecutionContext)
extends BackendController(cc):

Expand All @@ -59,4 +59,6 @@ extends BackendController(cc):
(for
_ <- riskingResultsService.processResultsFiles()
_ <- subscriptionService.subscribeApprovedApplications()
_ <- emailService.findAndSendRegisteredEmail()
_ <- emailService.findAndSendNonFixableFailureEmails()
yield ()).recover { case ex: Exception => logger.error(s"Error processing file ready notification", ex) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2026 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.agentregistrationrisking.model

import play.api.libs.json.Json
import play.api.libs.json.OFormat

final case class EmailInformation(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider renaming EmailInformation to SendEmailRequest to better reflect that it represents the actual HTTP request used when calling the email microservice? It seems we already follow this naming convention in other connectors, so it might be good to stay consistent with that.

to: Seq[String],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if it might be worth using a dedicated type for the email address, such as EmailAddress, instead of a plain String. It already seems to handle the necessary JSON serialization, and it could make the code more type-safe overall.

to: Seq[String] feels a bit unclear compared to to: Seq[EmailAddress], does it?

templateId: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider using a dedicated type for this as well? Since we only have a small set of email template values, it might make sense to model them as an enum. That way, we could reduce the risk of passing incompatible or unsupported values.

parameters: Map[String, String],
force: Boolean = false,
eventUrl: Option[String] = None,
onSendUrl: Option[String] = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if those fields are still needed, as they don’t seem to be used anywhere. Maybe it would make sense to remove them to avoid maintaining code that isn’t actually in use?

)

object EmailInformation:
given OFormat[EmailInformation] = Json.format[EmailInformation]
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ extends Repo[ApplicationReference, ApplicationForRisking](
)
).toFuture()

// def findReadyForSubscription(): Future[Seq[ApplicationForRisking]] = collection
Copy link
Copy Markdown
Contributor

@paweldigital paweldigital May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering what the purpose of this commented-out code is.

// .find(
// Filters.and(
// Filters.size("failures", 0),
// Filters.eq("isSubscribed", false)
// )
// ).toFuture()

def findApplicationsPendingAction(): Future[Seq[ApplicationForRisking]] = collection
.find(
Filters.and(
Filters.exists(FieldNames.entityRiskingResult),
Filters.eq(FieldNames.isSubscribed, false),
Filters.eq("isEmailSent", false)
)
).toFuture()

// def findReadyForSubscription(): Future[Seq[ApplicationForRisking]] = collection
// .find(
// Filters.and(
Expand All @@ -95,6 +112,22 @@ extends Repo[ApplicationReference, ApplicationForRisking](
)
).toFuture()
}
def findSubscribedReadyForSuccessEmail(): Future[Seq[ApplicationForRisking]] = collection
.find(
Filters.and(
Filters.eq("isSubscribed", true),
Filters.eq("isEmailSent", false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider using a FieldNames object to define these fields? It makes them easier to manage and help avoid typos in queries, especially since this has already happened in the project before.

)
).toFuture()

def updateEmailSent(applicationReference: ApplicationReference): Future[UpdateResult] = collection
.updateOne(
Filters.eq(FieldNames.applicationReference, applicationReference.value),
Updates.combine(
Updates.set("isEmailSent", true),
Updates.set(FieldNames.lastUpdatedAt, Instant.now(clock).toString)
)
).toFuture()

// when named ApplicationForRiskingRepo, Scala 3 compiler complains
// about cyclic reference error during compilation ...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2026 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.agentregistrationrisking.services

import uk.gov.hmrc.agentregistration.shared.risking.RiskingOutcome
import uk.gov.hmrc.agentregistration.shared.util.SafeEquals.===
import uk.gov.hmrc.agentregistrationrisking.model.*
import uk.gov.hmrc.agentregistrationrisking.repository.ApplicationForRiskingRepo
import uk.gov.hmrc.agentregistrationrisking.repository.IndividualForRiskingRepo
import uk.gov.hmrc.agentregistrationrisking.util.RequestAwareLogging
import uk.gov.hmrc.agentregistrationrisking.services.RiskingOutcomeHelper._

import java.time.Clock
import javax.inject.Inject
import javax.inject.Singleton
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

@Singleton
class ApplicationStatusService @Inject() (
applicationForRiskingRepo: ApplicationForRiskingRepo,
individualForRiskingRepo: IndividualForRiskingRepo
)(using
ExecutionContext,
Clock
)
extends RequestAwareLogging:

def findApprovedReadyToSubscribe(): Future[Seq[ApplicationForRisking]] = getApplicationsPendingActionWithIndividuals
.map(filterApprovedApplicationsWithIndividuals)
.map(_.map(_.application))

def findNonFixableReadyForFailureEmail(): Future[Seq[ApplicationWithIndividuals]] = getApplicationsPendingActionWithIndividuals
.map(filterNonFixableApplicationsWithIndividuals)

private def getApplicationsPendingActionWithIndividuals: Future[Seq[ApplicationWithIndividuals]] =
for
applications <- applicationForRiskingRepo.findApplicationsPendingAction()
individuals <- individualForRiskingRepo.findByApplicationReferences(applications.map(_.applicationReference))
yield ApplicationWithIndividuals
.merge(applications, individuals)
.filter(_.individuals.forall(_.individualRiskingResult.isDefined))

private def filterApprovedApplicationsWithIndividuals(applicationsWithIndividuals: Seq[ApplicationWithIndividuals]): Seq[ApplicationWithIndividuals] =
applicationsWithIndividuals.filter: appWithIndividuals =>
RiskingOutcomeHelper
.computeRiskingOutcome(appWithIndividuals)
.exists(_ === RiskingOutcome.Approved)

private def filterNonFixableApplicationsWithIndividuals(applicationsWithIndividuals: Seq[ApplicationWithIndividuals]): Seq[ApplicationWithIndividuals] =
applicationsWithIndividuals
.filter: appWithIndividuals =>
RiskingOutcomeHelper
.computeRiskingOutcome(appWithIndividuals)
.exists(_ === RiskingOutcome.FailedNonFixable)
.map: appWithIndividuals =>
val nonFixableIndividuals = appWithIndividuals.individuals.filter: individual =>
individual.individualRiskingResult.exists(_.failures.outcome === RiskingOutcome.FailedNonFixable)
ApplicationWithIndividuals(appWithIndividuals.application, nonFixableIndividuals)

private def filterFixableApplicationsWithIndividuals(applicationsWithIndividuals: Seq[ApplicationWithIndividuals]): Seq[ApplicationWithIndividuals] =
applicationsWithIndividuals
.filter: appWithIndividuals =>
RiskingOutcomeHelper
.computeRiskingOutcome(appWithIndividuals)
.exists(_ === RiskingOutcome.FailedFixable)
.map: appWithIndividuals =>
val fixableIndividuals = appWithIndividuals.individuals.filter: individual =>
individual.individualRiskingResult.exists(_.failures.outcome === RiskingOutcome.FailedFixable)
ApplicationWithIndividuals(appWithIndividuals.application, fixableIndividuals)
Loading