Skip to content

Apb 11788 - Special Case to allow setting previously Failed applications to be Success - #143

Open
geoffreywatson wants to merge 14 commits into
mainfrom
APB-11788
Open

geoffreywatson wants to merge 14 commits into
mainfrom
APB-11788

Conversation

@geoffreywatson

Copy link
Copy Markdown
Contributor
  • Applications that have been approved following an appeals process may be added to a new config array (the id's) so the results can be overturned and the Failed state is instead set to Success.

  • Unsets the previous Failed responses and replaces with Success, allowing the applications to be subscribed to ASA, with all side effects (new emails and audit events).

@platops-pr-bot

Copy link
Copy Markdown

nubz and others added 8 commits July 6, 2026 13:39
* APB-11588 feature flag sending notifications for failed fixable

Signed-off-by: pav <173694350+paweldigital@users.noreply.github.com>

* APB-11588 PR

Signed-off-by: pav <173694350+paweldigital@users.noreply.github.com>

---------

Signed-off-by: pav <173694350+paweldigital@users.noreply.github.com>
…g into APB-11788

Signed-off-by: geoffreywatson <9043337+geoffreywatson@users.noreply.github.com>
@geoffreywatson geoffreywatson changed the title Apb 11788 Apb 11788 - Special Case to allow setting previously Failed applications to be Success Jul 6, 2026
to be Approved instead (to allow for an ASA to be created). For example, where an applicant has successfully
appealed a Failed Non-Fixable.
*/
def specialCaseApprovePreviouslyFailedApplications()(using request: RequestHeader): Future[Unit] =

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 do not see apart from testing where that function is called

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah, oh dear...I think Pav moved changed the functionality so all that logic is run from a scheduler instead....I manually tested this before rebasing so I'll need to add this method call to that scheduled process.

)
).toFuture().map(_ => ())
}

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.

In this project we decided not to do field level updates. We update whole records

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.

Few flags that needs to reset too isEmailSent=false, correctiveActionExpiryDate=None, emailSentAt=None , isSubscribed=false. Please note emailSentAt is new in my PR

def specialCaseApprovePreviouslyFailedApplications()(using request: RequestHeader): Future[Unit] =
if (appConfig.enableUnsetRiskingResponses) {
logger.info("[SpecialCaseApprovePreviouslyFailedApplications] feature ENABLED.")
Future.sequence(

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.

Future sequence has some drow back like failed fast that leave orphan futures etc. would be better to use ProcessInSequence.processInSequence

)
.toFuture().map(_ => ())
}

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.

Small suggestion — the Repo base class already provides upsert(...) that does the same whole-document replaceOne under the hood. It's used everywhere else in the codebase (e.g. BackendNotificationService, EmailServiceForApprovedApplications, EmailServiceForFailedNonFixable). Would you mind switching this to upsert(updated) for consistency?

if (appConfig.enableUnsetRiskingResponses) {
logger.info("[SpecialCaseApprovePreviouslyFailedApplications] feature ENABLED.")
ProcessInSequence.processInSequence[ApplicationReference, Unit](
appConfig.applicationIdsForUnsettingRiskingResponses.map(ApplicationReference(_))

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.

Small suggestion — switch processInSequence to processAllInSequence so a single problematic config entry doesn't fail I think. The other batch services (BackendNotificationService, EmailServiceForFailedNonFixable, RiskingArchivalService) use this pattern to get per-item error

Comment on lines +66 to +76
private def processFeatureFlagged(applicationWithIndividuals: ApplicationWithIndividuals)(using RequestHeader): Future[Unit] =
val applicationForRisking: ApplicationForRisking = applicationWithIndividuals.application
applicationForRisking.overallStatus.riskingOutcome.getOrThrowExpectedDataMissing("risking outcome") match
case RiskingOutcome.FailedFixable =>
if appConfig.Features.fixableFailures
then process(applicationWithIndividuals)
else
logger.info(s"Not notifying backend for application ${applicationForRisking.applicationReference} with failed fixable outcome because of feature flag: ${appConfig.Features.fixableFailures}")
Future.unit
case _ => process(applicationWithIndividuals)

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.

This code is already on the main branch. Could you please check that your merge from main was completed correctly?

Email
CorrectiveAction
Base64
Features

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.

This code is already on the main branch. Could you please check that your merge from main was completed correctly?

def findAlreadyRiskedApplication(applicationReference: ApplicationReference): Future[Option[ApplicationWithIndividuals]] = findApplicationWithIndividuals(
applicationFilter = Filters.and(
Filters.eq(FieldNames.applicationReference, applicationReference.value),
Filters.exists(FieldNames.entityRiskingResult),

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.

riskingOutcome can only exist if entityRiskingResult is already present, so we don't really need that condition in the filter.

That said, I'm happy to leave it as is since this is one-off migration code that will be removed afterwards.

Filters.exists(FieldNames.entityRiskingResult),
Filters.exists(FieldNames.overallStatus.riskingOutcome, true)
),
individualForAllFilter = Filters.exists(FieldNames.individualRiskingResult)

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.

ditto

individualForAllFilter = Filters.exists(FieldNames.individualRiskingResult)
)

def findAlreadyRiskedApplication(applicationReference: ApplicationReference): Future[Option[ApplicationWithIndividuals]] = findApplicationWithIndividuals(

@paweldigital paweldigital Jul 7, 2026 •

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 don't think this needs to fetch the application together with its individuals. It should be simpler and sufficient to fetch just the application.

Comment on lines +413 to +423

val riskingOutcomeIndividualDetailsFix: RiskingOutcomeIndividual.FailedFixable = RiskingOutcomeIndividual.FailedFixable(
fixes = Seq(
IndividualFix._10.IndividualDetailsFix(
dateOfBirth = Some(IndividualDateOfBirth.Provided(dateOfBirth)),
nino = Some(IndividualNino.Provided(nino)),
saUtr = Some(IndividualSaUtr.Provided(saUtr)),
isConfirmed = 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.

This code is already on the main branch. Could you please check that your merge from main was completed correctly?


import java.time.ZoneOffset

class BackendNotificationServiceFeatureFlagSpec

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.

This is already in the main branch

class BackendNotificationServiceSpec
extends ISpec:

override protected def configOverrides: Map[String, Any] = Map(

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.

already on main branch


riskingResultsService.specialCaseApprovePreviouslyFailedApplications().futureValue

val modified = applicationForRiskingRepo.findById(ApplicationReference(failedNonFixableAppRef)).futureValue

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 you please name the value and add the type annotation, as we usually do in this codebase?

)
)

modified.flatMap(_.overallStatus.riskingOutcome) shouldBe Some(RiskingOutcome.Approved)

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 you please assert the entire object, to ensure that only the expected fields are changed?

Also, please use OptionValues from ScalaTest (as we do in other places) when asserting on Option values.

individualForAllFilter = Filters.exists(FieldNames.individualRiskingResult)
)

def findAlreadyRiskedApplication(applicationReference: ApplicationReference): Future[Option[ApplicationWithIndividuals]] = findApplicationWithIndividuals(

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 don't think this function is needed. The goal is to find the application and its individuals by applicationReference, so it would be simpler to reuse the existing functions that are already dedicated to this purpose instead of introducing new ones.

Also, please note that in this project all custom find functions need to be covered by unit tests. This is another reason to prefer reusing the existing functionality rather than adding a new method that would require additional testing.

Please use the existing methods:

ApplicationForRiskingRepo.findById(
  applicationRef: ApplicationReference
): Future[Option[ApplicationForRisking]]

to retrieve the application, and:

IndividualForRiskingRepo.findByApplicationReference(
  applicationReference: ApplicationReference
): Future[Seq[IndividualForRisking]]

to retrieve the corresponding individuals.

to be Approved instead (to allow for an ASA to be created). For example, where an applicant has successfully
appealed a Failed Non-Fixable.
*/
def specialCaseApprovePreviouslyFailedApplications()(using request: RequestHeader): Future[Unit] =

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 you rename the function so that its name better reflects what it does? It would make the code easier to understand and read.

Something like:

def updateApplicationOutcomeToApproved

or a similar name that clearly describes the action being performed.

As a general convention, we'd like function names to contain a verb so that it is immediately clear what the function does.

[APB-11788] special case / temporary solution for allowing applications that have already been determined as Failed
to be Approved instead (to allow for an ASA to be created). For example, where an applicant has successfully
appealed a Failed Non-Fixable.
*/

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.

Sorry for bothering, but could you convert this into a proper ScalaDoc comment instead of a multiline comment?

Comment on lines +152 to +164
if (appConfig.enableUnsetRiskingResponses) {
logger.info("[SpecialCaseApprovePreviouslyFailedApplications] feature ENABLED.")
ProcessInSequence.processAllInSequence[ApplicationReference, Unit](
appConfig.applicationIdsForUnsettingRiskingResponses.map(ApplicationReference(_))
)(appRef =>
logger.info(s"[SpecialCaseApprovePreviouslyFailedApplications] Trying from config appRef: ${appRef.value}")
applicationForRiskingRepo.findAlreadyRiskedApplication(appRef).flatMap {
case Some(appWithIndividuals) =>
appWithIndividuals.application.overallStatus.riskingOutcome match {
case Some(RiskingOutcome.FailedNonFixable | RiskingOutcome.FailedFixable) =>
for {
_ <- updateRiskingResults(RiskingResult.ForEntity(
applicationReference = appWithIndividuals.application.applicationReference,

@paweldigital paweldigital Jul 7, 2026 •

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 you consider a simpler implementation, something along these lines?

def updateApplicationOutcomeToApprovedIfNeeded(): Future[Unit] = 
  if appConfig.enableUnsetRiskingResponses
  then updateApplicationOutcomeToApproved()
  else Future.unit

private def requiresUpdate(application: ApplicationForRisking): Boolean =  ...
private def requiresUpdate(individual: IndividualForRisking): Boolean =  ...

private def updateApplicationOutcomeToApproved(): Future[Unit] =
  ProcessInSequence.processAllInSequence(appConfig.applicationIdsForUnsettingRiskingResponses):
  applicationReference => 
    for
    application <- applicationForRiskingRepo.findById(applicationReference)
    individuals <- individualForRiskingRepo.findByApplicationReference(applicationReference)
    updatedIndividuals = individuals.map(_.copy(...))
    _ <- 
      if requiresUpdate(application) 
      then applicationForRiskingRepo.upsert(application.copy(...)) 
      else Future.unit
    _ <- ProcessInSequence.processAllInSequence(individuals)(individual => 
       if requiresUpdate(individual) 
       then individualForRiskingRepo.upsert(updatedIndividual))
       else Future.unit
    yield logger.info("all done")

//please include missing logger statements, I skipeed them for simplicity
//probably better if the last two setps could be implemented as private helper functions.

val hipBaseUrl: String = servicesConfig.baseUrl("hip")
val hipAuthToken: HipAuthToken = HipAuthToken(config.get[String]("microservice.services.hip.authorization-token"))
val enableUnsetRiskingResponses: Boolean = servicesConfig.getBoolean("features.enable-unset-risking-responses")
val applicationIdsForUnsettingRiskingResponses: Seq[String] = config.get[Seq[String]]("applicationIdsForUnsettingRiskingResponses")

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 you please change the type signature to Seq[ApplicationReference]?

Also, since these are application references, could you rename this value and the corresponding config property to:

val applicationReferencesToBeApproved: Seq[ApplicationReference]

and in appconfig:

applicationReferencesToBeApproved = []

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants