Skip to content

Commit

Permalink
GFORMS-2360 - Specify list of page IDs to revisit on form component d…
Browse files Browse the repository at this point in the history
…ata change (#2352)

* GFORMS-2360 - Add ability to specify page ids to display again on change of field

* GFORMS-2360 - Refactor revisit code

* GFORMS-2360 - Added tests

{
    "_id": "gforms2360",
    "formName": "Redisplay calculations",
    "description": "",
    "version": 1,
    "authConfig": {
      "authModule": "hmrc"
    },
    "sections": [
      {
        "type": "addToList",
        "title": "Tax years",
        "shortName": "Tax year $n",
        "summaryDescription": "${taxYear}",
        "summaryName": "Tax year costs",
        "description": "Tax year $n",
        "presentationHint": "invisiblePageTitle",
        "addAnotherQuestion": {
          "id": "addAnotherTaxYear",
          "type": "choice",
          "label": "Do you want to add another tax year?",
          "labelSize": "s",
          "format": "yesno",
          "errorMessage": "Select yes if you want to add another tax year"
        },
        "pages": [
          {
            "title": "What tax year?",
            "fields": [
              {
                "id": "taxYear",
                "shortName": "Tax year",
                "type": "choice",
                "choices": [
                  "2023",
                  "2024",
                  "2025"
                ]
              }
            ]
          },
          {
            "title": "How many passengers?",
            "fields": [
              {
                "id": "passengers",
                "shortName": "Passengers",
                "type": "text",
                "format": "positiveWholeNumber",
                "pageIdsToDisplayOnChange": [
                  "calculationPage"
                ]
              }
            ]
          },
          {
            "title": "What rate was paid?",
            "fields": [
              {
                "id": "rate",
                "shortName": "Rate paid",
                "type": "text",
                "format": "positiveSterling",
                "pageIdsToDisplayOnChange": [
                  "calculationPage",
                  "passengersPage"
                ]
              }
            ]
          },
          {
            "title": "Calculation",
            "id": "calculationPage",
            "fields": [
              {
                "id": "calculation",
                "type": "info",
                "label": "",
                "infoText": "Amount owed - ${rate * passengers}",
                "infoType": "noformat"
              }
            ]
          }
        ]
      },
      {
        "title": "How many exempt passengers?",
        "presentationHint": "invisiblePageTitle",
        "fields": [
          {
            "id": "passengersExempt",
            "shortName": "Exempt passengers",
            "type": "text",
            "format": "positiveWholeNumber",
            "pageIdsToDisplayOnChange": [
              "passengersPage"
            ]
          }
        ]
      },
      {
        "title": "Total passengers",
        "id": "passengersPage",
        "fields": [
          {
            "id": "totalPassengers",
            "type": "info",
            "label": "",
            "infoText": "Total passengers - ${passengersExempt + passengers.sum}",
            "infoType": "noformat"
          }
        ]
      },
      {
        "title": "Some extra page",
        "fields": [
          {
            "id": "value",
            "shortName": "Extra value",
            "type": "text",
            "format": "positiveSterling"
          }
        ]
      }
    ],
    "acknowledgementSection": {
      "title": "Confirmation page ",
      "fields": []
    },
    "destinations": [
      {
        "id": "transitionToSubmitted",
        "type": "stateTransition",
        "requiredState": "Submitted"
      }
    ]
  }

* GFORMS-2360 - Fix tests after rebase
  • Loading branch information
cmanson authored Dec 17, 2024
1 parent 2829d2e commit 810d5f4
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 9 deletions.
55 changes: 53 additions & 2 deletions app/uk/gov/hmrc/gform/gform/processor/FormProcessor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import uk.gov.hmrc.gform.gformbackend.GformConnector
import uk.gov.hmrc.gform.graph.Recalculation
import uk.gov.hmrc.gform.models._
import uk.gov.hmrc.gform.models.gform.{ FormValidationOutcome, NoSpecificAction }
import uk.gov.hmrc.gform.models.ids.ModelPageId
import uk.gov.hmrc.gform.models.ids.{ ModelComponentId, ModelPageId }
import uk.gov.hmrc.gform.models.optics.DataOrigin.Mongo
import uk.gov.hmrc.gform.models.optics.{ DataOrigin, FormModelVisibilityOptics }
import uk.gov.hmrc.gform.sharedmodel._
Expand Down Expand Up @@ -237,6 +237,55 @@ class FormProcessor(
} yield redirect
}

private def getComponentsWithUpdatedValues(
oldMap: Map[ModelComponentId, VariadicValue],
newMap: Map[ModelComponentId, VariadicValue]
): Set[ModelComponentId] = {
val commonKeys = newMap.keySet.intersect(oldMap.keySet)
val oldKeysDiff = oldMap.keySet.diff(newMap.keySet)
val newKeysDiff = newMap.keySet.diff(oldMap.keySet)

val removed: Map[ModelComponentId, VariadicValue] = oldKeysDiff.map(k => k -> oldMap(k)).toMap
val added: Map[ModelComponentId, VariadicValue] = newKeysDiff.map(k => k -> newMap(k)).toMap
val updated: Map[ModelComponentId, VariadicValue] =
commonKeys.filter(k => oldMap(k).toSeq =!= newMap(k).toSeq).map(k => k -> newMap(k)).toMap

removed.keySet ++ added.keySet ++ updated.keySet
}

def checkForRevisits(
pageModel: PageModel[Visibility],
visitsIndex: VisitIndex,
formModelOptics: FormModelOptics[Mongo],
enteredVariadicFormData: EnteredVariadicFormData
): VisitIndex = {
val formComponentsUpdated: Set[ModelComponentId] =
getComponentsWithUpdatedValues(formModelOptics.pageOpticsData.data, enteredVariadicFormData.userData.data)

val pageList: Set[(List[PageId], Option[Int])] = formComponentsUpdated.flatMap(updated =>
pageModel.allFormComponents
.filter(fc => fc.baseComponentId === updated.baseComponentId)
.flatMap(fc => fc.pageIdsToDisplayOnChange.map(p => p -> updated.indexedComponentId.maybeIndex))
)

val sectionsToRevisit: Set[SectionNumber] = pageList.flatMap {
case (pageList: List[PageId], maybeIndex: Option[Int]) =>
pageList.map { pageId =>
val pageIdWithMaybeIndex: PageId = maybeIndex.map(idx => pageId.withIndex(idx)).getOrElse(pageId)
val maybeSectionNumber: Option[SectionNumber] =
formModelOptics.formModelVisibilityOptics.formModel.pageIdSectionNumberMap
.get(pageIdWithMaybeIndex.modelPageId)
maybeSectionNumber.getOrElse(
formModelOptics.formModelVisibilityOptics.formModel.pageIdSectionNumberMap(pageId.modelPageId)
)
}.toSet
}

sectionsToRevisit.foldLeft(visitsIndex) { case (acc, sn) =>
acc.unvisit(sn)
}
}

def validateAndUpdateData(
cache: AuthCacheWithForm,
processData: ProcessData,
Expand Down Expand Up @@ -368,13 +417,15 @@ class FormProcessor(
processData.visitsIndex.visit(sectionNumber)
else processData.visitsIndex.unvisit(sectionNumber)

val updatedVisitsIndex = checkForRevisits(pageModel, visitsIndex, formModelOptics, enteredVariadicFormData)

val cacheUpd =
cache.copy(
form = cache.form
.copy(
thirdPartyData = updatedThirdPartyData.copy(obligations = processData.obligations),
formData = formDataU,
visitsIndex = visitsIndex
visitsIndex = updatedVisitsIndex
)
)

Expand Down
11 changes: 10 additions & 1 deletion app/uk/gov/hmrc/gform/sharedmodel/form/VisitIndex.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ sealed trait VisitIndex extends Product with Serializable {
}(identity)

private def unvisitTaskList(sectionNumber: SectionNumber.TaskList): VisitIndex =
fold[VisitIndex](identity)(identity)
fold[VisitIndex](identity) { taskList =>
val update =
taskList.visitsIndex.get(sectionNumber.coordinates).fold(Set(sectionNumber.sectionNumber)) { alreadyVisited =>
alreadyVisited - sectionNumber.sectionNumber
}

val visitsIndexUpd = taskList.visitsIndex ++ Map(sectionNumber.coordinates -> update)

VisitIndex.TaskList(visitsIndexUpd)
}

private def containsClassic(sectionNumber: SectionNumber.Classic): Boolean =
fold[Boolean](classic => classic.visitsIndex.contains(sectionNumber))(_ => false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ case class FormComponent(
errorShortNameStart: Option[SmartString] = None,
errorExample: Option[SmartString] = None,
extraLetterSpacing: Option[Boolean] = None,
displayInSummary: Option[Boolean] = None
displayInSummary: Option[Boolean] = None,
pageIdsToDisplayOnChange: Option[List[PageId]] = None
) {

val modelComponentId: ModelComponentId = id.modelComponentId
Expand Down
219 changes: 219 additions & 0 deletions test/uk/gov/hmrc/gform/gform/processor/FormProcessorSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright 2024 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.gform.gform.processor

import cats.Id
import org.mockito.MockitoSugar.mock
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.prop.Tables.Table
import play.api.http.HttpConfiguration
import play.api.i18n._
import play.api.{ Configuration, Environment }
import uk.gov.hmrc.gform.Spec
import uk.gov.hmrc.gform.addresslookup.AddressLookupService
import uk.gov.hmrc.gform.api.{ BankAccountInsightsAsyncConnector, CompanyInformationAsyncConnector, NinoInsightsAsyncConnector }
import uk.gov.hmrc.gform.bars.BankAccountReputationAsyncConnector
import uk.gov.hmrc.gform.gform.handlers.FormControllerRequestHandler
import uk.gov.hmrc.gform.gform.{ FastForwardService, FileSystemConnector }
import uk.gov.hmrc.gform.gformbackend.GformConnector
import uk.gov.hmrc.gform.graph.FormTemplateBuilder.ls
import uk.gov.hmrc.gform.graph.{ GraphException, Recalculation }
import uk.gov.hmrc.gform.models._
import uk.gov.hmrc.gform.models.optics.{ DataOrigin, FormModelVisibilityOptics }
import uk.gov.hmrc.gform.objectStore.ObjectStoreService
import uk.gov.hmrc.gform.sharedmodel.form.{ EnvelopeId, FormModelOptics, VisitIndex }
import uk.gov.hmrc.gform.sharedmodel.formtemplate.SectionNumber.Classic
import uk.gov.hmrc.gform.sharedmodel.formtemplate.{ FormComponent, FormComponentId, PageId, ShortText, TemplateSectionIndex, Text, Value }
import uk.gov.hmrc.gform.sharedmodel.{ LangADT, SourceOrigin, VariadicFormData }
import uk.gov.hmrc.gform.validation.ValidationService

import scala.concurrent.Future

class FormProcessorSpec extends Spec with FormModelSupport with VariadicFormDataSupport {

override val envelopeId: EnvelopeId = EnvelopeId("dummy")
private val environment: Environment = Environment.simple()
private val configuration: Configuration = Configuration.load(environment)
private val langs: Langs = new DefaultLangs()
private val httpConfiguration: HttpConfiguration = HttpConfiguration.fromConfiguration(configuration, environment)
private val localMessagesApi: MessagesApi =
new DefaultMessagesApiProvider(environment, configuration, langs, httpConfiguration).get
private val i18nSupport: I18nSupport = new I18nSupport {
override def messagesApi: MessagesApi = localMessagesApi
}

implicit val messages: Messages = i18nSupport.messagesApi.preferred(Seq(langs.availables.head))
implicit val lang: LangADT = LangADT.En

private val processDataService: ProcessDataService[Future] = mock[ProcessDataService[Future]]
private val gformConnector: GformConnector = mock[GformConnector]
private val fileSystemConnector: FileSystemConnector = mock[FileSystemConnector]
private val validationService: ValidationService = mock[ValidationService]
private val fastForwardService: FastForwardService = mock[FastForwardService]
private val objectStoreService: ObjectStoreService = mock[ObjectStoreService]
private val formControllerRequestHandler: FormControllerRequestHandler = mock[FormControllerRequestHandler]
private val bankAccountReputationConnector: BankAccountReputationAsyncConnector =
mock[BankAccountReputationAsyncConnector]
private val companyInformationConnector: CompanyInformationAsyncConnector = mock[CompanyInformationAsyncConnector]
private val ninoInsightsConnector: NinoInsightsAsyncConnector = mock[NinoInsightsAsyncConnector]
private val addressLookupService: AddressLookupService[Future] = mock[AddressLookupService[Future]]
private val bankAccountInsightsConnector: BankAccountInsightsAsyncConnector = mock[BankAccountInsightsAsyncConnector]
private val localRecalculation: Recalculation[Future, Throwable] =
new Recalculation[Future, Throwable](
eligibilityStatusTrue,
delegatedEnrolmentCheckStatus,
dbLookupCheckStatus,
(s: GraphException) => new IllegalArgumentException(s.reportProblem)
)

val formProcessor = new FormProcessor(
i18nSupport,
processDataService,
gformConnector,
fileSystemConnector,
validationService,
fastForwardService,
localRecalculation,
objectStoreService,
formControllerRequestHandler,
bankAccountReputationConnector,
companyInformationConnector,
ninoInsightsConnector,
addressLookupService,
bankAccountInsightsConnector,
messages
)

"checkForRevisits" should "correctly remove page(s) from visits index" in {
val sections = (0 to 4).map { i =>
val pagesToRevisit = i match {
case 0 => Some(List(PageId("page1id"), PageId("page2id"), PageId("page4id")))
case 2 => Some(List(PageId("page3id")))
case _ => None
}

nonRepeatingPageSection(
s"Page $i",
fields = List(
FormComponent(
FormComponentId(s"comp$i"),
Text(ShortText.default, Value),
ls,
false,
None,
None,
None,
None,
true,
true,
true,
false,
false,
None,
None,
pageIdsToDisplayOnChange = pagesToRevisit
)
),
pageId = Some(PageId(s"page${i}id"))
)
}.toList

val existingData: VariadicFormData[SourceOrigin.OutOfDate] =
variadicFormData[SourceOrigin.OutOfDate]((0 to 4).map(i => s"comp$i" -> s"val$i"): _*)

val fmb: FormModelBuilder[Throwable, Id] = mkFormModelFromSections(sections)

val visibilityOpticsMongo: FormModelVisibilityOptics[DataOrigin.Mongo] =
fmb.visibilityModel[DataOrigin.Mongo, SectionSelectorType.Normal](existingData, None)
val formModelOpticsMongo =
fmb.renderPageModel[DataOrigin.Mongo, SectionSelectorType.Normal](visibilityOpticsMongo, None)
val visibilityFormModelVisibility: FormModel[Visibility] = formModelOpticsMongo.formModelVisibilityOptics.formModel
val initialVisitsIndex = VisitIndex.Classic(
(0 to 4).map(pageIdx => Classic.NormalPage(TemplateSectionIndex(pageIdx))).toSet
)

val table = Table(
("enteredData", "pageIdxToValidate", "expected"),
(
variadicFormData[SourceOrigin.OutOfDate](
"comp0" -> "val0",
"comp1" -> "val1",
"comp2" -> "valUpdate",
"comp3" -> "val3",
"comp4" -> "val4"
),
1,
Set(0, 1, 2, 3, 4)
),
(
variadicFormData[SourceOrigin.OutOfDate](
"comp0" -> "val0",
"comp1" -> "val1",
"comp2" -> "valUpdate",
"comp3" -> "val3",
"comp4" -> "val4"
),
2,
Set(0, 1, 2, 4)
),
(
variadicFormData[SourceOrigin.OutOfDate](
"comp0" -> "valUpdate",
"comp1" -> "val1",
"comp2" -> "val2",
"comp3" -> "val3",
"comp4" -> "val4"
),
0,
Set(0, 3)
),
(
variadicFormData[SourceOrigin.OutOfDate](
"comp0" -> "val0",
"comp1" -> "val1",
"comp2" -> "val2",
"comp3" -> "val3",
"comp4" -> "val4"
),
0,
Set(0, 1, 2, 3, 4)
)
)
TableDrivenPropertyChecks.forAll(table) { (enteredFormData, pageIdxToValidate, expectedPageSet) =>
val enteredVariadicFormData: EnteredVariadicFormData = EnteredVariadicFormData(enteredFormData)

val expected = VisitIndex.Classic(
expectedPageSet.map(pageIdx => Classic.NormalPage(TemplateSectionIndex(pageIdx)))
)

val visibilityPageModel: PageModel[Visibility] =
visibilityFormModelVisibility(Classic.NormalPage(TemplateSectionIndex(pageIdxToValidate)))
val formModelOptics: FormModelOptics[DataOrigin.Mongo] =
fmb.renderPageModel[DataOrigin.Mongo, SectionSelectorType.Normal](visibilityOpticsMongo, None)

val actual: VisitIndex = formProcessor.checkForRevisits(
visibilityPageModel,
initialVisitsIndex,
formModelOptics,
enteredVariadicFormData
)

expected shouldBe actual
}
}

}
6 changes: 3 additions & 3 deletions test/uk/gov/hmrc/gform/models/FormModelSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ trait FormModelSupport extends GraphSpec {
val thirdPartyData: ThirdPartyData = ThirdPartyData.empty
val envelopeId: EnvelopeId = EnvelopeId("dummy")

private def eligibilityStatusTrue[F[_]: Monad]: SeissEligibilityChecker[F] =
protected def eligibilityStatusTrue[F[_]: Monad]: SeissEligibilityChecker[F] =
new SeissEligibilityChecker[F]((_, _) => true.pure[F])

private def delegatedEnrolmentCheckStatus[F[_]: Monad]: DelegatedEnrolmentChecker[F] =
protected def delegatedEnrolmentCheckStatus[F[_]: Monad]: DelegatedEnrolmentChecker[F] =
new DelegatedEnrolmentChecker(delegatedEnrolmentCheckStatusTrue[F])

private def dbLookupCheckStatus[F[_]: Monad]: DbLookupChecker[F] =
protected def dbLookupCheckStatus[F[_]: Monad]: DbLookupChecker[F] =
new DbLookupChecker(dbLookupStatusTrue[F])

val recalculation: Recalculation[Id, Throwable] =
Expand Down
5 changes: 3 additions & 2 deletions test/uk/gov/hmrc/gform/sharedmodel/ExampleData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -641,12 +641,13 @@ trait ExampleSection { dependecies: ExampleFieldId with ExampleFieldValue =>
includeIf: Option[IncludeIf] = None,
instruction: Option[Instruction] = None,
presentationHint: Option[PresentationHint] = None,
confirmation: Option[Confirmation] = None
confirmation: Option[Confirmation] = None,
pageId: Option[PageId] = None
) =
Section.NonRepeatingPage(
Page(
toSmartString(title),
None,
pageId,
noPIITitle.map(toSmartString),
None,
None,
Expand Down

0 comments on commit 810d5f4

Please sign in to comment.