From fb9dd8c78ba3aafbe67e548b34f8565a2a0434be Mon Sep 17 00:00:00 2001 From: abhinavgupta-hmrc <269448599+abhinavgupta-hmrc@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:45:54 +0100 Subject: [PATCH 01/48] DTR-4472 Feat: Screen VH-02a/b/c Verification request submitted --- ...rificationRequestSubmittedController.scala | 61 ++++++ .../VerificationSubmittedViewModel.scala | 30 +++ app/views/components/PrintLink.scala.html | 29 +++ ...erificationRequestSubmittedView.scala.html | 182 ++++++++++++++++ conf/app.routes | 9 +- conf/messages.en | 195 +++++++++--------- ...cationRequestSubmittedControllerSpec.scala | 77 +++++++ .../VerificationSubmittedViewModelSpec.scala | 115 +++++++++++ test/views/components/PrintLinkSpec.scala | 91 ++++++++ ...VerificationRequestSubmittedViewSpec.scala | 180 ++++++++++++++++ 10 files changed, 870 insertions(+), 99 deletions(-) create mode 100644 app/controllers/verify/VerificationRequestSubmittedController.scala create mode 100644 app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala create mode 100644 app/views/components/PrintLink.scala.html create mode 100644 app/views/verify/VerificationRequestSubmittedView.scala.html create mode 100644 test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala create mode 100644 test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala create mode 100644 test/views/components/PrintLinkSpec.scala create mode 100644 test/views/verify/VerificationRequestSubmittedViewSpec.scala diff --git a/app/controllers/verify/VerificationRequestSubmittedController.scala b/app/controllers/verify/VerificationRequestSubmittedController.scala new file mode 100644 index 00000000..773b65af --- /dev/null +++ b/app/controllers/verify/VerificationRequestSubmittedController.scala @@ -0,0 +1,61 @@ +/* + * 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 controllers.verify + +import config.FrontendAppConfig +import controllers.actions.* +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +import views.html.verify.VerificationRequestSubmittedView + +import java.time.LocalDateTime +import javax.inject.Inject + +class VerificationRequestSubmittedController @Inject() ( + override val messagesApi: MessagesApi, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + val controllerComponents: MessagesControllerComponents, + view: VerificationRequestSubmittedView +)(implicit appConfig: FrontendAppConfig) + extends FrontendBaseController + with I18nSupport { + + def onPageLoad(): Action[AnyContent] = + (identify andThen getData andThen requireData) { implicit request => + + // TODO: Replace this with your real source: + // TODO: Replace 1. referenceNumber 2. submittedAt 3. verify list 4. Reverify lists 5. confirmationEmail + val vm = VerificationSubmittedViewModel( + referenceNumber = "Reference number 12345", + submittedAt = LocalDateTime.now(), + subcontractorsToVerify = + Seq("Brody, Martin", "Hooper And Associates", "Quint Transportation", "The Kintner Group"), + // To check the validation of empty reverify list + // subcontractorsToReverify = Seq.empty, + subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), + // To test no email provided scenario + // confirmationEmail = None + confirmationEmail = Some("test@testmail.com") + ) + + Ok(view(vm)) + } +} diff --git a/app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala b/app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala new file mode 100644 index 00000000..e5d6a0d1 --- /dev/null +++ b/app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala @@ -0,0 +1,30 @@ +/* + * 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 viewmodels.checkAnswers.verify +import java.time.LocalDateTime + +case class VerificationSubmittedViewModel( + referenceNumber: String, + submittedAt: LocalDateTime, + subcontractorsToVerify: Seq[String], + subcontractorsToReverify: Seq[String] = Seq.empty, + confirmationEmail: Option[String] = None +) { + def showEmail: Boolean = confirmationEmail.isDefined + def showVerify: Boolean = subcontractorsToVerify.nonEmpty + def showReverify: Boolean = subcontractorsToReverify.nonEmpty +} diff --git a/app/views/components/PrintLink.scala.html b/app/views/components/PrintLink.scala.html new file mode 100644 index 00000000..348ca302 --- /dev/null +++ b/app/views/components/PrintLink.scala.html @@ -0,0 +1,29 @@ +@* + * 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. + *@ + +@this() + +@(textKey: String, id: String = "print-link", extraClasses: String = "")(implicit messages: play.api.i18n.Messages) + +

+ + @messages(textKey) + +

diff --git a/app/views/verify/VerificationRequestSubmittedView.scala.html b/app/views/verify/VerificationRequestSubmittedView.scala.html new file mode 100644 index 00000000..cad66c56 --- /dev/null +++ b/app/views/verify/VerificationRequestSubmittedView.scala.html @@ -0,0 +1,182 @@ +@* + * 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. + *@ + +@import config.FrontendAppConfig +@import java.time.LocalDateTime +@import java.time.format.DateTimeFormatter +@import uk.gov.hmrc.govukfrontend.views.viewmodels.panel.Panel +@import uk.gov.hmrc.govukfrontend.views.Aliases.* + +@import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist._ +@import viewmodels.govuk.summarylist.SummaryListViewModel + +@import views.html.components._ +@import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel + + +@this( + layout: templates.Layout, + govukPanel: GovukPanel, + govukInsetText: GovukInsetText, + govukSummaryList: GovukSummaryList, + heading: H1, + subHeading: H2, + paragraph: Paragraph, + printLink: PrintLink, + link: Link +) + +@(vm: VerificationSubmittedViewModel +)(implicit request: Request[_],appConfig: FrontendAppConfig, messages: Messages) + +@layout(pageTitle = titleNoForm(messages("verify.verificationRequestSubmitted.title"))) { + + @govukPanel( + Panel( + title = Text(messages("verify.verificationRequestSubmitted.heading")), + content = HtmlContent( + s""" +

${messages("verify.verificationRequestSubmitted.reference")}

+ (${HtmlFormat.escape(vm.referenceNumber)}) + """ + ) + ) + ) + + @paragraph( + messages( + "verify.verificationRequestSubmitted.submittedAt", + vm.submittedAt.format(DateTimeFormatter.ofPattern("HH:mm 'on' dd MMMM yyyy")) + ) + ) + + @subHeading( + messages( + "verify.verificationRequestSubmitted.details.subHeading" + ) + ) + + @govukSummaryList( + SummaryListViewModel( + rows = Seq( + SummaryListRowViewModel( + key = messages("verify.verificationRequestSubmitted.subcontractorsToVerify.label"), + value = ValueViewModel( + HtmlContent( + vm.subcontractorsToVerify + .map(name => s"
  • $name
  • ") + .mkString("") + ) + ) + ) + ) + ) + ) + + @if(vm.showReverify) { + @govukSummaryList( + SummaryListViewModel( + rows = Seq( + SummaryListRowViewModel( + key = messages("verify.verificationRequestSubmitted.subcontractorsToReverify.label"), + value = ValueViewModel( + HtmlContent( + vm.subcontractorsToReverify + .map(name => s"
  • $name
  • ") + .mkString("") + ) + ) + ) + ) + ) + ) + } + + + @if(vm.showEmail) { + @vm.confirmationEmail.map { email => + @paragraph( + messages( + "verify.verificationRequestSubmitted.email.confirmation", + {email}, + "verify.verificationRequestSubmitted.fullStop" + ) + ) + } + } + + @link( + linkTextKey = "verify.verificationRequestSubmitted.email.verification.link", + linkUrl = appConfig.cisGeneralEnquiries, // TODO: Link to be updated later + // TODO: Link to be opened in new tab ? isNewTab = true, + prefixTextKey = "verify.verificationRequestSubmitted.email.verification" + ) + + @govukInsetText( + InsetText( + content = HtmlContent( + s""" + ${paragraph(messages("verify.verificationRequestSubmitted.print.text"))} + ${printLink("verify.verificationRequestSubmitted.print.link")} + """ + ) + ) + ) + + @subHeading( + messages( + "verify.verificationRequestSubmitted.needHelp.subHeading") + ) + + @link( + linkTextKey = "verify.verificationRequestSubmitted.needHelp.contactHMRC.link", + linkUrl = appConfig.cisGeneralEnquiries, + // TODO: Link to be opened in new tab ? isNewTab = true, + prefixTextKey = "verify.verificationRequestSubmitted.needHelp.p1", + ) + + @paragraph( + messages( + "verify.verificationRequestSubmitted.needHelp.p2" + ) + ) + + @link( + linkTextKey = "verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link", + linkUrl = appConfig.manageSubcontractorsUrl, // TODO: Link to be confirmed and verified + // TODO: Validate the URL in application.conf file + // manageSubcontractors = "http://localhost:6996/construction-industry-scheme/management/manage-subcontractors" + // TODO: Link to be opened in new tab ? isNewTab = true, + prefixTextKey = "verify.verificationRequestSubmitted.needHelp.p3", + ) + + @subHeading( + messages("verify.verificationRequestSubmitted.feedback.subHeading") + ) + + @paragraph( + messages( + "verify.verificationRequestSubmitted.feedback.p1" + ) + ) + + @link( + "verify.verificationRequestSubmitted.feedback.survey.link", + appConfig.exitSurveyUrl, // TODO: Link to be confirmed & verified + isNewTab = true, // TODO: Link to be opened in new tab ? + suffixTextKey = "verify.verificationRequestSubmitted.feedback.p2" + ) +} diff --git a/conf/app.routes b/conf/app.routes index a7fda6c1..fe713b79 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -364,8 +364,6 @@ GET /verify/no-subcontractors-added controllers.verify.NoSubc GET /verification/newest controllers.verification.NewestVerificationBatchController.onPageLoad() -GET /verify/current controllers.verify.CurrentVerificationBatchController.onPageLoad() - GET /verify/verification-request-in-progress controllers.verify.VerificationRequestInProgressController.onPageLoad() GET /verify/confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onPageLoad(mode: Mode = NormalMode) @@ -373,11 +371,6 @@ POST /verify/confirmation-email-stored controllers.verify.Contra GET /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onPageLoad(mode: Mode = CheckMode) POST /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onSubmit(mode: Mode = CheckMode) -GET /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = NormalMode, page: Int ?= 1) -POST /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onSubmit(mode: Mode = NormalMode, page: Int ?= 1) -GET /verify/change-select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = CheckMode, page: Int ?= 1) -POST /verify/change-select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onSubmit(mode: Mode = CheckMode, page: Int ?= 1) - GET /verify/enter-confirmation-email controllers.verify.EmailAddressController.onPageLoad(mode: Mode = NormalMode) POST /verify/enter-confirmation-email controllers.verify.EmailAddressController.onSubmit(mode: Mode = NormalMode) GET /verify/change-enter-confirmation-email controllers.verify.EmailAddressController.onPageLoad(mode: Mode = CheckMode) @@ -390,3 +383,5 @@ POST /verify/change-reverify-existing-subcontractors controllers.verify.Reveri GET /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onPageLoad POST /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onSubmit + +GET /verify/verification-request-submitted controllers.verify.VerificationRequestSubmittedController.onPageLoad() diff --git a/conf/messages.en b/conf/messages.en index ed751a7d..7bf391c0 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -53,10 +53,6 @@ checkYourAnswers.heading.h2 = Now add this subcontractor checkYourAnswers.p1 = By adding this subcontractor you are confirming that, to the best of your knowledge, the details you are providing are correct. checkYourAnswers.addSubcontractor = Accept and submit -site.pagination.previous = Previous -site.pagination.next = Next -site.pagination.landmark = Pagination - # Errors & Auth pageNotFound.title = Page not found pageNotFound.heading = Page not found @@ -302,7 +298,7 @@ partnershipWorksReferenceNumberYesNo.title = Does thi partnershipWorksReferenceNumberYesNo.heading = Does {0} have a works reference number? partnershipWorksReferenceNumberYesNo.hint = This is the reference number used to identify your subcontractor’s work or project. It can contain letters and numbers partnershipWorksReferenceNumberYesNo.checkYourAnswersLabel = Add works reference number? -partnershipWorksReferenceNumberYesNo.error.required = Select whether this partnership has a works reference number +partnershipWorksReferenceNumberYesNo.error.required = Select one option partnershipWorksReferenceNumberYesNo.change.hidden = add works reference number? partnershipName.title = What is the partnership name? @@ -775,90 +771,105 @@ trustCheckYourAnswers.trailText = By adding this trustCheckYourAnswers.continue = Accept and submit # Verify -verify.submissionSending.title = Submitting your verification request -verify.submissionSending.heading = Your verification request is being sent to HMRC -verify.submissionSending.paragraph = Do not refresh this page or press the back button while your submission is being processed. - -verify.contractorEmailConfirmationNotStored.title = Do you want an email confirmation of this verification request? -verify.contractorEmailConfirmationNotStored.heading = Do you want an email confirmation of this verification request? -verify.contractorEmailConfirmationNotStored.checkYourAnswersLabel = Do you want confirmation by email? -verify.contractorEmailConfirmationNotStored.error.required = Select yes if you want an email confirmation of this verification request -verify.contractorEmailConfirmationNotStored.change.hidden = do you want confirmation by email? - -verify.verificationDeclaration.title = Verification declaration -verify.verificationDeclaration.heading = Declaration -verify.verificationDeclaration.p1 = By submitting this verification request, you confirm that: -verify.verificationDeclaration.list.l1 = a formal arrangement is in place (for example, tender accepted, contract agreed, order placed) for all of the subcontractors to be verified in this request. -verify.verificationDeclaration.list.l2 = the information given in this verification request is correct and complete to the best of your knowledge and belief. -verify.verificationDeclaration.warningText = If you give any false information you may face financial penalties and prosecution. -verify.verificationDeclaration.confirm = Confirm - -verify.noSubcontractorsAdded.title = No subcontractors added -verify.noSubcontractorsAdded.heading = Verify your subcontractors -verify.noSubcontractorsAdded.p1 = You do not have any subcontractors at the moment. -verify.noSubcontractorsAdded.p2 = You will need to -verify.noSubcontractorsAdded.p2.link = add some subcontractors first -verify.noSubcontractorsAdded.p2.end = before you can create a verification request or monthly return. -verify.noSubcontractorsAdded.p3 = Back to -verify.noSubcontractorsAdded.p3.link = Manage your subcontractors - -verify.contractorEmailConfirmationStored.title = Do you want an email confirmation of this verification request? -verify.contractorEmailConfirmationStored.heading = Do you want an email confirmation of this verification request? -verify.contractorEmailConfirmationStored.paragraph = Your email address is currently {0}. You can get a confirmation sent to this address or use a different one. -verify.contractorEmailConfirmationStored.currentEmail = Send confirmation to current email address -verify.contractorEmailConfirmationStored.differentEmail = Use a different email address -verify.contractorEmailConfirmationStored.or = or -verify.contractorEmailConfirmationStored.doNotSend = Do not send an email confirmation -verify.contractorEmailConfirmationStored.checkYourAnswersLabel = Do you want confirmation by email? -verify.contractorEmailConfirmationStored.error.required = Select whether you want an email confirmation of this verification request -verify.contractorEmailConfirmationStored.change.hidden = do you want confirmation by email? - -verify.verifyYourSubcontractorsYesNo.title = No new subcontractors to verify -verify.verifyYourSubcontractorsYesNo.heading = Verify your subcontractors -verify.verifyYourSubcontractorsYesNo.p1 = There are no unverified subcontractors that you need to verify at the moment. -verify.verifyYourSubcontractorsYesNo.p2 = However, you can reverify any of your existing subcontractors to make sure that their tax treatment is correct and up to date. -verify.verifyYourSubcontractorsYesNo.p3 = You may want to reverify subcontractors if they: -verify.verifyYourSubcontractorsYesNo.list.l1 = have recently changed any of their information -verify.verifyYourSubcontractorsYesNo.list.l2 = have not been included on a return recently and have lost their verified status -verify.verifyYourSubcontractorsYesNo.list.l3 = are about to lose their verified status -verify.verifyYourSubcontractorsYesNo.heading2 = Do you want to add existing subcontractors to this verification request? -verify.verifyYourSubcontractorsYesNo.error.required = Select yes if you want to add existing subcontractors to this verification request - -verify.verificationRequestInProgress.title = Verification request in progress -verify.verificationRequestInProgress.heading = Verification request in progress -verify.verificationRequestInProgress.p1 = HMRC has not responded to your previous verification request yet. -verify.verificationRequestInProgress.p2 = You must wait for this response before you can verify any more subcontractors. -verify.verificationRequestInProgress.p3.link = Contact HMRC -verify.verificationRequestInProgress.p3 = if you have any questions about this verification request. -verify.verificationRequestInProgress.p4 = Back to -verify.verificationRequestInProgress.p4.link = Manage your subcontractors - -verify.selectSubcontractor.title = Which subcontractors do you want to verify? -verify.selectSubcontractor.heading = Which subcontractors do you want to verify? -verify.selectSubcontractor.hint = Select the unverified subcontractors you want to add to this verification request -verify.selectSubcontractor.checkYourAnswersLabel = Subcontractors to verify -verify.selectSubcontractor.showingResults = Showing {0} to {1} of {2} results -verify.selectSubcontractor.error.required = Select at least one subcontractor to verify -verify.selectSubcontractor.change.hidden = subcontractors to verify - -verify.emailAddress.title = What email address should HMRC send this confirmation to? -verify.emailAddress.heading = What email address should HMRC send this confirmation to? -verify.emailAddress.hint = HMRC will only use this email address to confirm this verification request. Your saved email address will not be changed. -verify.emailAddress.hint.notStored = HMRC will only use this email address to confirm this verification request -verify.emailAddress.checkYourAnswersLabel = Email Address -verify.emailAddress.error.required = Enter an email address in the correct format, like name@example.com -verify.emailAddress.error.length = Enter a valid email address in the correct format, like name@example.com, up to 254 characters -verify.emailAddress.error.invalid = Enter a valid email address in the correct format, like name@example.com -verify.emailAddress.change.hidden = email address - -verify.reverifyExistingSubcontractorsYesNo.title = Reverify existing subcontractors -verify.reverifyExistingSubcontractorsYesNo.heading = Reverify existing subcontractors -verify.reverifyExistingSubcontractorsYesNo.p1 = To make sure that their tax treatment is correct and up to date, you can reverify any of your existing subcontractors. -verify.reverifyExistingSubcontractorsYesNo.p2 = You may want to reverify subcontractors if they: -verify.reverifyExistingSubcontractorsYesNo.list.l1 = have recently changed any of their information -verify.reverifyExistingSubcontractorsYesNo.list.l2 = have not been included on a return recently and have lost their verified status -verify.reverifyExistingSubcontractorsYesNo.list.l3 = are about to lose their verified status -verify.reverifyExistingSubcontractorsYesNo.subHeading = Do you want to add other subcontractors to this verification request? -verify.reverifyExistingSubcontractorsYesNo.error.required = Select yes if you want to add existing subcontractors to this verification request -verify.reverifyExistingSubcontractorsYesNo.checkYourAnswersLabel = Do you want to add other subcontractors to this verification request? -verify.reverifyExistingSubcontractorsYesNo.change.hidden = do you want to add other subcontractors to this verification request? +verify.submissionSending.title = Submitting your verification request +verify.submissionSending.heading = Your verification request is being sent to HMRC +verify.submissionSending.paragraph = Do not refresh this page or press the back button while your submission is being processed. + +verify.contractorEmailConfirmationNotStored.title = Do you want an email confirmation of this verification request? +verify.contractorEmailConfirmationNotStored.heading = Do you want an email confirmation of this verification request? +verify.contractorEmailConfirmationNotStored.checkYourAnswersLabel = Do you want confirmation by email? +verify.contractorEmailConfirmationNotStored.error.required = Select yes if you want an email confirmation of this verification request +verify.contractorEmailConfirmationNotStored.change.hidden = do you want confirmation by email? + +verify.verificationDeclaration.title = Verification declaration +verify.verificationDeclaration.heading = Declaration +verify.verificationDeclaration.p1 = By submitting this verification request, you confirm that: +verify.verificationDeclaration.list.l1 = a formal arrangement is in place (for example, tender accepted, contract agreed, order placed) for all of the subcontractors to be verified in this request. +verify.verificationDeclaration.list.l2 = the information given in this verification request is correct and complete to the best of your knowledge and belief. +verify.verificationDeclaration.warningText = If you give any false information you may face financial penalties and prosecution. +verify.verificationDeclaration.confirm = Confirm + +verify.noSubcontractorsAdded.title = No subcontractors added +verify.noSubcontractorsAdded.heading = Verify your subcontractors +verify.noSubcontractorsAdded.p1 = You do not have any subcontractors at the moment. +verify.noSubcontractorsAdded.p2 = You will need to +verify.noSubcontractorsAdded.p2.link = add some subcontractors first +verify.noSubcontractorsAdded.p2.end = before you can create a verification request or monthly return. +verify.noSubcontractorsAdded.p3 = Back to +verify.noSubcontractorsAdded.p3.link = Manage your subcontractors + +verify.contractorEmailConfirmationStored.title = Do you want an email confirmation of this verification request? +verify.contractorEmailConfirmationStored.heading = Do you want an email confirmation of this verification request? +verify.contractorEmailConfirmationStored.paragraph = Your email address is currently {0}. You can get a confirmation sent to this address or use a different one. +verify.contractorEmailConfirmationStored.currentEmail = Send confirmation to current email address +verify.contractorEmailConfirmationStored.differentEmail = Use a different email address +verify.contractorEmailConfirmationStored.or = or +verify.contractorEmailConfirmationStored.doNotSend = Do not send an email confirmation +verify.contractorEmailConfirmationStored.checkYourAnswersLabel = Do you want confirmation by email? +verify.contractorEmailConfirmationStored.error.required = Select whether you want an email confirmation of this verification request +verify.contractorEmailConfirmationStored.change.hidden = do you want confirmation by email? + +verify.verifyYourSubcontractorsYesNo.title = No new subcontractors to verify +verify.verifyYourSubcontractorsYesNo.heading = Verify your subcontractors +verify.verifyYourSubcontractorsYesNo.p1 = There are no unverified subcontractors that you need to verify at the moment. +verify.verifyYourSubcontractorsYesNo.p2 = However, you can reverify any of your existing subcontractors to make sure that their tax treatment is correct and up to date. +verify.verifyYourSubcontractorsYesNo.p3 = You may want to reverify subcontractors if they: +verify.verifyYourSubcontractorsYesNo.list.l1 = have recently changed any of their information +verify.verifyYourSubcontractorsYesNo.list.l2 = have not been included on a return recently and have lost their verified status +verify.verifyYourSubcontractorsYesNo.list.l3 = are about to lose their verified status +verify.verifyYourSubcontractorsYesNo.heading2 = Do you want to add existing subcontractors to this verification request? +verify.verifyYourSubcontractorsYesNo.error.required = Select yes if you want to add existing subcontractors to this verification request + +verify.verificationRequestInProgress.title = Verification request in progress +verify.verificationRequestInProgress.heading = Verification request in progress +verify.verificationRequestInProgress.p1 = HMRC has not responded to your previous verification request yet. +verify.verificationRequestInProgress.p2 = You must wait for this response before you can verify any more subcontractors. +verify.verificationRequestInProgress.p3.link = Contact HMRC +verify.verificationRequestInProgress.p3 = if you have any questions about this verification request. +verify.verificationRequestInProgress.p4 = Back to +verify.verificationRequestInProgress.p4.link = Manage your subcontractors. + +verify.emailAddress.title = What email address should HMRC send this confirmation to? +verify.emailAddress.heading = What email address should HMRC send this confirmation to? +verify.emailAddress.hint = HMRC will only use this email address to confirm this verification request. Your saved email address will not be changed. +verify.emailAddress.hint.notStored = HMRC will only use this email address to confirm this verification request +verify.emailAddress.checkYourAnswersLabel = Email Address +verify.emailAddress.error.required = Enter an email address in the correct format, like name@example.com +verify.emailAddress.error.length = Enter a valid email address in the correct format, like name@example.com, up to 254 characters +verify.emailAddress.error.invalid = Enter a valid email address in the correct format, like name@example.com +verify.emailAddress.change.hidden = email address + +verify.reverifyExistingSubcontractorsYesNo.title = Reverify existing subcontractors +verify.reverifyExistingSubcontractorsYesNo.heading = Reverify existing subcontractors +verify.reverifyExistingSubcontractorsYesNo.p1 = To make sure that their tax treatment is correct and up to date, you can reverify any of your existing subcontractors. +verify.reverifyExistingSubcontractorsYesNo.p2 = You may want to reverify subcontractors if they: +verify.reverifyExistingSubcontractorsYesNo.list.l1 = have recently changed any of their information +verify.reverifyExistingSubcontractorsYesNo.list.l2 = have not been included on a return recently and have lost their verified status +verify.reverifyExistingSubcontractorsYesNo.list.l3 = are about to lose their verified status +verify.reverifyExistingSubcontractorsYesNo.subHeading = Do you want to add other subcontractors to this verification request? +verify.reverifyExistingSubcontractorsYesNo.error.required = Select yes if you want to add existing subcontractors to this verification request +verify.reverifyExistingSubcontractorsYesNo.checkYourAnswersLabel = Do you want to add other subcontractors to this verification request? +verify.reverifyExistingSubcontractorsYesNo.change.hidden = do you want to add other subcontractors to this verification request? + +verify.verificationRequestSubmitted.title = Verification request submitted +verify.verificationRequestSubmitted.heading = Verification request submitted +verify.verificationRequestSubmitted.reference = Your verification reference number +verify.verificationRequestSubmitted.submittedAt = Submitted at {0} +verify.verificationRequestSubmitted.details.subHeading = Verification request details +verify.verificationRequestSubmitted.subcontractorsToVerify.label = Subcontractors to verify +verify.verificationRequestSubmitted.subcontractorsToReverify.label = Subcontractors to reverify +verify.verificationRequestSubmitted.email.confirmation = Confirmation of submission has been sent to {0} +verify.verificationRequestSubmitted.email.verification = You can access this request in read-only format from your +verify.verificationRequestSubmitted.email.verification.link = verification history. +verify.verificationRequestSubmitted.print.text = You can also save this page or print a copy for your records. +verify.verificationRequestSubmitted.print.link = Print this page +verify.verificationRequestSubmitted.needHelp.subHeading = Need help? +verify.verificationRequestSubmitted.needHelp.contactHMRC.link = contact HMRC. +verify.verificationRequestSubmitted.needHelp.p1 = For any questions about your verification request, +verify.verificationRequestSubmitted.needHelp.p2 = You will need the reference number of this verification request. +verify.verificationRequestSubmitted.needHelp.p3 = Back to +verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link = Manage your subcontractors. +verify.verificationRequestSubmitted.feedback.subHeading = Before you go +verify.verificationRequestSubmitted.feedback.p1 = Your feedback helps us make our service better. +verify.verificationRequestSubmitted.feedback.p2 = to share your feedback on this service. +verify.verificationRequestSubmitted.feedback.survey.link = Take a short survey diff --git a/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala b/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala new file mode 100644 index 00000000..fa693107 --- /dev/null +++ b/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala @@ -0,0 +1,77 @@ +/* + * 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 controllers.verify + +import base.SpecBase +import play.api.test.FakeRequest +import play.api.test.Helpers.* +import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +import views.html.verify.VerificationRequestSubmittedView + +import java.time.LocalDateTime + +class VerificationRequestSubmittedControllerSpec extends SpecBase { + + "VerificationRequestSubmitted Controller" - { + + "must return OK and the correct view for a GET" in { + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + running(application) { + + val request = + FakeRequest( + GET, + routes.VerificationRequestSubmittedController.onPageLoad().url + ) + + val result = + route(application, request).value + + val view = + application.injector.instanceOf[VerificationRequestSubmittedView] + + val appConfig = + application.injector.instanceOf[config.FrontendAppConfig] + + val expectedViewModel = + VerificationSubmittedViewModel( + referenceNumber = "Reference number 12345", + submittedAt = LocalDateTime.now(), + subcontractorsToVerify = Seq( + "Brody, Martin", + "Hooper And Associates", + "Quint Transportation", + "The Kintner Group" + ), + subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), + confirmationEmail = Some("test@testmail.com") + ) + + status(result) mustEqual OK + contentAsString(result) mustEqual + view(expectedViewModel)( + request, + appConfig, + messages(application) + ).toString + } + } + } +} diff --git a/test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala b/test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala new file mode 100644 index 00000000..ce5d8eaa --- /dev/null +++ b/test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala @@ -0,0 +1,115 @@ +/* + * 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 viewmodels.checkAnswers.verify + +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +import java.time.LocalDateTime + +class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { + + private val now = LocalDateTime.of(2026, 4, 27, 10, 30) + + "VerificationSubmittedViewModel" - { + + "showEmail" - { + + "must return true when confirmationEmail is defined" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq("Sub A"), + confirmationEmail = Some("test@test.com") + ) + + vm.showEmail shouldBe true + } + + "must return false when confirmationEmail is not defined" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq("Sub A"), + confirmationEmail = None + ) + + vm.showEmail shouldBe false + } + } + + "showVerify" - { + + "must return true when subcontractorsToVerify is non-empty" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq("Sub A") + ) + + vm.showVerify shouldBe true + } + + "must return false when subcontractorsToVerify is empty" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq.empty + ) + + vm.showVerify shouldBe false + } + } + + "showReverify" - { + + "must return true when subcontractorsToReverify is non-empty" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq("Sub A"), + subcontractorsToReverify = Seq("Sub B") + ) + + vm.showReverify shouldBe true + } + + "must return false when subcontractorsToReverify is empty" in { + + val vm = + VerificationSubmittedViewModel( + referenceNumber = "REF123", + submittedAt = now, + subcontractorsToVerify = Seq("Sub A"), + subcontractorsToReverify = Seq.empty + ) + + vm.showReverify shouldBe false + } + } + } +} diff --git a/test/views/components/PrintLinkSpec.scala b/test/views/components/PrintLinkSpec.scala new file mode 100644 index 00000000..c3531121 --- /dev/null +++ b/test/views/components/PrintLinkSpec.scala @@ -0,0 +1,91 @@ +/* + * 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 views.components + +import base.SpecBase +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.select.Elements +import play.api.i18n.{Lang, Messages, MessagesApi, MessagesImpl} +import play.api.mvc.RequestHeader +import play.api.test.FakeRequest +import play.twirl.api.HtmlFormat +import views.html.components.PrintLink + +class PrintLinkSpec extends SpecBase { + + "PrintLink" - { + + "must render a govuk link inside a govuk-body paragraph with default id" in new Setup { + val html: HtmlFormat.Appendable = printLink("monthlyreturns.submissionSuccessful.print") + + paragraph(html).size mustBe 1 + + val link: Elements = linkById(html, defaultId) + link.size mustBe 1 + link.attr("href") mustBe "#" + link.attr("data-module") mustBe "hmrc-print-link" + link.text() mustBe messages("monthlyreturns.submissionSuccessful.print") + + link.hasClass("govuk-link") mustBe true + link.hasClass("hmrc-!-js-visible") mustBe true + link.hasClass("govuk-!-display-none-print") mustBe true + } + + "must apply the provided id" in new Setup { + val html: HtmlFormat.Appendable = + printLink("monthlyreturns.submissionSuccessful.print", id = "print-bottom") + + val link: Elements = linkById(html, "print-bottom") + link.size mustBe 1 + } + + "must apply extra classes when provided" in new Setup { + val html: HtmlFormat.Appendable = + printLink( + "monthlyreturns.submissionSuccessful.print", + id = "print-extra", + extraClasses = "custom-class" + ) + + val link: Elements = linkById(html, "print-extra") + link.hasClass("custom-class") mustBe true + } + } + + trait Setup { + private val app = applicationBuilder().build() + val printLink: PrintLink = app.injector.instanceOf[PrintLink] + val defaultId = "print-link" + + implicit val request: RequestHeader = FakeRequest() + implicit val messages: Messages = + MessagesImpl(Lang.defaultLang, app.injector.instanceOf[MessagesApi]) + + def docOf(html: HtmlFormat.Appendable): Document = + Jsoup.parse(html.body) + + def select(html: HtmlFormat.Appendable, cssSelector: String): Elements = + docOf(html).select(cssSelector) + + def paragraph(html: HtmlFormat.Appendable): Elements = + select(html, "p.govuk-body") + + def linkById(html: HtmlFormat.Appendable, id: String): Elements = + select(html, s"a.govuk-link#$id") + } +} diff --git a/test/views/verify/VerificationRequestSubmittedViewSpec.scala b/test/views/verify/VerificationRequestSubmittedViewSpec.scala new file mode 100644 index 00000000..cd15b57a --- /dev/null +++ b/test/views/verify/VerificationRequestSubmittedViewSpec.scala @@ -0,0 +1,180 @@ +/* + * 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 views.verify + +import config.FrontendAppConfig +import org.jsoup.Jsoup +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.i18n.{Lang, Messages, MessagesApi, MessagesImpl} +import play.api.mvc.Request +import play.api.test.FakeRequest +import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +import views.html.verify.VerificationRequestSubmittedView + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class VerificationRequestSubmittedViewSpec extends AnyWordSpec with Matchers with GuiceOneAppPerSuite { + + "VerificationRequestSubmittedView" should { + + "render the page correctly when reverify list and email are present" in new Setup { + + val doc = Jsoup.parse(html.toString()) + + doc.title must include( + messages("verify.verificationRequestSubmitted.title") + ) + + doc.select(".govuk-panel__title").text mustBe + messages("verify.verificationRequestSubmitted.heading") + + doc.select(".govuk-panel__body").text must include(referenceNumber) + + doc.text must include( + messages( + "verify.verificationRequestSubmitted.submittedAt", + submittedAt.format( + DateTimeFormatter.ofPattern("HH:mm 'on' dd MMMM yyyy") + ) + ) + ) + + doc.select("h2").text must include( + messages("verify.verificationRequestSubmitted.details.subHeading") + ) + + subcontractorsToVerify.foreach { name => + doc.select("ul.govuk-list--bullet").text must include(name) + } + + subcontractorsToReverify.foreach { name => + doc.select("ul.govuk-list--bullet").text must include(name) + } + + doc.select("p.govuk-body").text must include(email) + + val emailVerificationLink = + doc.select(s"a[href='${appConfig.cisGeneralEnquiries}']") + + emailVerificationLink.text must include( + messages("verify.verificationRequestSubmitted.email.verification.link") + ) + + // Inset text print section + doc.select(".govuk-inset-text").text must include( + messages("verify.verificationRequestSubmitted.print.text") + ) + + doc.select(".govuk-inset-text").text must include( + messages("verify.verificationRequestSubmitted.print.link") + ) + + doc.select("h2").text must include( + messages("verify.verificationRequestSubmitted.needHelp.subHeading") + ) + + val manageLink = + doc.select(s"a[href='${appConfig.manageSubcontractorsUrl}']") + + manageLink.text must include( + messages("verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link") + ) + + doc.select("h2").text must include( + messages("verify.verificationRequestSubmitted.feedback.subHeading") + ) + + val surveyLink = + doc.select(s"a[href='${appConfig.exitSurveyUrl}']") + + surveyLink.size mustBe 1 + surveyLink.attr("target") mustBe "_blank" + + surveyLink.first.parent.text must include( + messages("verify.verificationRequestSubmitted.feedback.p2") + ) + } + + "not render reverify section or email paragraph when both are absent" in new Setup { + + override val viewModel: VerificationSubmittedViewModel = + viewModel.copy( + subcontractorsToReverify = Seq.empty, + confirmationEmail = None + ) + + val doc = Jsoup.parse(html.toString()) + + doc.text must not include + messages("verify.verificationRequestSubmitted.subcontractorsToReverify.label") + + doc.text must not include email + } + } + + trait Setup { + + implicit val request: Request[_] = + FakeRequest() + + implicit val messages: Messages = + MessagesImpl( + Lang.defaultLang, + app.injector.instanceOf[MessagesApi] + ) + + implicit val appConfig: FrontendAppConfig = + app.injector.instanceOf[FrontendAppConfig] + + val view: VerificationRequestSubmittedView = + app.injector.instanceOf[VerificationRequestSubmittedView] + + val referenceNumber = "Reference number 12345" + val submittedAt = LocalDateTime.of(2026, 4, 27, 10, 30) + + val subcontractorsToVerify = + Seq( + "Brody, Martin", + "Hooper And Associates", + "Quint Transportation", + "The Kintner Group" + ) + + val subcontractorsToReverify = + Seq( + "Grant, Alan", + "InGen Research" + ) + + val email = "test@testmail.com" + + val viewModel: VerificationSubmittedViewModel = + VerificationSubmittedViewModel( + referenceNumber = referenceNumber, + submittedAt = submittedAt, + subcontractorsToVerify = subcontractorsToVerify, + subcontractorsToReverify = subcontractorsToReverify, + confirmationEmail = Some(email) + ) + + lazy val html = + view(viewModel) + } +} From 03f31857b85b1681fa720da8e1f69c44041f480e Mon Sep 17 00:00:00 2001 From: abhinavgupta-hmrc <269448599+abhinavgupta-hmrc@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:51:43 +0100 Subject: [PATCH 02/48] DTR-4472 Fix: remove commented code line --- test/views/verify/VerificationRequestSubmittedViewSpec.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/test/views/verify/VerificationRequestSubmittedViewSpec.scala b/test/views/verify/VerificationRequestSubmittedViewSpec.scala index cd15b57a..586d2ef1 100644 --- a/test/views/verify/VerificationRequestSubmittedViewSpec.scala +++ b/test/views/verify/VerificationRequestSubmittedViewSpec.scala @@ -77,7 +77,6 @@ class VerificationRequestSubmittedViewSpec extends AnyWordSpec with Matchers wit messages("verify.verificationRequestSubmitted.email.verification.link") ) - // Inset text print section doc.select(".govuk-inset-text").text must include( messages("verify.verificationRequestSubmitted.print.text") ) From bc5a71fe576c03bc879bb06506eeed4e89fad338 Mon Sep 17 00:00:00 2001 From: abhinavgupta-hmrc <269448599+abhinavgupta-hmrc@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:46:37 +0100 Subject: [PATCH 03/48] DTR-4472 Fix: commented code removed and full stop issue fixed --- ...rificationRequestSubmittedController.scala | 3 +- ...erificationRequestSubmittedView.scala.html | 35 +++++++++---------- conf/messages.en | 8 ++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/verify/VerificationRequestSubmittedController.scala b/app/controllers/verify/VerificationRequestSubmittedController.scala index 773b65af..b705584a 100644 --- a/app/controllers/verify/VerificationRequestSubmittedController.scala +++ b/app/controllers/verify/VerificationRequestSubmittedController.scala @@ -48,9 +48,10 @@ class VerificationRequestSubmittedController @Inject() ( submittedAt = LocalDateTime.now(), subcontractorsToVerify = Seq("Brody, Martin", "Hooper And Associates", "Quint Transportation", "The Kintner Group"), + // Seq.empty, // To check the validation of empty reverify list - // subcontractorsToReverify = Seq.empty, subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), + // Seq.empty, // To test no email provided scenario // confirmationEmail = None confirmationEmail = Some("test@testmail.com") diff --git a/app/views/verify/VerificationRequestSubmittedView.scala.html b/app/views/verify/VerificationRequestSubmittedView.scala.html index cad66c56..4087980a 100644 --- a/app/views/verify/VerificationRequestSubmittedView.scala.html +++ b/app/views/verify/VerificationRequestSubmittedView.scala.html @@ -69,22 +69,24 @@ ) ) - @govukSummaryList( - SummaryListViewModel( - rows = Seq( - SummaryListRowViewModel( - key = messages("verify.verificationRequestSubmitted.subcontractorsToVerify.label"), - value = ValueViewModel( - HtmlContent( - vm.subcontractorsToVerify - .map(name => s"
  • $name
  • ") - .mkString("") + @if(vm.showVerify) { + @govukSummaryList( + SummaryListViewModel( + rows = Seq( + SummaryListRowViewModel( + key = messages("verify.verificationRequestSubmitted.subcontractorsToVerify.label"), + value = ValueViewModel( + HtmlContent( + vm.subcontractorsToVerify + .map(name => s"
  • $name
  • ") + .mkString("") + ) ) ) ) ) ) - ) + } @if(vm.showReverify) { @govukSummaryList( @@ -121,7 +123,7 @@ @link( linkTextKey = "verify.verificationRequestSubmitted.email.verification.link", linkUrl = appConfig.cisGeneralEnquiries, // TODO: Link to be updated later - // TODO: Link to be opened in new tab ? isNewTab = true, + hasFullStop = true, prefixTextKey = "verify.verificationRequestSubmitted.email.verification" ) @@ -144,7 +146,7 @@ @link( linkTextKey = "verify.verificationRequestSubmitted.needHelp.contactHMRC.link", linkUrl = appConfig.cisGeneralEnquiries, - // TODO: Link to be opened in new tab ? isNewTab = true, + hasFullStop = true, prefixTextKey = "verify.verificationRequestSubmitted.needHelp.p1", ) @@ -156,10 +158,8 @@ @link( linkTextKey = "verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link", - linkUrl = appConfig.manageSubcontractorsUrl, // TODO: Link to be confirmed and verified - // TODO: Validate the URL in application.conf file - // manageSubcontractors = "http://localhost:6996/construction-industry-scheme/management/manage-subcontractors" - // TODO: Link to be opened in new tab ? isNewTab = true, + linkUrl = appConfig.manageSubcontractorsUrl, // TODO: Link to be updated later. + hasFullStop = true, prefixTextKey = "verify.verificationRequestSubmitted.needHelp.p3", ) @@ -176,7 +176,6 @@ @link( "verify.verificationRequestSubmitted.feedback.survey.link", appConfig.exitSurveyUrl, // TODO: Link to be confirmed & verified - isNewTab = true, // TODO: Link to be opened in new tab ? suffixTextKey = "verify.verificationRequestSubmitted.feedback.p2" ) } diff --git a/conf/messages.en b/conf/messages.en index 7bf391c0..e791077b 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -858,17 +858,17 @@ verify.verificationRequestSubmitted.submittedAt = Submi verify.verificationRequestSubmitted.details.subHeading = Verification request details verify.verificationRequestSubmitted.subcontractorsToVerify.label = Subcontractors to verify verify.verificationRequestSubmitted.subcontractorsToReverify.label = Subcontractors to reverify -verify.verificationRequestSubmitted.email.confirmation = Confirmation of submission has been sent to {0} +verify.verificationRequestSubmitted.email.confirmation = Confirmation of submission has been sent to {0}. verify.verificationRequestSubmitted.email.verification = You can access this request in read-only format from your -verify.verificationRequestSubmitted.email.verification.link = verification history. +verify.verificationRequestSubmitted.email.verification.link = verification history verify.verificationRequestSubmitted.print.text = You can also save this page or print a copy for your records. verify.verificationRequestSubmitted.print.link = Print this page verify.verificationRequestSubmitted.needHelp.subHeading = Need help? -verify.verificationRequestSubmitted.needHelp.contactHMRC.link = contact HMRC. +verify.verificationRequestSubmitted.needHelp.contactHMRC.link = contact HMRC verify.verificationRequestSubmitted.needHelp.p1 = For any questions about your verification request, verify.verificationRequestSubmitted.needHelp.p2 = You will need the reference number of this verification request. verify.verificationRequestSubmitted.needHelp.p3 = Back to -verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link = Manage your subcontractors. +verify.verificationRequestSubmitted.needHelp.manageSubcontractors.link = Manage your subcontractors verify.verificationRequestSubmitted.feedback.subHeading = Before you go verify.verificationRequestSubmitted.feedback.p1 = Your feedback helps us make our service better. verify.verificationRequestSubmitted.feedback.p2 = to share your feedback on this service. From 40f48e729fe219e8554fa8bcc523649f7290a1d0 Mon Sep 17 00:00:00 2001 From: abhinavgupta-hmrc <269448599+abhinavgupta-hmrc@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:06:55 +0100 Subject: [PATCH 04/48] DTR-4472 Fix: resolve merge issues --- conf/app.routes | 7 +++++++ conf/messages.en | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/conf/app.routes b/conf/app.routes index fe713b79..1eab4a19 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -364,6 +364,8 @@ GET /verify/no-subcontractors-added controllers.verify.NoSubc GET /verification/newest controllers.verification.NewestVerificationBatchController.onPageLoad() +GET /verify/current controllers.verify.CurrentVerificationBatchController.onPageLoad() + GET /verify/verification-request-in-progress controllers.verify.VerificationRequestInProgressController.onPageLoad() GET /verify/confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onPageLoad(mode: Mode = NormalMode) @@ -371,6 +373,11 @@ POST /verify/confirmation-email-stored controllers.verify.Contra GET /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onPageLoad(mode: Mode = CheckMode) POST /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onSubmit(mode: Mode = CheckMode) +GET /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = NormalMode, page: Int ?= 1) +POST /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onSubmit(mode: Mode = NormalMode, page: Int ?= 1) +GET /verify/change-select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = CheckMode, page: Int ?= 1) +POST /verify/change-select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onSubmit(mode: Mode = CheckMode, page: Int ?= 1) + GET /verify/enter-confirmation-email controllers.verify.EmailAddressController.onPageLoad(mode: Mode = NormalMode) POST /verify/enter-confirmation-email controllers.verify.EmailAddressController.onSubmit(mode: Mode = NormalMode) GET /verify/change-enter-confirmation-email controllers.verify.EmailAddressController.onPageLoad(mode: Mode = CheckMode) diff --git a/conf/messages.en b/conf/messages.en index e791077b..e9adcfe0 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -53,6 +53,10 @@ checkYourAnswers.heading.h2 = Now add this subcontractor checkYourAnswers.p1 = By adding this subcontractor you are confirming that, to the best of your knowledge, the details you are providing are correct. checkYourAnswers.addSubcontractor = Accept and submit +site.pagination.previous = Previous +site.pagination.next = Next +site.pagination.landmark = Pagination + # Errors & Auth pageNotFound.title = Page not found pageNotFound.heading = Page not found @@ -298,7 +302,7 @@ partnershipWorksReferenceNumberYesNo.title = Does thi partnershipWorksReferenceNumberYesNo.heading = Does {0} have a works reference number? partnershipWorksReferenceNumberYesNo.hint = This is the reference number used to identify your subcontractor’s work or project. It can contain letters and numbers partnershipWorksReferenceNumberYesNo.checkYourAnswersLabel = Add works reference number? -partnershipWorksReferenceNumberYesNo.error.required = Select one option +partnershipWorksReferenceNumberYesNo.error.required = Select whether this partnership has a works reference number partnershipWorksReferenceNumberYesNo.change.hidden = add works reference number? partnershipName.title = What is the partnership name? @@ -827,7 +831,15 @@ verify.verificationRequestInProgress.p2 = You m verify.verificationRequestInProgress.p3.link = Contact HMRC verify.verificationRequestInProgress.p3 = if you have any questions about this verification request. verify.verificationRequestInProgress.p4 = Back to -verify.verificationRequestInProgress.p4.link = Manage your subcontractors. +verify.verificationRequestInProgress.p4.link = Manage your subcontractors + +verify.selectSubcontractor.title = Which subcontractors do you want to verify? +verify.selectSubcontractor.heading = Which subcontractors do you want to verify? +verify.selectSubcontractor.hint = Select the unverified subcontractors you want to add to this verification request +verify.selectSubcontractor.checkYourAnswersLabel = Subcontractors to verify +verify.selectSubcontractor.showingResults = Showing {0} to {1} of {2} results +verify.selectSubcontractor.error.required = Select at least one subcontractor to verify +verify.selectSubcontractor.change.hidden = subcontractors to verify verify.emailAddress.title = What email address should HMRC send this confirmation to? verify.emailAddress.heading = What email address should HMRC send this confirmation to? From 0fdbf79dbd0093eaad85bc833862ed1ce2fad086 Mon Sep 17 00:00:00 2001 From: abhinavgupta-hmrc <269448599+abhinavgupta-hmrc@users.noreply.github.com> Date: Tue, 5 May 2026 16:31:21 +0100 Subject: [PATCH 05/48] DTR-4472 refactor: integration implmented for two data source --- ...rificationRequestSubmittedController.scala | 21 +----- ...rificationRequestSubmittedViewModel.scala} | 22 +++++- ...erificationRequestSubmittedView.scala.html | 4 +- ...cationRequestSubmittedControllerSpec.scala | 32 +------- ...cationRequestSubmittedViewModelSpec.scala} | 74 +++++++++++++++++-- ...VerificationRequestSubmittedViewSpec.scala | 10 +-- 6 files changed, 99 insertions(+), 64 deletions(-) rename app/viewmodels/checkAnswers/verify/{VerificationSubmittedViewModel.scala => VerificationRequestSubmittedViewModel.scala} (56%) rename test/viewmodels/checkAnswers/verify/{VerificationSubmittedViewModelSpec.scala => VerificationRequestSubmittedViewModelSpec.scala} (56%) diff --git a/app/controllers/verify/VerificationRequestSubmittedController.scala b/app/controllers/verify/VerificationRequestSubmittedController.scala index b705584a..8532d0ff 100644 --- a/app/controllers/verify/VerificationRequestSubmittedController.scala +++ b/app/controllers/verify/VerificationRequestSubmittedController.scala @@ -21,10 +21,8 @@ import controllers.actions.* import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController -import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +import viewmodels.checkAnswers.verify.VerificationRequestSubmittedViewModel import views.html.verify.VerificationRequestSubmittedView - -import java.time.LocalDateTime import javax.inject.Inject class VerificationRequestSubmittedController @Inject() ( @@ -43,20 +41,9 @@ class VerificationRequestSubmittedController @Inject() ( // TODO: Replace this with your real source: // TODO: Replace 1. referenceNumber 2. submittedAt 3. verify list 4. Reverify lists 5. confirmationEmail - val vm = VerificationSubmittedViewModel( - referenceNumber = "Reference number 12345", - submittedAt = LocalDateTime.now(), - subcontractorsToVerify = - Seq("Brody, Martin", "Hooper And Associates", "Quint Transportation", "The Kintner Group"), - // Seq.empty, - // To check the validation of empty reverify list - subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), - // Seq.empty, - // To test no email provided scenario - // confirmationEmail = None - confirmationEmail = Some("test@testmail.com") - ) - + val vm = + VerificationRequestSubmittedViewModel + .fromUserAnswers(request.userAnswers) Ok(view(vm)) } } diff --git a/app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala b/app/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModel.scala similarity index 56% rename from app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala rename to app/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModel.scala index e5d6a0d1..5b4edc81 100644 --- a/app/viewmodels/checkAnswers/verify/VerificationSubmittedViewModel.scala +++ b/app/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModel.scala @@ -15,9 +15,11 @@ */ package viewmodels.checkAnswers.verify +import models.UserAnswers +import pages.verify._ import java.time.LocalDateTime -case class VerificationSubmittedViewModel( +case class VerificationRequestSubmittedViewModel( referenceNumber: String, submittedAt: LocalDateTime, subcontractorsToVerify: Seq[String], @@ -28,3 +30,21 @@ case class VerificationSubmittedViewModel( def showVerify: Boolean = subcontractorsToVerify.nonEmpty def showReverify: Boolean = subcontractorsToReverify.nonEmpty } + +object VerificationRequestSubmittedViewModel { + + def fromUserAnswers(userAnswers: UserAnswers): VerificationRequestSubmittedViewModel = + VerificationRequestSubmittedViewModel( + // TODO: Replace below with actuals - 1. referenceNumber 2. submittedAt + referenceNumber = "Reference Number 12345", + submittedAt = LocalDateTime.now(), + subcontractorsToVerify = userAnswers + .get(SelectSubcontractorPage) + .getOrElse(Seq.empty) + .map(_.name) + .toSeq, + // TODO: Replace below hardcoded values with - SelectSubcontractorsToReverifyPage + subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), + confirmationEmail = userAnswers.get(EmailAddressPage) + ) +} diff --git a/app/views/verify/VerificationRequestSubmittedView.scala.html b/app/views/verify/VerificationRequestSubmittedView.scala.html index 4087980a..afb92ac8 100644 --- a/app/views/verify/VerificationRequestSubmittedView.scala.html +++ b/app/views/verify/VerificationRequestSubmittedView.scala.html @@ -24,7 +24,7 @@ @import viewmodels.govuk.summarylist.SummaryListViewModel @import views.html.components._ -@import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +@import viewmodels.checkAnswers.verify.VerificationRequestSubmittedViewModel @this( @@ -39,7 +39,7 @@ link: Link ) -@(vm: VerificationSubmittedViewModel +@(vm: VerificationRequestSubmittedViewModel )(implicit request: Request[_],appConfig: FrontendAppConfig, messages: Messages) @layout(pageTitle = titleNoForm(messages("verify.verificationRequestSubmitted.title"))) { diff --git a/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala b/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala index fa693107..6740bdd9 100644 --- a/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala +++ b/test/controllers/verify/VerificationRequestSubmittedControllerSpec.scala @@ -19,16 +19,12 @@ package controllers.verify import base.SpecBase import play.api.test.FakeRequest import play.api.test.Helpers.* -import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel -import views.html.verify.VerificationRequestSubmittedView - -import java.time.LocalDateTime class VerificationRequestSubmittedControllerSpec extends SpecBase { "VerificationRequestSubmitted Controller" - { - "must return OK and the correct view for a GET" in { + "must return OK for a GET" in { val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() @@ -44,33 +40,7 @@ class VerificationRequestSubmittedControllerSpec extends SpecBase { val result = route(application, request).value - val view = - application.injector.instanceOf[VerificationRequestSubmittedView] - - val appConfig = - application.injector.instanceOf[config.FrontendAppConfig] - - val expectedViewModel = - VerificationSubmittedViewModel( - referenceNumber = "Reference number 12345", - submittedAt = LocalDateTime.now(), - subcontractorsToVerify = Seq( - "Brody, Martin", - "Hooper And Associates", - "Quint Transportation", - "The Kintner Group" - ), - subcontractorsToReverify = Seq("Grant, Alan", "InGen Research"), - confirmationEmail = Some("test@testmail.com") - ) - status(result) mustEqual OK - contentAsString(result) mustEqual - view(expectedViewModel)( - request, - appConfig, - messages(application) - ).toString } } } diff --git a/test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala b/test/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModelSpec.scala similarity index 56% rename from test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala rename to test/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModelSpec.scala index ce5d8eaa..339d4353 100644 --- a/test/viewmodels/checkAnswers/verify/VerificationSubmittedViewModelSpec.scala +++ b/test/viewmodels/checkAnswers/verify/VerificationRequestSubmittedViewModelSpec.scala @@ -16,23 +16,27 @@ package viewmodels.checkAnswers.verify +import models.UserAnswers +import models.SubcontractorViewModel import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers +import org.scalatest.TryValues.convertTryToSuccessOrFailure +import pages.verify.{EmailAddressPage, SelectSubcontractorPage} import java.time.LocalDateTime -class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { +class VerificationRequestSubmittedViewModelSpec extends AnyFreeSpec with Matchers { private val now = LocalDateTime.of(2026, 4, 27, 10, 30) - "VerificationSubmittedViewModel" - { + "VerificationRequestSubmittedViewModel" - { "showEmail" - { "must return true when confirmationEmail is defined" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq("Sub A"), @@ -45,7 +49,7 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { "must return false when confirmationEmail is not defined" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq("Sub A"), @@ -61,7 +65,7 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { "must return true when subcontractorsToVerify is non-empty" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq("Sub A") @@ -73,7 +77,7 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { "must return false when subcontractorsToVerify is empty" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq.empty @@ -88,7 +92,7 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { "must return true when subcontractorsToReverify is non-empty" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq("Sub A"), @@ -101,7 +105,7 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { "must return false when subcontractorsToReverify is empty" in { val vm = - VerificationSubmittedViewModel( + VerificationRequestSubmittedViewModel( referenceNumber = "REF123", submittedAt = now, subcontractorsToVerify = Seq("Sub A"), @@ -112,4 +116,58 @@ class VerificationSubmittedViewModelSpec extends AnyFreeSpec with Matchers { } } } + + "VerificationRequestSubmittedViewModel.fromUserAnswers" - { + + "must map subcontractorsToVerify from SelectSubcontractorPage" in { + + val subcontractors = + Set( + SubcontractorViewModel(id = "ID1", name = "Brody, Martin"), + SubcontractorViewModel(id = "ID2", name = "Hooper And Associates") + ) + + val userAnswers = + UserAnswers("id") + .set(SelectSubcontractorPage, subcontractors) + .success + .value + + val vm = + VerificationRequestSubmittedViewModel.fromUserAnswers(userAnswers) + + vm.subcontractorsToVerify shouldBe + Seq("Brody, Martin", "Hooper And Associates") + + vm.showVerify shouldBe true + } + + "must map confirmationEmail from EmailAddressPage when present" in { + + val userAnswers = + UserAnswers("id") + .set(EmailAddressPage, "test@testmail.com") + .success + .value + + val vm = + VerificationRequestSubmittedViewModel.fromUserAnswers(userAnswers) + + vm.confirmationEmail shouldBe Some("test@testmail.com") + vm.showEmail shouldBe true + } + + "must return empty values when pages are absent" in { + + val vm = + VerificationRequestSubmittedViewModel.fromUserAnswers( + UserAnswers("id") + ) + + vm.subcontractorsToVerify shouldBe Seq.empty + vm.confirmationEmail shouldBe None + vm.showVerify shouldBe false + vm.showEmail shouldBe false + } + } } diff --git a/test/views/verify/VerificationRequestSubmittedViewSpec.scala b/test/views/verify/VerificationRequestSubmittedViewSpec.scala index 586d2ef1..3f239c0a 100644 --- a/test/views/verify/VerificationRequestSubmittedViewSpec.scala +++ b/test/views/verify/VerificationRequestSubmittedViewSpec.scala @@ -24,7 +24,7 @@ import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.i18n.{Lang, Messages, MessagesApi, MessagesImpl} import play.api.mvc.Request import play.api.test.FakeRequest -import viewmodels.checkAnswers.verify.VerificationSubmittedViewModel +import viewmodels.checkAnswers.verify.VerificationRequestSubmittedViewModel import views.html.verify.VerificationRequestSubmittedView import java.time.LocalDateTime @@ -104,7 +104,7 @@ class VerificationRequestSubmittedViewSpec extends AnyWordSpec with Matchers wit doc.select(s"a[href='${appConfig.exitSurveyUrl}']") surveyLink.size mustBe 1 - surveyLink.attr("target") mustBe "_blank" + surveyLink.attr("target") mustBe "" surveyLink.first.parent.text must include( messages("verify.verificationRequestSubmitted.feedback.p2") @@ -113,7 +113,7 @@ class VerificationRequestSubmittedViewSpec extends AnyWordSpec with Matchers wit "not render reverify section or email paragraph when both are absent" in new Setup { - override val viewModel: VerificationSubmittedViewModel = + override val viewModel: VerificationRequestSubmittedViewModel = viewModel.copy( subcontractorsToReverify = Seq.empty, confirmationEmail = None @@ -164,8 +164,8 @@ class VerificationRequestSubmittedViewSpec extends AnyWordSpec with Matchers wit val email = "test@testmail.com" - val viewModel: VerificationSubmittedViewModel = - VerificationSubmittedViewModel( + val viewModel: VerificationRequestSubmittedViewModel = + VerificationRequestSubmittedViewModel( referenceNumber = referenceNumber, submittedAt = submittedAt, subcontractorsToVerify = subcontractorsToVerify, From a2e5d3d4c4dbd4321f6b18265333d6bca2219751 Mon Sep 17 00:00:00 2001 From: Richy Jassal <20478717+jassalrichy@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:42:03 +0100 Subject: [PATCH 06/48] [DTR-4461] - add comma to stub data fopr lastname, firstname --- app/services/SubcontractorSource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/SubcontractorSource.scala b/app/services/SubcontractorSource.scala index 88ba1031..1c395071 100644 --- a/app/services/SubcontractorSource.scala +++ b/app/services/SubcontractorSource.scala @@ -24,7 +24,7 @@ object SubcontractorSource { // Placeholder list — TODO will be replaced with a backend call when the connector is implemented val subcontractors: Seq[SubcontractorViewModel] = Seq( - SubcontractorViewModel("100", "Brody Martin"), + SubcontractorViewModel("100", "Brody, Martin"), SubcontractorViewModel("95", "Hooper Associates"), SubcontractorViewModel("96", "Alpha Plumbing"), SubcontractorViewModel("98", "Beta Builders"), From 4491a5b5636e8a7a77618f9d63a829ca5c10419d Mon Sep 17 00:00:00 2001 From: Edward Pau <232416446+edpau-hmrc@users.noreply.github.com> Date: Fri, 1 May 2026 15:29:33 +0100 Subject: [PATCH 07/48] DTR-4694: CIS VSF: Screen SM-07 Verification failure (departmental error) (#156) * DTR-4694: Screen SM-07 Verification failure (departmental error) * DTR-4694: apply scalafmtAll * DTR-4694: remove unuse GovukButton and back link --- .../VerifyDepartmentalErrorController.scala | 50 ++++++++++ .../VerifyDepartmentalErrorView.scala.html | 52 ++++++++++ conf/app.routes | 2 + conf/messages.en | 8 ++ ...erifyDepartmentalErrorControllerSpec.scala | 98 +++++++++++++++++++ .../VerifyDepartmentalErrorViewSpec.scala | 93 ++++++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 app/controllers/verify/VerifyDepartmentalErrorController.scala create mode 100644 app/views/verify/VerifyDepartmentalErrorView.scala.html create mode 100644 test/controllers/verify/VerifyDepartmentalErrorControllerSpec.scala create mode 100644 test/views/verify/VerifyDepartmentalErrorViewSpec.scala diff --git a/app/controllers/verify/VerifyDepartmentalErrorController.scala b/app/controllers/verify/VerifyDepartmentalErrorController.scala new file mode 100644 index 00000000..70bc29a1 --- /dev/null +++ b/app/controllers/verify/VerifyDepartmentalErrorController.scala @@ -0,0 +1,50 @@ +/* + * 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 controllers.verify + +import config.FrontendAppConfig +import controllers.actions.* +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import queries.CisIdQuery +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.verify.VerifyDepartmentalErrorView + +import javax.inject.Inject + +class VerifyDepartmentalErrorController @Inject() ( + override val messagesApi: MessagesApi, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + val controllerComponents: MessagesControllerComponents, + view: VerifyDepartmentalErrorView, + appConfig: FrontendAppConfig +) extends FrontendBaseController + with I18nSupport { + + def onPageLoad: Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => + request.userAnswers.get(CisIdQuery) match { + case Some(cisId) => + val manageSubcontractorsUrl = s"${appConfig.manageSubcontractorsUrl}/$cisId" + Ok(view(manageSubcontractorsUrl)) + + case None => + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + } + } +} diff --git a/app/views/verify/VerifyDepartmentalErrorView.scala.html b/app/views/verify/VerifyDepartmentalErrorView.scala.html new file mode 100644 index 00000000..e4fb848b --- /dev/null +++ b/app/views/verify/VerifyDepartmentalErrorView.scala.html @@ -0,0 +1,52 @@ +@* + * 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. + *@ + +@import views.html.components._ + +@this( + layout: templates.Layout, + header: H1, + paragraph: Paragraph, + link: Link +) + +@(manageSubcontractorsUrl: String)(implicit request: Request[_], messages: Messages) + +@layout( + pageTitle = titleNoForm(messages("verify.verifyDepartmentalError.title")), + showBackLink = false +) { + + @header(messages("verify.verifyDepartmentalError.heading"), classes = "govuk-heading-xl") + + @paragraph(messages("verify.verifyDepartmentalError.p1")) + + @link( + linkTextKey = "verify.verifyDepartmentalError.contactHMRC.p1.link", + linkUrl = "https://www.gov.uk/find-hmrc-contacts/construction-industry-scheme-general-enquiries", + hasFullStop = false, + suffixTextKey = "verify.verifyDepartmentalError.contactHMRC.p1", + isNewTab = false + ) + + @link( + linkTextKey = "verify.verifyDepartmentalError.manageSubcontractors.p1.link", + linkUrl = manageSubcontractorsUrl, + hasFullStop = true, + prefixTextKey = "verify.verifyDepartmentalError.manageSubcontractors.p1", + isNewTab = false + ) +} diff --git a/conf/app.routes b/conf/app.routes index 1eab4a19..02a4455d 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -391,4 +391,6 @@ POST /verify/change-reverify-existing-subcontractors controllers.verify.Reveri GET /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onPageLoad POST /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onSubmit +GET /verify/problem-with-verification-warning controllers.verify.VerifyDepartmentalErrorController.onPageLoad() + GET /verify/verification-request-submitted controllers.verify.VerificationRequestSubmittedController.onPageLoad() diff --git a/conf/messages.en b/conf/messages.en index e9adcfe0..0a310a5c 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -863,6 +863,14 @@ verify.reverifyExistingSubcontractorsYesNo.error.required = Selec verify.reverifyExistingSubcontractorsYesNo.checkYourAnswersLabel = Do you want to add other subcontractors to this verification request? verify.reverifyExistingSubcontractorsYesNo.change.hidden = do you want to add other subcontractors to this verification request? +verify.verifyDepartmentalError.title = There was a problem with your verification request +verify.verifyDepartmentalError.heading = There was a problem with your verification request +verify.verifyDepartmentalError.p1 = The subcontractors that you selected have not been verified and your request has not been saved. +verify.verifyDepartmentalError.contactHMRC.p1 = for more help. +verify.verifyDepartmentalError.contactHMRC.p1.link = Contact HMRC by phone +verify.verifyDepartmentalError.manageSubcontractors.p1 = Back to +verify.verifyDepartmentalError.manageSubcontractors.p1.link = Manage your subcontractors + verify.verificationRequestSubmitted.title = Verification request submitted verify.verificationRequestSubmitted.heading = Verification request submitted verify.verificationRequestSubmitted.reference = Your verification reference number diff --git a/test/controllers/verify/VerifyDepartmentalErrorControllerSpec.scala b/test/controllers/verify/VerifyDepartmentalErrorControllerSpec.scala new file mode 100644 index 00000000..7d3f0280 --- /dev/null +++ b/test/controllers/verify/VerifyDepartmentalErrorControllerSpec.scala @@ -0,0 +1,98 @@ +/* + * 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 controllers.verify + +import base.SpecBase +import controllers.routes +import models.UserAnswers +import org.mockito.Mockito.* +import org.mockito.ArgumentMatchers.any +import play.api.inject.bind +import org.scalatestplus.mockito.MockitoSugar +import play.api.test.FakeRequest +import play.api.test.Helpers.* +import repositories.SessionRepository +import views.html.verify.VerifyDepartmentalErrorView + +import scala.concurrent.Future +import queries.CisIdQuery + +class VerifyDepartmentalErrorControllerSpec extends SpecBase with MockitoSugar { + + private val cisId = "12345" + + "VerifyDepartmentalError Controller" - { + + "must return OK and the correct view for a GET when cisId is in ua" in { + + def ua: UserAnswers = + emptyUserAnswers + .set(CisIdQuery, cisId) + .success + .value + + val mockRepo = mock[SessionRepository] + + when(mockRepo.set(any())).thenReturn(Future.successful(true)) + + val application = + applicationBuilder(userAnswers = Some(ua)) + .overrides( + bind[SessionRepository].toInstance(mockRepo) + ) + .build() + + running(application) { + val request = FakeRequest(GET, controllers.verify.routes.VerifyDepartmentalErrorController.onPageLoad().url) + + val result = route(application, request).value + + val view = application.injector.instanceOf[VerifyDepartmentalErrorView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view(s"${applicationConfig.manageSubcontractorsUrl}/$cisId")( + request, + messages(application) + ).toString + } + } + + "must redirect to JourneyRecovery for a GET when CisId is missing" in { + def ua: UserAnswers = emptyUserAnswers + + val mockRepo = mock[SessionRepository] + + when(mockRepo.set(any())).thenReturn(Future.successful(true)) + + val application = + applicationBuilder(userAnswers = Some(ua)) + .overrides( + bind[SessionRepository].toInstance(mockRepo) + ) + .build() + + running(application) { + val request = FakeRequest(GET, controllers.verify.routes.VerifyDepartmentalErrorController.onPageLoad().url) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + } + } + } +} diff --git a/test/views/verify/VerifyDepartmentalErrorViewSpec.scala b/test/views/verify/VerifyDepartmentalErrorViewSpec.scala new file mode 100644 index 00000000..e5736da5 --- /dev/null +++ b/test/views/verify/VerifyDepartmentalErrorViewSpec.scala @@ -0,0 +1,93 @@ +/* + * 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 views.verify + +import org.jsoup.nodes.Document +import org.jsoup.select.Elements +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.i18n.Messages +import play.api.mvc.Request +import play.api.test.FakeRequest +import play.twirl.api.HtmlFormat +import views.html.verify.VerifyDepartmentalErrorView + +class VerifyDepartmentalErrorViewSpec extends AnyWordSpec with Matchers with GuiceOneAppPerSuite { + "VerifyDepartmentalErrorView" should { + + "render the page with correct title, heading, paragraphs and both links" in new Setup { + + private val manageSubcontractorsUrl = + "http://localhost:6996/construction-industry-scheme/management/manage-subcontractors/12345" + + private val contactHMRCURL = + "https://www.gov.uk/find-hmrc-contacts/construction-industry-scheme-general-enquiries" + + val html: HtmlFormat.Appendable = view(manageSubcontractorsUrl) + val doc: Document = org.jsoup.Jsoup.parse(html.toString()) + + doc.select("title").text() must include(messages("verify.verifyDepartmentalError.title")) + + doc.select("h1").text must include(messages("verify.verifyDepartmentalError.heading")) + + doc.select("p").text must include(messages("verify.verifyDepartmentalError.p1")) + + doc.select("p").text must include(messages("verify.verifyDepartmentalError.contactHMRC.p1")) + doc.getElementsByClass("govuk-link").text must include( + messages("verify.verifyDepartmentalError.contactHMRC.p1.link") + ) + + val contactHMRCLink: Elements = + doc.select(s"a[href='$contactHMRCURL']") + contactHMRCLink.size() mustBe 1 + contactHMRCLink.text() mustBe + messages("verify.verifyDepartmentalError.contactHMRC.p1.link") + + val contactHMRText: String = contactHMRCLink.first().parent().text() + + contactHMRText must include( + messages("verify.verifyDepartmentalError.contactHMRC.p1") + ) + + val manageLink: Elements = + doc.select(s"a[href='$manageSubcontractorsUrl']") + manageLink.size() mustBe 1 + manageLink.text() mustBe + messages("verify.verifyDepartmentalError.manageSubcontractors.p1.link") + + val manageText: String = manageLink.first().parent().text() + + manageText must include( + messages("verify.verifyDepartmentalError.manageSubcontractors.p1") + ) + + manageText.trim.endsWith(".") mustBe true + } + } + + trait Setup { + implicit val request: Request[_] = FakeRequest() + implicit val messages: Messages = + play.api.i18n.MessagesImpl( + play.api.i18n.Lang.defaultLang, + app.injector.instanceOf[play.api.i18n.MessagesApi] + ) + + val view: VerifyDepartmentalErrorView = app.injector.instanceOf[VerifyDepartmentalErrorView] + } +} From f7763eb5689ebf5d76b3defb3b07d858caabe73e Mon Sep 17 00:00:00 2001 From: codeneto-hmrc <253322371+codeneto-hmrc@users.noreply.github.com> Date: Fri, 1 May 2026 09:01:29 +0100 Subject: [PATCH 08/48] DTR-4160: update GetNewestVerificationBatchResponse and GetCurrentVerificationBatchResponse --- app/models/Subcontractor.scala | 8 +++- .../GetCurrentVerificationBatchResponse.scala | 2 +- .../GetNewestVerificationBatchResponse.scala | 8 ++-- test-utils/generators/ModelGenerators.scala | 14 ++++++- ...structionIndustrySchemeConnectorSpec.scala | 10 ++--- ...mailConfirmationStoredControllerSpec.scala | 12 +++--- .../verify/EmailAddressControllerSpec.scala | 8 ++-- ...CurrentVerificationBatchResponseSpec.scala | 6 +-- ...tNewestVerificationBatchResponseSpec.scala | 38 ++++++++++++------- ...estVerificationBatchResponsePageSpec.scala | 24 ++++++------ ...entVerificationBatchResponsePageSpec.scala | 6 +-- test/services/VerificationServiceSpec.scala | 10 ++--- 12 files changed, 88 insertions(+), 58 deletions(-) diff --git a/app/models/Subcontractor.scala b/app/models/Subcontractor.scala index 1ad0f620..3e28d2f8 100644 --- a/app/models/Subcontractor.scala +++ b/app/models/Subcontractor.scala @@ -31,7 +31,13 @@ case class Subcontractor( taxTreatment: Option[String], verificationDate: Option[LocalDateTime], lastMonthlyReturnDate: Option[LocalDateTime], - createDate: Option[LocalDateTime] + createDate: Option[LocalDateTime], + subcontractorType: Option[String], + subbieResourceRef: Option[Long], + utr: Option[String], + partnerUtr: Option[String], + crn: Option[String], + nino: Option[String] ) object Subcontractor: diff --git a/app/models/response/GetCurrentVerificationBatchResponse.scala b/app/models/response/GetCurrentVerificationBatchResponse.scala index f3712a68..ec221471 100644 --- a/app/models/response/GetCurrentVerificationBatchResponse.scala +++ b/app/models/response/GetCurrentVerificationBatchResponse.scala @@ -21,7 +21,7 @@ import models.* final case class GetCurrentVerificationBatchResponse( subcontractors: Seq[SubcontractorCurrentVerification], - verificationBatch: Seq[VerificationBatchCurrentVerification], + verificationBatch: Option[VerificationBatchCurrentVerification], verifications: Seq[VerificationCurrentVerification] ) diff --git a/app/models/response/GetNewestVerificationBatchResponse.scala b/app/models/response/GetNewestVerificationBatchResponse.scala index 8cb67680..90e6ca5e 100644 --- a/app/models/response/GetNewestVerificationBatchResponse.scala +++ b/app/models/response/GetNewestVerificationBatchResponse.scala @@ -20,12 +20,12 @@ import play.api.libs.json.{Json, OFormat} import models.* final case class GetNewestVerificationBatchResponse( - scheme: Seq[ContractorScheme], + scheme: Option[ContractorScheme], subcontractors: Seq[Subcontractor], - verificationBatch: Seq[VerificationBatch], + verificationBatch: Option[VerificationBatch], verifications: Seq[Verification], - submission: Seq[Submission], - monthlyReturn: Seq[MonthlyReturn] + submission: Option[Submission], + monthlyReturn: Option[MonthlyReturn] ) object GetNewestVerificationBatchResponse { diff --git a/test-utils/generators/ModelGenerators.scala b/test-utils/generators/ModelGenerators.scala index 1e87db61..0531ef9b 100644 --- a/test-utils/generators/ModelGenerators.scala +++ b/test-utils/generators/ModelGenerators.scala @@ -95,6 +95,12 @@ trait ModelGenerators { verificationDate <- Gen.option(genLocalDateTime) lastMonthlyReturnDate <- Gen.option(genLocalDateTime) createDate <- Gen.option(genLocalDateTime) + subcontractorType <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + subbieResourceRef <- Gen.option(Gen.posNum[Long]) + utr <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + partnerUtr <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + crn <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + nino <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) } yield Subcontractor( subcontractorId = subcontractorId, firstName = firstName, @@ -107,7 +113,13 @@ trait ModelGenerators { taxTreatment = taxTreatment, verificationDate = verificationDate, lastMonthlyReturnDate = lastMonthlyReturnDate, - createDate = createDate + createDate = createDate, + subcontractorType = subcontractorType, + subbieResourceRef = subbieResourceRef, + utr = utr, + partnerUtr = partnerUtr, + crn = crn, + nino = nino ) } private val genLocalDateTime: Gen[LocalDateTime] = diff --git a/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala b/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala index 97df0b60..0b19c041 100644 --- a/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala +++ b/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala @@ -164,12 +164,12 @@ class ConstructionIndustrySchemeConnectorSpec extends AnyWordSpec with Matchers val expected = GetNewestVerificationBatchResponse( - scheme = Nil, + scheme = None, subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil, - submission = Nil, - monthlyReturn = Nil + submission = None, + monthlyReturn = None ) when(rb.execute[GetNewestVerificationBatchResponse](any(), any())) @@ -203,7 +203,7 @@ class ConstructionIndustrySchemeConnectorSpec extends AnyWordSpec with Matchers val expected = GetCurrentVerificationBatchResponse( subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil ) diff --git a/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala b/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala index f85ae1f2..c56c22b8 100644 --- a/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala +++ b/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala @@ -56,12 +56,12 @@ class ContractorEmailConfirmationStoredControllerSpec extends SpecBase with Mock ) private val testResponse = GetNewestVerificationBatchResponse( - scheme = Seq(testScheme), + scheme = Some(testScheme), subcontractors = Seq.empty, - verificationBatch = Seq.empty, + verificationBatch = None, verifications = Seq.empty, - submission = Seq.empty, - monthlyReturn = Seq.empty + submission = None, + monthlyReturn = None ) private def userAnswersWithEmail: UserAnswers = @@ -108,7 +108,7 @@ class ContractorEmailConfirmationStoredControllerSpec extends SpecBase with Mock "must redirect to Journey Recovery for a GET when the email address is absent from the scheme" in { - val responseWithNoEmail = testResponse.copy(scheme = Seq(testScheme.copy(emailAddress = None))) + val responseWithNoEmail = testResponse.copy(scheme = Some(testScheme.copy(emailAddress = None))) val userAnswers = emptyUserAnswers.set(NewestVerificationBatchResponsePage, responseWithNoEmail).success.value val application = applicationBuilder(userAnswers = Some(userAnswers)).build() @@ -196,7 +196,7 @@ class ContractorEmailConfirmationStoredControllerSpec extends SpecBase with Mock "must redirect to Journey Recovery for a POST when the email address is absent from the scheme" in { - val responseWithNoEmail = testResponse.copy(scheme = Seq(testScheme.copy(emailAddress = None))) + val responseWithNoEmail = testResponse.copy(scheme = Some(testScheme.copy(emailAddress = None))) val userAnswers = emptyUserAnswers.set(NewestVerificationBatchResponsePage, responseWithNoEmail).success.value val application = applicationBuilder(userAnswers = Some(userAnswers)).build() diff --git a/test/controllers/verify/EmailAddressControllerSpec.scala b/test/controllers/verify/EmailAddressControllerSpec.scala index b3b6f017..9b80a3ca 100644 --- a/test/controllers/verify/EmailAddressControllerSpec.scala +++ b/test/controllers/verify/EmailAddressControllerSpec.scala @@ -49,12 +49,12 @@ class EmailAddressControllerSpec extends SpecBase with MockitoSugar { ) private def response(email: Option[String]) = GetNewestVerificationBatchResponse( - scheme = Seq(scheme(email)), + scheme = Some(scheme(email)), subcontractors = Seq.empty, - verificationBatch = Seq.empty, + verificationBatch = None, verifications = Seq.empty, - submission = Seq.empty, - monthlyReturn = Seq.empty + submission = None, + monthlyReturn = None ) private def ua(email: Option[String]): UserAnswers = diff --git a/test/models/response/GetCurrentVerificationBatchResponseSpec.scala b/test/models/response/GetCurrentVerificationBatchResponseSpec.scala index 9fd35469..f442cdde 100644 --- a/test/models/response/GetCurrentVerificationBatchResponseSpec.scala +++ b/test/models/response/GetCurrentVerificationBatchResponseSpec.scala @@ -42,7 +42,7 @@ class GetCurrentVerificationBatchResponseSpec extends AnyWordSpec with Matchers partnershipTradingName = Some("ACME trading") ) ), - verificationBatch = Seq( + verificationBatch = Some( VerificationBatchCurrentVerification( verificationBatchId = 99L, verifBatchResourceRef = Some(999L) @@ -74,7 +74,7 @@ class GetCurrentVerificationBatchResponseSpec extends AnyWordSpec with Matchers (sub0 \ "partnerUtr").as[String] mustBe "5860920998" (sub0 \ "partnershipTradingName").as[String] mustBe "ACME trading" - val vb0 = (json \ "verificationBatch")(0) + val vb0 = json \ "verificationBatch" (vb0 \ "verificationBatchId").as[Long] mustBe 99L (vb0 \ "verifBatchResourceRef").as[Long] mustBe 999L @@ -90,7 +90,7 @@ class GetCurrentVerificationBatchResponseSpec extends AnyWordSpec with Matchers "round-trip (model -> json -> model) without losing data" in { val model = GetCurrentVerificationBatchResponse( subcontractors = Seq.empty, - verificationBatch = Seq.empty, + verificationBatch = None, verifications = Seq.empty ) diff --git a/test/models/response/GetNewestVerificationBatchResponseSpec.scala b/test/models/response/GetNewestVerificationBatchResponseSpec.scala index a28a83a1..b418c635 100644 --- a/test/models/response/GetNewestVerificationBatchResponseSpec.scala +++ b/test/models/response/GetNewestVerificationBatchResponseSpec.scala @@ -29,7 +29,7 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc "write a response to JSON" in { val model = GetNewestVerificationBatchResponse( - scheme = Seq( + scheme = Some( ContractorScheme( accountsOfficeReference = Some("123PA00123456"), utr = Some("1111111111"), @@ -50,10 +50,16 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc taxTreatment = Some("0"), verificationDate = Some(LocalDateTime.of(2026, 1, 12, 11, 0, 0)), lastMonthlyReturnDate = None, - createDate = Some(LocalDateTime.of(2026, 1, 4, 10, 0, 0)) + createDate = Some(LocalDateTime.of(2026, 1, 4, 10, 0, 0)), + subcontractorType = Some("soletrader"), + subbieResourceRef = Some(10L), + utr = Some("1111111111"), + partnerUtr = None, + crn = None, + nino = Some("AA123456A") ) ), - verificationBatch = Seq( + verificationBatch = Some( VerificationBatch( verificationBatchId = 99L, status = Some("STARTED"), @@ -70,7 +76,7 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc subcontractorId = Some(1L) ) ), - submission = Seq( + submission = Some( Submission( submissionId = 555L, activeObjectId = Some(99L), @@ -78,7 +84,7 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc submissionRequestDate = Some(LocalDateTime.of(2026, 1, 12, 11, 59, 0)) ) ), - monthlyReturn = Seq( + monthlyReturn = Some( MonthlyReturn( monthlyReturnId = 777L, decNoMoreSubPayments = Some("N") @@ -88,7 +94,7 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc val json = Json.toJson(model) - val scheme0 = (json \ "scheme")(0) + val scheme0 = json \ "scheme" (scheme0 \ "accountsOfficeReference").as[String] mustBe "123PA00123456" (scheme0 \ "utr").as[String] mustBe "1111111111" @@ -109,8 +115,14 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc (sub0 \ "verificationDate").as[String] mustBe "2026-01-12T11:00:00" (sub0 \ "lastMonthlyReturnDate").toOption mustBe None (sub0 \ "createDate").as[String] mustBe "2026-01-04T10:00:00" + (sub0 \ "subcontractorType").as[String] mustBe "soletrader" + (sub0 \ "subbieResourceRef").as[Long] mustBe 10L + (sub0 \ "utr").as[String] mustBe "1111111111" + (sub0 \ "partnerUtr").toOption mustBe None + (sub0 \ "crn").toOption mustBe None + (sub0 \ "nino").as[String] mustBe "AA123456A" - val vb0 = (json \ "verificationBatch")(0) + val vb0 = json \ "verificationBatch" (vb0 \ "verificationBatchId").as[Long] mustBe 99L (vb0 \ "status").as[String] mustBe "STARTED" @@ -125,14 +137,14 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc (v0 \ "verificationBatchId").as[Long] mustBe 99L (v0 \ "subcontractorId").as[Long] mustBe 1L - val subm0 = (json \ "submission")(0) + val subm0 = json \ "submission" (subm0 \ "submissionId").as[Long] mustBe 555L (subm0 \ "activeObjectId").as[Long] mustBe 99L (subm0 \ "status").as[String] mustBe "ACCEPTED" (subm0 \ "submissionRequestDate").as[String] mustBe "2026-01-12T11:59:00" - val mr0 = (json \ "monthlyReturn")(0) + val mr0 = json \ "monthlyReturn" (mr0 \ "monthlyReturnId").as[Long] mustBe 777L (mr0 \ "decNoMoreSubPayments").as[String] mustBe "N" @@ -141,12 +153,12 @@ final class GetNewestVerificationBatchResponseSpec extends AnyWordSpec with Matc "round-trip (model -> json -> model) without losing data" in { val model = GetNewestVerificationBatchResponse( - scheme = Seq.empty, + scheme = None, subcontractors = Seq.empty, - verificationBatch = Seq.empty, + verificationBatch = None, verifications = Seq.empty, - submission = Seq.empty, - monthlyReturn = Seq( + submission = None, + monthlyReturn = Some( MonthlyReturn( monthlyReturnId = 777L, decNoMoreSubPayments = Some("N") diff --git a/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala b/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala index a61c1b3e..caf55945 100644 --- a/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala +++ b/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala @@ -30,12 +30,12 @@ class NewestVerificationBatchResponsePageSpec extends SpecBase { "must be able to set and get a value" in { val model = GetNewestVerificationBatchResponse( - scheme = Nil, + scheme = None, subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil, - submission = Nil, - monthlyReturn = Nil + submission = None, + monthlyReturn = None ) val uaWithValue = @@ -49,12 +49,12 @@ class NewestVerificationBatchResponsePageSpec extends SpecBase { "must be able to remove the value" in { val model = GetNewestVerificationBatchResponse( - scheme = Nil, + scheme = None, subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil, - submission = Nil, - monthlyReturn = Nil + submission = None, + monthlyReturn = None ) val uaWithValue = @@ -74,12 +74,12 @@ class NewestVerificationBatchResponsePageSpec extends SpecBase { "must serialise to the expected JSON key" in { val model = GetNewestVerificationBatchResponse( - scheme = Nil, + scheme = None, subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil, - submission = Nil, - monthlyReturn = Nil + submission = None, + monthlyReturn = None ) val uaWithValue = diff --git a/test/pages/verify/CurrentVerificationBatchResponsePageSpec.scala b/test/pages/verify/CurrentVerificationBatchResponsePageSpec.scala index 33aeeef7..892665dc 100644 --- a/test/pages/verify/CurrentVerificationBatchResponsePageSpec.scala +++ b/test/pages/verify/CurrentVerificationBatchResponsePageSpec.scala @@ -30,7 +30,7 @@ class CurrentVerificationBatchResponsePageSpec extends SpecBase { "must be able to set and get a value" in { val model = GetCurrentVerificationBatchResponse( subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil ) @@ -46,7 +46,7 @@ class CurrentVerificationBatchResponsePageSpec extends SpecBase { "must be able to remove the value" in { val model = GetCurrentVerificationBatchResponse( subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil ) @@ -68,7 +68,7 @@ class CurrentVerificationBatchResponsePageSpec extends SpecBase { "must serialise to the expected JSON key" in { val model = GetCurrentVerificationBatchResponse( subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil ) diff --git a/test/services/VerificationServiceSpec.scala b/test/services/VerificationServiceSpec.scala index b24a0a2b..5c2b139f 100644 --- a/test/services/VerificationServiceSpec.scala +++ b/test/services/VerificationServiceSpec.scala @@ -62,16 +62,16 @@ final class VerificationServiceSpec extends SpecBase with MockitoSugar with Mode private val responseWithSubcontractors = GetNewestVerificationBatchResponse( - scheme = Nil, + scheme = None, subcontractors = Seq( verifiedSubcontractor, unverifiedSub1, unverifiedSub2 ), - verificationBatch = Nil, + verificationBatch = None, verifications = Nil, - submission = Nil, - monthlyReturn = Nil + submission = None, + monthlyReturn = None ) "VerificationService.refreshNewestVerificationBatch" - { @@ -198,7 +198,7 @@ final class VerificationServiceSpec extends SpecBase with MockitoSugar with Mode val response = GetCurrentVerificationBatchResponse( subcontractors = Nil, - verificationBatch = Nil, + verificationBatch = None, verifications = Nil ) From d3d0160f2b8750707a2cfc121d8b11af13a3ea8e Mon Sep 17 00:00:00 2001 From: codeneto-hmrc <253322371+codeneto-hmrc@users.noreply.github.com> Date: Fri, 1 May 2026 10:29:27 +0100 Subject: [PATCH 09/48] DTR-4160: move files from verification folder to verify --- .../verify/ContractorEmailConfirmationStoredController.scala | 3 +-- app/controllers/verify/EmailAddressController.scala | 3 +-- .../NewestVerificationBatchController.scala | 2 +- .../NewestVerificationBatchResponsePage.scala | 2 +- app/services/VerificationService.scala | 3 +-- conf/app.routes | 2 +- .../ContractorEmailConfirmationStoredControllerSpec.scala | 3 +-- test/controllers/verify/EmailAddressControllerSpec.scala | 3 +-- .../NewestVerificationBatchControllerSpec.scala | 4 ++-- .../NewestVerificationBatchResponsePageSpec.scala | 3 ++- test/services/VerificationServiceSpec.scala | 3 +-- 11 files changed, 13 insertions(+), 18 deletions(-) rename app/controllers/{verification => verify}/NewestVerificationBatchController.scala (98%) rename app/pages/{verification => verify}/NewestVerificationBatchResponsePage.scala (97%) rename test/controllers/{verification => verify}/NewestVerificationBatchControllerSpec.scala (97%) rename test/pages/{verification => verify}/NewestVerificationBatchResponsePageSpec.scala (97%) diff --git a/app/controllers/verify/ContractorEmailConfirmationStoredController.scala b/app/controllers/verify/ContractorEmailConfirmationStoredController.scala index 45fa9e63..cdf97670 100644 --- a/app/controllers/verify/ContractorEmailConfirmationStoredController.scala +++ b/app/controllers/verify/ContractorEmailConfirmationStoredController.scala @@ -21,8 +21,7 @@ import forms.verify.ContractorEmailConfirmationStoredFormProvider import models.Mode import models.requests.DataRequest import navigation.Navigator -import pages.verification.NewestVerificationBatchResponsePage -import pages.verify.ContractorEmailConfirmationStoredPage +import pages.verify.{ContractorEmailConfirmationStoredPage, NewestVerificationBatchResponsePage} import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository diff --git a/app/controllers/verify/EmailAddressController.scala b/app/controllers/verify/EmailAddressController.scala index 0e38e2cd..8393506a 100644 --- a/app/controllers/verify/EmailAddressController.scala +++ b/app/controllers/verify/EmailAddressController.scala @@ -20,13 +20,12 @@ import controllers.actions.* import forms.verify.EmailAddressFormProvider import models.Mode import navigation.Navigator -import pages.verify.EmailAddressPage +import pages.verify.{EmailAddressPage, NewestVerificationBatchResponsePage} import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import views.html.verify.EmailAddressView -import pages.verification.NewestVerificationBatchResponsePage import models.UserAnswers import javax.inject.Inject diff --git a/app/controllers/verification/NewestVerificationBatchController.scala b/app/controllers/verify/NewestVerificationBatchController.scala similarity index 98% rename from app/controllers/verification/NewestVerificationBatchController.scala rename to app/controllers/verify/NewestVerificationBatchController.scala index aab45bdd..cfac7413 100644 --- a/app/controllers/verification/NewestVerificationBatchController.scala +++ b/app/controllers/verify/NewestVerificationBatchController.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package controllers.verification +package controllers.verify import controllers.actions.{DataRequiredAction, DataRetrievalAction, IdentifierAction} import play.api.Logging diff --git a/app/pages/verification/NewestVerificationBatchResponsePage.scala b/app/pages/verify/NewestVerificationBatchResponsePage.scala similarity index 97% rename from app/pages/verification/NewestVerificationBatchResponsePage.scala rename to app/pages/verify/NewestVerificationBatchResponsePage.scala index aa013aed..b84b1885 100644 --- a/app/pages/verification/NewestVerificationBatchResponsePage.scala +++ b/app/pages/verify/NewestVerificationBatchResponsePage.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package pages.verification +package pages.verify import models.response.GetNewestVerificationBatchResponse import pages.QuestionPage diff --git a/app/services/VerificationService.scala b/app/services/VerificationService.scala index d30701e4..db7f8c9e 100644 --- a/app/services/VerificationService.scala +++ b/app/services/VerificationService.scala @@ -18,8 +18,7 @@ package services import connectors.ConstructionIndustrySchemeConnector import models.{Subcontractor, UserAnswers} -import pages.verification.NewestVerificationBatchResponsePage -import pages.verify.{CurrentVerificationBatchResponsePage, UnverifiedSubcontractorsPage} +import pages.verify.{CurrentVerificationBatchResponsePage, NewestVerificationBatchResponsePage, UnverifiedSubcontractorsPage} import queries.CisIdQuery import repositories.SessionRepository import uk.gov.hmrc.http.HeaderCarrier diff --git a/conf/app.routes b/conf/app.routes index 02a4455d..5e9e5ffa 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -362,7 +362,7 @@ GET /verify/submitting-verification-request controllers.verify.Submis GET /verify/no-subcontractors-added controllers.verify.NoSubcontractorsAddedController.onPageLoad() -GET /verification/newest controllers.verification.NewestVerificationBatchController.onPageLoad() +GET /verify/newest controllers.verify.NewestVerificationBatchController.onPageLoad() GET /verify/current controllers.verify.CurrentVerificationBatchController.onPageLoad() diff --git a/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala b/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala index c56c22b8..86f16f35 100644 --- a/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala +++ b/test/controllers/verify/ContractorEmailConfirmationStoredControllerSpec.scala @@ -26,8 +26,7 @@ import navigation.{FakeNavigator, Navigator} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.when import org.scalatestplus.mockito.MockitoSugar -import pages.verification.NewestVerificationBatchResponsePage -import pages.verify.ContractorEmailConfirmationStoredPage +import pages.verify.{ContractorEmailConfirmationStoredPage, NewestVerificationBatchResponsePage} import play.api.inject.bind import play.api.mvc.Call import play.api.test.FakeRequest diff --git a/test/controllers/verify/EmailAddressControllerSpec.scala b/test/controllers/verify/EmailAddressControllerSpec.scala index 9b80a3ca..da5eddde 100644 --- a/test/controllers/verify/EmailAddressControllerSpec.scala +++ b/test/controllers/verify/EmailAddressControllerSpec.scala @@ -23,12 +23,11 @@ import navigation.Navigator import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.when import org.scalatestplus.mockito.MockitoSugar -import pages.verification.NewestVerificationBatchResponsePage import play.api.inject.bind import play.api.mvc.Call import play.api.test.FakeRequest import models.response.GetNewestVerificationBatchResponse -import pages.verify.EmailAddressPage +import pages.verify.{EmailAddressPage, NewestVerificationBatchResponsePage} import play.api.test.Helpers.* import repositories.SessionRepository diff --git a/test/controllers/verification/NewestVerificationBatchControllerSpec.scala b/test/controllers/verify/NewestVerificationBatchControllerSpec.scala similarity index 97% rename from test/controllers/verification/NewestVerificationBatchControllerSpec.scala rename to test/controllers/verify/NewestVerificationBatchControllerSpec.scala index 41954401..431373ce 100644 --- a/test/controllers/verification/NewestVerificationBatchControllerSpec.scala +++ b/test/controllers/verify/NewestVerificationBatchControllerSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package controllers.verification +package controllers.verify import base.SpecBase import controllers.routes @@ -33,7 +33,7 @@ import scala.concurrent.Future class NewestVerificationBatchControllerSpec extends SpecBase with MockitoSugar { - private val endpointUrl = "/verification/newest" + private val endpointUrl = "/verify/newest" "NewestVerificationBatchController" - { diff --git a/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala b/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala similarity index 97% rename from test/pages/verification/NewestVerificationBatchResponsePageSpec.scala rename to test/pages/verify/NewestVerificationBatchResponsePageSpec.scala index caf55945..9c74e216 100644 --- a/test/pages/verification/NewestVerificationBatchResponsePageSpec.scala +++ b/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala @@ -14,10 +14,11 @@ * limitations under the License. */ -package pages.verification +package pages.verify import base.SpecBase import models.response.GetNewestVerificationBatchResponse +import pages.verify.NewestVerificationBatchResponsePage import play.api.libs.json.Json class NewestVerificationBatchResponsePageSpec extends SpecBase { diff --git a/test/services/VerificationServiceSpec.scala b/test/services/VerificationServiceSpec.scala index 5c2b139f..47287d78 100644 --- a/test/services/VerificationServiceSpec.scala +++ b/test/services/VerificationServiceSpec.scala @@ -26,8 +26,7 @@ import org.mockito.Mockito.{never, verify, verifyNoMoreInteractions, when} import org.scalatest.RecoverMethods.recoverToExceptionIf import org.scalatestplus.mockito.MockitoSugar import pages.QuestionPage -import pages.verification.NewestVerificationBatchResponsePage -import pages.verify.{CurrentVerificationBatchResponsePage, UnverifiedSubcontractorsPage} +import pages.verify.{CurrentVerificationBatchResponsePage, NewestVerificationBatchResponsePage, UnverifiedSubcontractorsPage} import play.api.libs.json.{JsPath, Writes} import queries.CisIdQuery import repositories.SessionRepository From 5c8de2e221dafd0e92bbadc1e112c67d5c6b1d1f Mon Sep 17 00:00:00 2001 From: codeneto-hmrc <253322371+codeneto-hmrc@users.noreply.github.com> Date: Fri, 1 May 2026 10:43:13 +0100 Subject: [PATCH 10/48] DTR-4160: fix integration test --- .../ConstructionIndustrySchemeConnectorSpec.scala | 8 +++----- .../verify/NewestVerificationBatchResponsePageSpec.scala | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala b/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala index 599a9ead..969edd61 100644 --- a/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala +++ b/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala @@ -276,11 +276,9 @@ class ConstructionIndustrySchemeConnectorSpec | "subcontractorId": 1 | } | ], - | "verificationBatch": [ - | { + | "verificationBatch": { | "verificationBatchId": 99 - | } - | ], + | }, | "verifications": [ | { | "verificationId": 1001 @@ -300,7 +298,7 @@ class ConstructionIndustrySchemeConnectorSpec val result = connector.getCurrentVerificationBatch(instanceId).futureValue result.subcontractors.map(_.subcontractorId) mustBe Seq(1L) - result.verificationBatch.map(_.verificationBatchId) mustBe Seq(99L) + result.verificationBatch.map(_.verificationBatchId) mustBe Some(99L) result.verifications.map(_.verificationId) mustBe Seq(1001L) } diff --git a/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala b/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala index 9c74e216..19a689af 100644 --- a/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala +++ b/test/pages/verify/NewestVerificationBatchResponsePageSpec.scala @@ -18,7 +18,6 @@ package pages.verify import base.SpecBase import models.response.GetNewestVerificationBatchResponse -import pages.verify.NewestVerificationBatchResponsePage import play.api.libs.json.Json class NewestVerificationBatchResponsePageSpec extends SpecBase { From bf8650324b53c4175a0046af753cf87764877f6f Mon Sep 17 00:00:00 2001 From: Richy Jassal <20478717+jassalrichy@users.noreply.github.com> Date: Tue, 5 May 2026 09:24:59 +0100 Subject: [PATCH 11/48] [DTR-000-FIX] - fix intermittently failing test for VerificationServiceSpec --- test-utils/generators/ModelGenerators.scala | 42 ++++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/test-utils/generators/ModelGenerators.scala b/test-utils/generators/ModelGenerators.scala index 0531ef9b..a0a77a5e 100644 --- a/test-utils/generators/ModelGenerators.scala +++ b/test-utils/generators/ModelGenerators.scala @@ -27,23 +27,29 @@ import java.time.{Instant, LocalDateTime, ZoneOffset} trait ModelGenerators { + private val genNonEmptyAlphaStr: Gen[String] = + Gen.nonEmptyListOf(Gen.alphaChar).map(_.mkString) + + private val genNonEmptyAlphaNumStr: Gen[String] = + Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString) + implicit lazy val arbitrarySubcontractorViewModel: Arbitrary[SubcontractorViewModel] = Arbitrary { for { - id <- Gen.alphaStr.suchThat(_.nonEmpty) - name <- Gen.alphaStr.suchThat(_.nonEmpty) + id <- genNonEmptyAlphaStr + name <- genNonEmptyAlphaStr } yield SubcontractorViewModel(id, name) } implicit lazy val arbitraryInternationalAddress: Arbitrary[InternationalAddress] = Arbitrary { for { - addressLine1 <- Gen.alphaStr.suchThat(_.nonEmpty) + addressLine1 <- genNonEmptyAlphaStr addressLine2 <- Gen.option(Gen.alphaStr) - addressLine3 <- Gen.alphaStr.suchThat(_.nonEmpty) + addressLine3 <- genNonEmptyAlphaStr addressLine4 <- Gen.option(Gen.alphaStr) - postalCode <- Gen.alphaStr.suchThat(_.nonEmpty) - country <- Gen.alphaStr.suchThat(_.nonEmpty) + postalCode <- genNonEmptyAlphaStr + country <- genNonEmptyAlphaStr } yield InternationalAddress( addressLine1 = addressLine1, addressLine2 = addressLine2, @@ -84,23 +90,23 @@ trait ModelGenerators { Arbitrary { for { subcontractorId <- Gen.posNum[Long] - firstName <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - secondName <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - surname <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - tradingName <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - partnershipTradingName <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + firstName <- Gen.option(genNonEmptyAlphaStr) + secondName <- Gen.option(genNonEmptyAlphaStr) + surname <- Gen.option(genNonEmptyAlphaStr) + tradingName <- Gen.option(genNonEmptyAlphaStr) + partnershipTradingName <- Gen.option(genNonEmptyAlphaStr) verified <- Gen.option(Gen.oneOf("Y", "N")) - verificationNumber <- Gen.option(Gen.alphaNumStr.suchThat(_.nonEmpty)) - taxTreatment <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + verificationNumber <- Gen.option(genNonEmptyAlphaNumStr) + taxTreatment <- Gen.option(genNonEmptyAlphaStr) verificationDate <- Gen.option(genLocalDateTime) lastMonthlyReturnDate <- Gen.option(genLocalDateTime) createDate <- Gen.option(genLocalDateTime) - subcontractorType <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + subcontractorType <- Gen.option(genNonEmptyAlphaStr) subbieResourceRef <- Gen.option(Gen.posNum[Long]) - utr <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - partnerUtr <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - crn <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) - nino <- Gen.option(Gen.alphaStr.suchThat(_.nonEmpty)) + utr <- Gen.option(genNonEmptyAlphaStr) + partnerUtr <- Gen.option(genNonEmptyAlphaStr) + crn <- Gen.option(genNonEmptyAlphaStr) + nino <- Gen.option(genNonEmptyAlphaStr) } yield Subcontractor( subcontractorId = subcontractorId, firstName = firstName, From 4cd6e702a2d0aae81601639fd16bf936277baca7 Mon Sep 17 00:00:00 2001 From: Richy Jassal <20478717+jassalrichy@users.noreply.github.com> Date: Tue, 5 May 2026 09:31:54 +0100 Subject: [PATCH 12/48] [DTR-000-FIX] - scala formatting --- conf/app.routes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/app.routes b/conf/app.routes index 5e9e5ffa..a8d877dd 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -362,7 +362,7 @@ GET /verify/submitting-verification-request controllers.verify.Submis GET /verify/no-subcontractors-added controllers.verify.NoSubcontractorsAddedController.onPageLoad() -GET /verify/newest controllers.verify.NewestVerificationBatchController.onPageLoad() +GET /verify/newest controllers.verify.NewestVerificationBatchController.onPageLoad() GET /verify/current controllers.verify.CurrentVerificationBatchController.onPageLoad() From d34ada68ecec350f7c63a9692c55bc96b3f0b416 Mon Sep 17 00:00:00 2001 From: mounkumar pradhana <49392940+Mounkumar@users.noreply.github.com> Date: Tue, 5 May 2026 13:02:35 +0100 Subject: [PATCH 13/48] DTR-4806 business function F4 - conditional routing (#158) * DTR-4806 business function F4 - conditional routing * DTR-4806 missing test cases added for None case --- .../NewestVerificationBatchController.scala | 28 +++- ...ewestVerificationBatchControllerSpec.scala | 146 ++++++++++++++++-- 2 files changed, 158 insertions(+), 16 deletions(-) diff --git a/app/controllers/verify/NewestVerificationBatchController.scala b/app/controllers/verify/NewestVerificationBatchController.scala index cfac7413..6da48772 100644 --- a/app/controllers/verify/NewestVerificationBatchController.scala +++ b/app/controllers/verify/NewestVerificationBatchController.scala @@ -17,6 +17,9 @@ package controllers.verify import controllers.actions.{DataRequiredAction, DataRetrievalAction, IdentifierAction} +import models.NormalMode +import pages.verify.NewestVerificationBatchResponsePage +import pages.verify.UnverifiedSubcontractorsPage import play.api.Logging import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} @@ -42,11 +45,30 @@ class NewestVerificationBatchController @Inject() ( (identify andThen getData andThen requireData).async { implicit request => verificationBatchService .refreshNewestVerificationBatch(request.userAnswers) - .map { _ => - Ok("newest verification batch saved to session") + .map { updatedAnswers => + + val batch = updatedAnswers.get(NewestVerificationBatchResponsePage) + val unverified = updatedAnswers.get(UnverifiedSubcontractorsPage).getOrElse(Seq.empty) + + batch match { + case Some(response) if response.subcontractors.isEmpty => + Redirect(controllers.verify.routes.NoSubcontractorsAddedController.onPageLoad()) + + case Some(response) if response.subcontractors.nonEmpty && unverified.isEmpty => + Redirect(controllers.verify.routes.VerifyYourSubcontractorsYesNoController.onPageLoad) + + case Some(_) => + Redirect(controllers.verify.routes.SelectSubcontractorController.onPageLoad(NormalMode)) + + case None => + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + } } .recover { case t => - logger.error("[NewestVerificationBatchController.onPageLoad] Failed to refresh newest verification batch", t) + logger.error( + "[NewestVerificationBatchController.onPageLoad] Failed to refresh newest verification batch", + t + ) Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) } } diff --git a/test/controllers/verify/NewestVerificationBatchControllerSpec.scala b/test/controllers/verify/NewestVerificationBatchControllerSpec.scala index 431373ce..e1646a20 100644 --- a/test/controllers/verify/NewestVerificationBatchControllerSpec.scala +++ b/test/controllers/verify/NewestVerificationBatchControllerSpec.scala @@ -18,30 +18,62 @@ package controllers.verify import base.SpecBase import controllers.routes -import models.UserAnswers -import org.mockito.ArgumentCaptor +import models.response.GetNewestVerificationBatchResponse +import models.{NormalMode, Subcontractor, UserAnswers} +import generators.ModelGenerators import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.{never, verify, verifyNoMoreInteractions, when} import org.scalatestplus.mockito.MockitoSugar +import pages.verify.NewestVerificationBatchResponsePage +import pages.verify.UnverifiedSubcontractorsPage import play.api.inject.bind import play.api.test.FakeRequest import play.api.test.Helpers.* import services.VerificationService import uk.gov.hmrc.http.HeaderCarrier +import java.time.LocalDateTime import scala.concurrent.Future -class NewestVerificationBatchControllerSpec extends SpecBase with MockitoSugar { +class NewestVerificationBatchControllerSpec extends SpecBase with MockitoSugar with ModelGenerators { private val endpointUrl = "/verify/newest" + private val verifiedSubcontractor: Subcontractor = + arbitrarySubcontractor.arbitrary.sample.value.copy( + subcontractorId = 1L, + verified = Some("Y") + ) + + private val unverifiedSubcontractor: Subcontractor = + arbitrarySubcontractor.arbitrary.sample.value.copy( + subcontractorId = 2L, + verified = Some("N") + ) + + private def newestBatchResponse(subcontractors: Seq[Subcontractor]) = + GetNewestVerificationBatchResponse( + scheme = None, + subcontractors = subcontractors, + verificationBatch = None, + verifications = Seq.empty, + submission = None, + monthlyReturn = None + ) + "NewestVerificationBatchController" - { - "must return OK with confirmation message when refreshNewestVerificationBatch succeeds" in { + "must redirect to NoSubcontractorsAdded when no subcontractors exist" in { val mockService = mock[VerificationService] + val updatedAnswers = + emptyUserAnswers + .set(NewestVerificationBatchResponsePage, newestBatchResponse(Seq.empty)) + .success + .value + when(mockService.refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier])) - .thenReturn(Future.successful(emptyUserAnswers)) + .thenReturn(Future.successful(updatedAnswers)) val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) @@ -49,18 +81,106 @@ class NewestVerificationBatchControllerSpec extends SpecBase with MockitoSugar { .build() running(application) { - val request = FakeRequest(GET, endpointUrl) - val result = route(application, request).value + val result = route(application, FakeRequest(GET, endpointUrl)).value + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe + controllers.verify.routes.NoSubcontractorsAddedController.onPageLoad().url + + verify(mockService).refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier]) + verifyNoMoreInteractions(mockService) + } + } + + "must redirect to VerifyYourSubcontractorsYesNo when all subcontractors are verified" in { + val mockService = mock[VerificationService] + + val updatedAnswers = + emptyUserAnswers + .set( + NewestVerificationBatchResponsePage, + newestBatchResponse(Seq(verifiedSubcontractor)) + ) + .flatMap(_.set(UnverifiedSubcontractorsPage, Seq.empty)) + .success + .value + + when(mockService.refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier])) + .thenReturn(Future.successful(updatedAnswers)) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[VerificationService].toInstance(mockService)) + .build() + + running(application) { + val result = route(application, FakeRequest(GET, endpointUrl)).value - status(result) mustEqual OK - contentAsString(result) mustBe "newest verification batch saved to session" + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe + controllers.verify.routes.VerifyYourSubcontractorsYesNoController.onPageLoad.url + + verify(mockService).refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier]) + verifyNoMoreInteractions(mockService) + } + } + + "must redirect to SelectSubcontractor when unverified subcontractors exist" in { + val mockService = mock[VerificationService] - val uaCaptor = ArgumentCaptor.forClass(classOf[UserAnswers]) - verify(mockService).refreshNewestVerificationBatch(uaCaptor.capture())(any[HeaderCarrier]) + val unverified = unverifiedSubcontractor - uaCaptor.getValue.id mustBe emptyUserAnswers.id - uaCaptor.getValue.data mustBe emptyUserAnswers.data + val updatedAnswers = + emptyUserAnswers + .set( + NewestVerificationBatchResponsePage, + newestBatchResponse(Seq(unverified)) + ) + .flatMap(_.set(UnverifiedSubcontractorsPage, Seq(unverified))) + .success + .value + when(mockService.refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier])) + .thenReturn(Future.successful(updatedAnswers)) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[VerificationService].toInstance(mockService)) + .build() + + running(application) { + val result = route(application, FakeRequest(GET, endpointUrl)).value + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe + controllers.verify.routes.SelectSubcontractorController + .onPageLoad(NormalMode) + .url + + verify(mockService).refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier]) + verifyNoMoreInteractions(mockService) + } + } + + "must redirect to JourneyRecovery when NewestVerificationBatchResponsePage is missing" in { + val mockService = mock[VerificationService] + + when(mockService.refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier])) + .thenReturn(Future.successful(emptyUserAnswers)) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[VerificationService].toInstance(mockService)) + .build() + + running(application) { + val result = route(application, FakeRequest(GET, endpointUrl)).value + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe + routes.JourneyRecoveryController.onPageLoad().url + + verify(mockService).refreshNewestVerificationBatch(any[UserAnswers])(any[HeaderCarrier]) verifyNoMoreInteractions(mockService) } } From 994fc0d07cdee113fcadf9a26b1f465f5089ee1c Mon Sep 17 00:00:00 2001 From: Edward Pau <232416446+edpau-hmrc@users.noreply.github.com> Date: Tue, 5 May 2026 13:32:22 +0100 Subject: [PATCH 14/48] DTR-4694Fix: update heading to Large (L) heading size (#162) --- app/views/verify/VerifyDepartmentalErrorView.scala.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/verify/VerifyDepartmentalErrorView.scala.html b/app/views/verify/VerifyDepartmentalErrorView.scala.html index e4fb848b..e3705078 100644 --- a/app/views/verify/VerifyDepartmentalErrorView.scala.html +++ b/app/views/verify/VerifyDepartmentalErrorView.scala.html @@ -30,7 +30,7 @@ showBackLink = false ) { - @header(messages("verify.verifyDepartmentalError.heading"), classes = "govuk-heading-xl") + @header(messages("verify.verifyDepartmentalError.heading")) @paragraph(messages("verify.verifyDepartmentalError.p1")) From 90ea15082886d18f65602e0ee96db7bc69c35912 Mon Sep 17 00:00:00 2001 From: Juely Kaikade <254691220+Juely-Kaikade-HMRC@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:29:35 +0100 Subject: [PATCH 15/48] DTR-4484 - Screen VF-03c Which subcontractors do you want to reverify? --- ...ctSubcontractorsToReverifyController.scala | 163 ++++++++ ...SubcontractorsToReverifyFormProvider.scala | 34 ++ .../SubcontractorsToReverifyViewModel.scala | 47 +++ .../SelectSubcontractorsToReverifyPage.scala | 27 ++ .../PaginationToReverifyService.scala | 110 +++++ ...electSubcontractorsToReverifySummary.scala | 56 +++ .../verify/SubcontractorReverifyData.scala | 95 +++++ .../verify/SubcontractorReverifyRow.scala | 27 ++ ...ectSubcontractorsToReverifyView.scala.html | 135 +++++++ conf/app.routes | 8 + ...bcontractorsToReverifyControllerSpec.scala | 375 ++++++++++++++++++ ...ontractorsToReverifyFormProviderSpec.scala | 55 +++ ...lectSubcontractorsToReverifyPageSpec.scala | 37 ++ .../PaginationToReverifyServiceSpec.scala | 134 +++++++ ...tSubcontractorsToReverifySummarySpec.scala | 100 +++++ ...lectSubcontractorsToReverifyViewSpec.scala | 170 ++++++++ 16 files changed, 1573 insertions(+) create mode 100644 app/controllers/verify/SelectSubcontractorsToReverifyController.scala create mode 100644 app/forms/verify/SelectSubcontractorsToReverifyFormProvider.scala create mode 100644 app/models/verify/SubcontractorsToReverifyViewModel.scala create mode 100644 app/pages/verify/SelectSubcontractorsToReverifyPage.scala create mode 100644 app/services/PaginationToReverifyService.scala create mode 100644 app/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummary.scala create mode 100644 app/viewmodels/verify/SubcontractorReverifyData.scala create mode 100644 app/viewmodels/verify/SubcontractorReverifyRow.scala create mode 100644 app/views/verify/SelectSubcontractorsToReverifyView.scala.html create mode 100644 test/controllers/verify/SelectSubcontractorsToReverifyControllerSpec.scala create mode 100644 test/forms/verify/SelectSubcontractorsToReverifyFormProviderSpec.scala create mode 100644 test/pages/verify/SelectSubcontractorsToReverifyPageSpec.scala create mode 100644 test/services/PaginationToReverifyServiceSpec.scala create mode 100644 test/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummarySpec.scala create mode 100644 test/views/verify/SelectSubcontractorsToReverifyViewSpec.scala diff --git a/app/controllers/verify/SelectSubcontractorsToReverifyController.scala b/app/controllers/verify/SelectSubcontractorsToReverifyController.scala new file mode 100644 index 00000000..bb67de36 --- /dev/null +++ b/app/controllers/verify/SelectSubcontractorsToReverifyController.scala @@ -0,0 +1,163 @@ +/* + * 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 controllers.verify + +import controllers.actions.* +import forms.verify.SelectSubcontractorsToReverifyFormProvider +import models.Mode +import navigation.Navigator +import pages.verify.SelectSubcontractorsToReverifyPage +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.verify.SelectSubcontractorsToReverifyView +import viewmodels.verify.SubcontractorReverifyData +import services.PaginationToReverifyService + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class SelectSubcontractorsToReverifyController @Inject() ( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: SelectSubcontractorsToReverifyFormProvider, + paginationToReverifyService: PaginationToReverifyService, + val controllerComponents: MessagesControllerComponents, + view: SelectSubcontractorsToReverifyView +)(implicit ec: ExecutionContext) + extends FrontendBaseController + with I18nSupport { + + val form = formProvider() + + private val allRows = SubcontractorReverifyData.rows + + def onPageLoad(mode: Mode, page: Int = 1): Action[AnyContent] = + (identify andThen getData andThen requireData) { implicit request => + + val result = + paginationToReverifyService.paginate( + allItems = allRows, + currentPage = page, + recordsPerPage = 6, + baseUrl = controllers.verify.routes.SelectSubcontractorsToReverifyController.onPageLoad(mode).url + ) + + val currentPageIds = result.items.map(_.id).toSet + + val preparedForm = + request.userAnswers + .get(SelectSubcontractorsToReverifyPage) + .map(_.intersect(currentPageIds)) + .map(form.fill) + .getOrElse(form) + + Ok( + view( + preparedForm, + mode, + result.items, + result.pagination, + page, + result.startIndex, + result.totalCount + ) + ) + } + + def onSubmit(mode: Mode, page: Int = 1): Action[AnyContent] = + (identify andThen getData andThen requireData).async { implicit request => + + val result = + paginationToReverifyService.paginate( + allItems = allRows, + currentPage = page, + recordsPerPage = 6, + baseUrl = controllers.verify.routes.SelectSubcontractorsToReverifyController.onPageLoad(mode).url + ) + + val boundForm = form.bindFromRequest() + + val currentSelections: Set[String] = + boundForm.value.getOrElse(Set.empty) + + val currentPageIds = + result.items.map(_.id).toSet + + val previousSelections: Set[String] = + request.userAnswers + .get(SelectSubcontractorsToReverifyPage) + .getOrElse(Set.empty[String]) + .diff(currentPageIds) + + val mergedSelections: Set[String] = + previousSelections ++ currentSelections + + val gotoPage: Option[Int] = + request.body.asFormUrlEncoded + .flatMap(_.get("gotoPage")) + .flatMap(_.headOption) + .flatMap(_.toIntOption) + + gotoPage match { + + // âś… Pagination click (NO validation) + case Some(targetPage) => + for { + updatedAnswers <- Future.fromTry( + request.userAnswers.set(SelectSubcontractorsToReverifyPage, mergedSelections) + ) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect( + routes.SelectSubcontractorsToReverifyController.onPageLoad(mode, targetPage) + ) + + // âś… Continue button (WITH validation) + case None => + boundForm.fold( + formWithErrors => + Future.successful( + BadRequest( + view( + formWithErrors, + mode, + result.items, + result.pagination, + page, + result.startIndex, + result.totalCount + ) + ) + ), + _ => + for { + updatedAnswers <- Future.fromTry( + request.userAnswers.set(SelectSubcontractorsToReverifyPage, mergedSelections) + ) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect( + navigator.nextPage(SelectSubcontractorsToReverifyPage, mode, updatedAnswers) + ) + ) + } + } +} diff --git a/app/forms/verify/SelectSubcontractorsToReverifyFormProvider.scala b/app/forms/verify/SelectSubcontractorsToReverifyFormProvider.scala new file mode 100644 index 00000000..73b2bf28 --- /dev/null +++ b/app/forms/verify/SelectSubcontractorsToReverifyFormProvider.scala @@ -0,0 +1,34 @@ +/* + * 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 forms.verify + +import forms.mappings.Mappings +import play.api.data.Form +import play.api.data.Forms.set + +import javax.inject.Inject + +class SelectSubcontractorsToReverifyFormProvider @Inject() extends Mappings { + + private val requiredKey = "verify.selectSubcontractorsToReverify.error.required" + + def apply(): Form[Set[String]] = + Form( + "value" -> set(text(requiredKey)) + .verifying(requiredKey, _.nonEmpty) + ) +} diff --git a/app/models/verify/SubcontractorsToReverifyViewModel.scala b/app/models/verify/SubcontractorsToReverifyViewModel.scala new file mode 100644 index 00000000..bebe9165 --- /dev/null +++ b/app/models/verify/SubcontractorsToReverifyViewModel.scala @@ -0,0 +1,47 @@ +/* + * 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 models.verify + +import uk.gov.hmrc.govukfrontend.views.viewmodels.checkboxes.CheckboxItem +import uk.gov.hmrc.govukfrontend.views.viewmodels.content.Text +import viewmodels.govuk.checkbox.CheckboxItemViewModel +import viewmodels.verify.SubcontractorReverifyRow + +object SubcontractorsToReverifyViewModel extends Enumeration { + + def checkboxItems( + rows: Seq[SubcontractorReverifyRow], + selected: Set[String] + ): Seq[CheckboxItem] = + rows.zipWithIndex.map { case (row, index) => + CheckboxItemViewModel( + content = Text(row.name), + fieldId = s"value-$index", + index = index, + value = row.id + ) + } + + def extractSelected(formData: Map[String, String]): Set[String] = + formData + .get("value") + .toSeq + .flatMap(_.split(",")) + .map(_.trim) + .filter(_.nonEmpty) + .toSet +} diff --git a/app/pages/verify/SelectSubcontractorsToReverifyPage.scala b/app/pages/verify/SelectSubcontractorsToReverifyPage.scala new file mode 100644 index 00000000..97552221 --- /dev/null +++ b/app/pages/verify/SelectSubcontractorsToReverifyPage.scala @@ -0,0 +1,27 @@ +/* + * 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 pages.verify + +import pages.QuestionPage +import play.api.libs.json.JsPath + +case object SelectSubcontractorsToReverifyPage extends QuestionPage[Set[String]] with VerifyJourney { + + override def path: JsPath = JsPath \ toString + + override def toString: String = "selectSubcontractorsToReverify" +} diff --git a/app/services/PaginationToReverifyService.scala b/app/services/PaginationToReverifyService.scala new file mode 100644 index 00000000..fecc3bee --- /dev/null +++ b/app/services/PaginationToReverifyService.scala @@ -0,0 +1,110 @@ +/* + * 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 services + +import javax.inject.{Inject, Singleton} +import viewmodels.govuk.PaginationFluency._ + +@Singleton +class PaginationToReverifyService @Inject() () { + + private val defaultRecordsPerPage = 6 + private val maxVisiblePages = 6 + + case class PaginatedResult[T]( + items: Seq[T], + pagination: PaginationViewModel, + currentPage: Int, + totalPages: Int, + startIndex: Int, + totalCount: Int + ) + + def paginate[T]( + allItems: Seq[T], + currentPage: Int, + recordsPerPage: Int = defaultRecordsPerPage, + baseUrl: String, + pageParam: String = "page" + ): PaginatedResult[T] = { + + val totalCount = allItems.size + + val totalPages = + math.ceil(allItems.size.toDouble / recordsPerPage).toInt.max(1) + + val page = + currentPage.max(1).min(totalPages) + + val start = (page - 1) * recordsPerPage + val end = start + recordsPerPage + + val pageItems = allItems.slice(start, end) + + PaginatedResult( + items = pageItems, + pagination = buildPagination(page, totalPages, baseUrl, pageParam), + currentPage = page, + totalPages = totalPages, + startIndex = start + 1, + totalCount = totalCount + ) + } + + private def buildPagination( + page: Int, + totalPages: Int, + baseUrl: String, + pageParam: String + ): PaginationViewModel = + if (totalPages <= 1) { + PaginationViewModel() + } else { + + val half = maxVisiblePages / 2 + val from = (page - half).max(1) + val to = (from + maxVisiblePages - 1).min(totalPages) + + PaginationViewModel() + .withItems( + (from to to).map { p => + PaginationItemViewModel( + number = p.toString, + href = s"$baseUrl?$pageParam=$p" + ).withCurrent(p == page) + } + ) + .copy( + previous = + if (page > 1) + Some( + PaginationLinkViewModel( + href = s"$baseUrl?$pageParam=${page - 1}" + ).withText("site.pagination.previous") + ) + else None, + next = + if (page < totalPages) + Some( + PaginationLinkViewModel( + href = s"$baseUrl?$pageParam=${page + 1}" + ).withText("site.pagination.next") + ) + else None + ) + } +} diff --git a/app/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummary.scala b/app/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummary.scala new file mode 100644 index 00000000..88c90b53 --- /dev/null +++ b/app/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummary.scala @@ -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 viewmodels.checkAnswers.verify + +import models.{CheckMode, UserAnswers} +import pages.verify.SelectSubcontractorsToReverifyPage +import play.api.i18n.Messages +import play.twirl.api.HtmlFormat +import uk.gov.hmrc.govukfrontend.views.viewmodels.content.HtmlContent +import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.govuk.summarylist.* +import viewmodels.implicits.* + +object SelectSubcontractorsToReverifySummary { + + def row(answers: UserAnswers)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(SelectSubcontractorsToReverifyPage).map { answers => + + val value = ValueViewModel( + HtmlContent( + answers + .map { answer => + HtmlFormat.escape(messages(s"verify.selectSubcontractorsToReverify.$answer")).toString + } + .mkString(",
    ") + ) + ) + + SummaryListRowViewModel( + key = "verify.selectSubcontractorsToReverify.checkYourAnswersLabel", + value = value, + actions = Seq( + ActionItemViewModel( + "site.change", + controllers.verify.routes.SelectSubcontractorsToReverifyController.onPageLoad(CheckMode).url + ) + .withVisuallyHiddenText(messages("verify.selectSubcontractorsToReverify.change.hidden")) + .withAttribute("id" -> "select-subcontractors-to-reverify") + ) + ) + } +} diff --git a/app/viewmodels/verify/SubcontractorReverifyData.scala b/app/viewmodels/verify/SubcontractorReverifyData.scala new file mode 100644 index 00000000..4828aa7d --- /dev/null +++ b/app/viewmodels/verify/SubcontractorReverifyData.scala @@ -0,0 +1,95 @@ +/* + * 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 viewmodels.verify + +object SubcontractorReverifyData { + + val rows: Seq[SubcontractorReverifyRow] = Seq( + SubcontractorReverifyRow( + id = "Grantalan", + name = "Grant, Alan", + utr = "0991272528", + verified = "No", + verificationNumber = "V0001256246", + taxTreatment = "Standard rate", + dateAdded = "11 May 2020" + ), + SubcontractorReverifyRow( + id = "Hammondhouse", + name = "Hammond House", + utr = "2904743750", + verified = "Yes", + verificationNumber = "V0001217702", + taxTreatment = "Gross", + dateAdded = "1 Oct 2025" + ), + SubcontractorReverifyRow( + id = "Ingenresearch", + name = "InGen Research", + utr = "9347488729", + verified = "No", + verificationNumber = "V0005617876", + taxTreatment = "Standard rate", + dateAdded = "1 Mar 2020" + ), + SubcontractorReverifyRow( + id = "Malcolmandsattler", + name = "Malcolm And Sattler", + utr = "0074742762", + verified = "Yes", + verificationNumber = "V0004635231", + taxTreatment = "Higher rate", + dateAdded = "1 Oct 2025" + ), + SubcontractorReverifyRow( + id = "brightwellPartners", + name = "Brightwell Partners", + utr = "1234567890", + verified = "No", + verificationNumber = "V0007771001", + taxTreatment = "Standard rate", + dateAdded = "23 Apr 2026" + ), + SubcontractorReverifyRow( + id = "carterfieldsLtd", + name = "Carterfields Ltd", + utr = "2345678901", + verified = "Yes", + verificationNumber = "V0007771002", + taxTreatment = "Gross", + dateAdded = "23 Apr 2026" + ), + SubcontractorReverifyRow( + id = "northbridgeBuild", + name = "Northbridge Build", + utr = "3456789012", + verified = "No", + verificationNumber = "V0007771003", + taxTreatment = "Standard rate", + dateAdded = "23 Apr 2026" + ), + SubcontractorReverifyRow( + id = "oakthornServices", + name = "Oakthorn Services", + utr = "4567890123", + verified = "Yes", + verificationNumber = "V0007771004", + taxTreatment = "Higher rate", + dateAdded = "23 Apr 2026" + ) + ).sortBy(_.name.toLowerCase) +} diff --git a/app/viewmodels/verify/SubcontractorReverifyRow.scala b/app/viewmodels/verify/SubcontractorReverifyRow.scala new file mode 100644 index 00000000..c31c45e2 --- /dev/null +++ b/app/viewmodels/verify/SubcontractorReverifyRow.scala @@ -0,0 +1,27 @@ +/* + * 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 viewmodels.verify + +case class SubcontractorReverifyRow( + id: String, + name: String, + utr: String, + verified: String, + verificationNumber: String, + taxTreatment: String, + dateAdded: String +) diff --git a/app/views/verify/SelectSubcontractorsToReverifyView.scala.html b/app/views/verify/SelectSubcontractorsToReverifyView.scala.html new file mode 100644 index 00000000..443117d2 --- /dev/null +++ b/app/views/verify/SelectSubcontractorsToReverifyView.scala.html @@ -0,0 +1,135 @@ +@* + * 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. + *@ + +@import viewmodels.LegendSize.Large +@import uk.gov.hmrc.govukfrontend.views.viewmodels.table._ +@import uk.gov.hmrc.govukfrontend.views.viewmodels.content._ +@import viewmodels.verify.SubcontractorReverifyRow +@import viewmodels.govuk.PaginationFluency._ +@import views.html.components._ + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukCheckboxes: GovukCheckboxes, + govukTable: GovukTable, + govukButton: GovukButton, + pageNavigator: components.PageNavigator +) + + +@(form: Form[Set[String]], mode: Mode, rows: Seq[SubcontractorReverifyRow], paginationViewModel: PaginationViewModel, page: Int, startIndex: Int, totalCount: Int)(implicit request: Request[_], messages: Messages) + +@layout(pageTitle = title(form, messages("verify.selectSubcontractorsToReverify.title"))) { + + @formHelper( + action = controllers.verify.routes.SelectSubcontractorsToReverifyController.onSubmit(mode, page), + Symbol("autoComplete") -> "off" + ) { + + @if(form.errors.nonEmpty) { + @govukErrorSummary( + ErrorSummaryViewModel(form, errorLinkOverrides = Map("value" -> "value-0")) + ) + } + +

    + @messages("verify.selectSubcontractorsToReverify.heading") +

    + +

    + @messages("verify.selectSubcontractorsToReverify.hint") +

    + + @if(paginationViewModel.items.nonEmpty) { +

    + @messages("verify.selectSubcontractor.showingResults", startIndex, startIndex + rows.size - 1, totalCount) +

    + } + + @defining( + form.value.getOrElse(Set.empty[String]) + ) { selected => + + @govukTable( + Table( + caption = None, + + head = Some(Seq( + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.include"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.name"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.utr"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.verified"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.verificationNumber"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.taxTreatment"))), + HeadCell(Text(messages("verify.selectSubcontractorsToReverify.dateAdded"))) + )), + + rows = rows.zipWithIndex.map { case (row, idx) => + val checkboxId = s"value-$idx" + val isChecked = selected.contains(row.id) + + Seq( + + TableRow( + content = HtmlContent( + s""" +
    +
    + + +
    +
    + """ + ) + ), + + TableRow(Text(row.name)), + TableRow(Text(row.utr)), + TableRow(Text(row.verified)), + TableRow(Text(row.verificationNumber)), + TableRow(Text(row.taxTreatment)), + TableRow(Text(row.dateAdded)) + ) + }, + + attributes = Map( + "id" -> "subcontractor-table", + "class" -> "govuk-table govuk-table--small-text-until-tablet" + ) + ) + ) + + @pageNavigator(paginationViewModel, page) + + @govukButton( + ButtonViewModel(messages("site.continue")) + ) + } + } +} + diff --git a/conf/app.routes b/conf/app.routes index a8d877dd..522587ba 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -373,6 +373,14 @@ POST /verify/confirmation-email-stored controllers.verify.Contra GET /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onPageLoad(mode: Mode = CheckMode) POST /verify/change-confirmation-email-stored controllers.verify.ContractorEmailConfirmationStoredController.onSubmit(mode: Mode = CheckMode) +GET /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onPageLoad +POST /verify/no-new-subcontractors-to-verify controllers.verify.VerifyYourSubcontractorsYesNoController.onSubmit + +GET /verify/select-subcontractors-to-reverify controllers.verify.SelectSubcontractorsToReverifyController.onPageLoad(mode: Mode = NormalMode, page: Int ?= 1) +POST /verify/select-subcontractors-to-reverify controllers.verify.SelectSubcontractorsToReverifyController.onSubmit(mode: Mode = NormalMode, page: Int ?= 1) +GET /verify/change-select-subcontractors-to-reverify controllers.verify.SelectSubcontractorsToReverifyController.onPageLoad(mode: Mode = CheckMode, page: Int ?= 1) +POST /verify/change-select-subcontractors-to-reverify controllers.verify.SelectSubcontractorsToReverifyController.onSubmit(mode: Mode = CheckMode, page: Int ?= 1) + GET /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = NormalMode, page: Int ?= 1) POST /verify/select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onSubmit(mode: Mode = NormalMode, page: Int ?= 1) GET /verify/change-select-subcontractors-to-verify controllers.verify.SelectSubcontractorController.onPageLoad(mode: Mode = CheckMode, page: Int ?= 1) diff --git a/test/controllers/verify/SelectSubcontractorsToReverifyControllerSpec.scala b/test/controllers/verify/SelectSubcontractorsToReverifyControllerSpec.scala new file mode 100644 index 00000000..467b4824 --- /dev/null +++ b/test/controllers/verify/SelectSubcontractorsToReverifyControllerSpec.scala @@ -0,0 +1,375 @@ +/* + * 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 controllers.verify + +import base.SpecBase +import controllers.routes +import forms.verify.SelectSubcontractorsToReverifyFormProvider +import models.{NormalMode, UserAnswers} +import navigation.{FakeNavigator, Navigator} +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when +import org.mockito.Mockito.verify +import org.scalatestplus.mockito.MockitoSugar +import pages.verify.SelectSubcontractorsToReverifyPage +import play.api.inject.bind +import play.api.mvc.Call +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import repositories.SessionRepository +import services.PaginationToReverifyService +import viewmodels.verify.SubcontractorReverifyData +import views.html.verify.SelectSubcontractorsToReverifyView + +import scala.concurrent.Future + +class SelectSubcontractorsToReverifyControllerSpec extends SpecBase with MockitoSugar { + + def onwardRoute = Call("GET", "/foo") + + lazy val selectSubcontractorsToReverifyRoute = + controllers.verify.routes.SelectSubcontractorsToReverifyController + .onPageLoad(NormalMode) + .url + + val formProvider = new SelectSubcontractorsToReverifyFormProvider() + val form = formProvider() + + val paginationService = new PaginationToReverifyService() + + private val allRows = SubcontractorReverifyData.rows + + private val firstRow = allRows.head + private val secondRow = allRows(1) + + def url(page: Int = 1): String = + controllers.verify.routes.SelectSubcontractorsToReverifyController + .onPageLoad(NormalMode, page) + .url + + "SubcontractorsToReverifyViewModel Controller" - { + + "must return OK and the correct view for a GET page 1" in { + + val ua = + emptyUserAnswers + .setOrException(SelectSubcontractorsToReverifyPage, Set("brightwellPartners", "carterfieldsLtd")) + + val application = applicationBuilder(userAnswers = Some(ua)).build() + + running(application) { + + val request = FakeRequest( + GET, + controllers.verify.routes.SelectSubcontractorsToReverifyController.onPageLoad(NormalMode, 1).url + ) + + val result = route(application, request).value + + status(result) mustBe OK + + val body = contentAsString(result) + + body must include("Which subcontractors do you want to reverify?") + body must include("Select the existing subcontractors you want to include in this verification request") + body must include("""id="subcontractor-table"""") + body must include("Showing 1 to 6 of 8 results") + + def inputSnippet(id: String): String = { + val afterId = body.split(s"""id="$id"""", 2)(1) + afterId.take(250) + } + + val v0 = inputSnippet("value-0") + v0 must include("""value="brightwellPartners"""") + v0 must include("checked") + + val v1 = inputSnippet("value-1") + v1 must include("""value="carterfieldsLtd"""") + v1 must include("checked") + + val v2 = inputSnippet("value-2") + v2 must include("""value="Grantalan"""") + v2 must not include "checked" + } + } + + "must populate the view correctly on a GET when the question has previously been answered" in { + + val userAnswers = + UserAnswers(userAnswersId) + .set(SelectSubcontractorsToReverifyPage, Set(firstRow.id, secondRow.id)) + .success + .value + + val application = + applicationBuilder(userAnswers = Some(userAnswers)).build() + + running(application) { + + val request = FakeRequest(GET, url(1)) + val result = route(application, request).value + + val view = application.injector.instanceOf[SelectSubcontractorsToReverifyView] + + val paginated = + paginationService.paginate( + allItems = allRows, + currentPage = 1, + recordsPerPage = 6, + baseUrl = url(1) + ) + + status(result) mustEqual OK + + contentAsString(result) mustEqual view( + form.fill(Set(firstRow.id, secondRow.id)), + NormalMode, + paginated.items, + paginated.pagination, + 1, + paginated.startIndex, + paginated.totalCount + )(request, messages(application)).toString + } + } + + "must redirect to the next page when valid data is submitted" in { + + val mockSessionRepository = mock[SessionRepository] + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + + val request = + FakeRequest(POST, selectSubcontractorsToReverifyRoute) + .withFormUrlEncodedBody(("value[0]", firstRow.id)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url + } + } + + "must return a Bad Request and errors when invalid data is submitted" in { + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + running(application) { + + val request = + FakeRequest(POST, url(1)) + .withFormUrlEncodedBody("value" -> "") + + val boundForm = form.bind(Map("value" -> "")) + + val view = application.injector.instanceOf[SelectSubcontractorsToReverifyView] + + val paginated = + paginationService.paginate( + allItems = allRows, + currentPage = 1, + recordsPerPage = 6, + baseUrl = url(1) + ) + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual view( + boundForm, + NormalMode, + paginated.items, + paginated.pagination, + 1, + paginated.startIndex, + paginated.totalCount + )(request, messages(application)).toString + } + } + + "must redirect to Journey Recovery for a GET if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + running(application) { + + val request = FakeRequest(GET, url(1)) + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual + routes.JourneyRecoveryController.onPageLoad().url + } + } + + "must redirect to Journey Recovery for a POST if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + running(application) { + + val request = + FakeRequest(POST, url(1)) + .withFormUrlEncodedBody( + "value[0]" -> firstRow.id + ) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual + routes.JourneyRecoveryController.onPageLoad().url + } + } + + "must redirect to target page when gotoPage field is present" in { + + val mockSessionRepository = mock[SessionRepository] + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[SessionRepository].toInstance(mockSessionRepository)) + .build() + + running(application) { + + val request = + FakeRequest(POST, url(1)) + .withFormUrlEncodedBody( + "value[0]" -> firstRow.id, + "gotoPage" -> "2" + ) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual url(2) + } + } + + "must save selections when gotoPage is present" in { + + val mockSessionRepository = mock[SessionRepository] + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[SessionRepository].toInstance(mockSessionRepository)) + .build() + + running(application) { + + val request = + FakeRequest(POST, url(1)) + .withFormUrlEncodedBody( + "value[0]" -> firstRow.id, + "gotoPage" -> "2" + ) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + val captor = org.mockito.ArgumentCaptor.forClass(classOf[UserAnswers]) + verify(mockSessionRepository).set(captor.capture()) + + captor.getValue + .get(SelectSubcontractorsToReverifyPage) + .value must contain(firstRow.id) + } + } + + "must merge previous selections with current page selections" in { + + val mockSessionRepository = mock[SessionRepository] + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val existingAnswers = + UserAnswers(userAnswersId) + .set(SelectSubcontractorsToReverifyPage, Set(firstRow.id)) + .success + .value + + val application = + applicationBuilder(userAnswers = Some(existingAnswers)) + .overrides( + bind[SessionRepository].toInstance(mockSessionRepository), + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)) + ) + .build() + + running(application) { + + val request = + FakeRequest(POST, url(1)) + .withFormUrlEncodedBody( + "value[0]" -> secondRow.id + ) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url + + val captor = org.mockito.ArgumentCaptor.forClass(classOf[UserAnswers]) + verify(mockSessionRepository).set(captor.capture()) + + captor.getValue + .get(SelectSubcontractorsToReverifyPage) + .value mustEqual Set(secondRow.id) + } + } + + "must redirect to next page when Continue is submitted on page 2" in { + + val mockSessionRepository = mock[SessionRepository] + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[SessionRepository].toInstance(mockSessionRepository), + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)) + ) + .build() + + running(application) { + + val request = + FakeRequest(POST, url(2)) + .withFormUrlEncodedBody() + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + } + } + } +} diff --git a/test/forms/verify/SelectSubcontractorsToReverifyFormProviderSpec.scala b/test/forms/verify/SelectSubcontractorsToReverifyFormProviderSpec.scala new file mode 100644 index 00000000..183e7ff0 --- /dev/null +++ b/test/forms/verify/SelectSubcontractorsToReverifyFormProviderSpec.scala @@ -0,0 +1,55 @@ +/* + * 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 forms.verify + +import forms.behaviours.CheckboxFieldBehaviours + +class SelectSubcontractorsToReverifyFormProviderSpec extends CheckboxFieldBehaviours { + + val form = new SelectSubcontractorsToReverifyFormProvider()() + + ".value" - { + + val fieldName = "value" + val requiredKey = "verify.selectSubcontractorsToReverify.error.required" + + behave like mandatoryCheckboxField( + form, + fieldName, + requiredKey + ) + + "bind multiple selected values correctly" in { + + val data = Map( + "value[0]" -> "Grantalan", + "value[1]" -> "Hammondhouse" + ) + + val result = form.bind(data) + + result.value.value mustBe Set("Grantalan", "Hammondhouse") + } + + "reject empty submission" in { + + val result = form.bind(Map.empty[String, String]) + + result.errors.headOption.value.message mustBe requiredKey + } + } +} diff --git a/test/pages/verify/SelectSubcontractorsToReverifyPageSpec.scala b/test/pages/verify/SelectSubcontractorsToReverifyPageSpec.scala new file mode 100644 index 00000000..399501a8 --- /dev/null +++ b/test/pages/verify/SelectSubcontractorsToReverifyPageSpec.scala @@ -0,0 +1,37 @@ +/* + * 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 pages.verify + +import pages.behaviours.PageBehaviours + +class SelectSubcontractorsToReverifyPageSpec extends PageBehaviours { + + "SelectSubcontractorsToReverifyPage" - { + + beRetrievable[Set[String]]( + SelectSubcontractorsToReverifyPage + ) + + beSettable[Set[String]]( + SelectSubcontractorsToReverifyPage + ) + + beRemovable[Set[String]]( + SelectSubcontractorsToReverifyPage + ) + } +} diff --git a/test/services/PaginationToReverifyServiceSpec.scala b/test/services/PaginationToReverifyServiceSpec.scala new file mode 100644 index 00000000..1ff5e181 --- /dev/null +++ b/test/services/PaginationToReverifyServiceSpec.scala @@ -0,0 +1,134 @@ +/* + * 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 services + +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.matchers.must.Matchers + +class PaginationToReverifyServiceSpec extends AnyWordSpec with Matchers { + + private val service = new PaginationToReverifyService() + + private def items(n: Int): Seq[String] = + (1 to n).map(i => s"Item $i") + + private val baseUrl = "/test-url" + + "PaginationToReverifyService.paginate" should { + + "return empty result when no items exist" in { + val result = service.paginate(Seq.empty[String], 1, baseUrl = baseUrl) + + result.items mustBe empty + result.pagination.items mustBe empty + result.pagination.previous mustBe None + result.pagination.next mustBe None + result.totalCount mustBe 0 + } + + "return single page when items fit within page size" in { + val result = service.paginate(items(6), 1, baseUrl = baseUrl) + + result.items.length mustBe 6 + result.pagination.items mustBe empty + result.pagination.previous mustBe None + result.pagination.next mustBe None + result.totalPages mustBe 1 + } + + "paginate correctly when more than one page exists" in { + val result = service.paginate(items(7), 1, baseUrl = baseUrl) + + result.items.length mustBe 6 + result.items.head mustBe "Item 1" + + result.pagination.items.length mustBe 2 + result.pagination.next.isDefined mustBe true + result.pagination.previous mustBe None + } + + "return correct second page data" in { + val result = service.paginate(items(12), 2, baseUrl = baseUrl) + + result.items.length mustBe 6 + result.items.head mustBe "Item 7" + + result.pagination.previous.isDefined mustBe true + result.pagination.next mustBe None + } + + "clamp page to minimum when page is less than 1" in { + val result = service.paginate(items(10), 0, baseUrl = baseUrl) + + result.currentPage mustBe 1 + result.items.head mustBe "Item 1" + } + + "clamp page to maximum when page exceeds total pages" in { + val result = service.paginate(items(10), 99, baseUrl = baseUrl) + + result.currentPage mustBe 2 + result.items.head mustBe "Item 7" + } + + "mark current page correctly in pagination model" in { + val result = service.paginate(items(20), 2, baseUrl = baseUrl) + + val current = result.pagination.items.find(_.current) + current.isDefined mustBe true + current.get.number mustBe "2" + } + + "handle exact multiple of page size correctly" in { + val result = service.paginate(items(12), 2, baseUrl = baseUrl) + + result.items mustBe Seq("Item 7", "Item 8", "Item 9", "Item 10", "Item 11", "Item 12") + result.pagination.next mustBe None + result.pagination.previous.isDefined mustBe true + } + + "generate correct pagination links" in { + val result = service.paginate(items(20), 2, baseUrl = baseUrl) + + val links = result.pagination.items.map(_.href) + + links must contain("/test-url?page=1") + links must contain("/test-url?page=2") + links must contain("/test-url?page=3") + } + + "generate previous and next links correctly" in { + val result = service.paginate(items(20), 2, baseUrl = baseUrl) + + result.pagination.previous.get.href mustBe "/test-url?page=1" + result.pagination.next.get.href mustBe "/test-url?page=3" + } + + "calculate startIndex correctly" in { + val result = service.paginate(items(20), 2, baseUrl = baseUrl) + + result.startIndex mustBe 7 + } + + "respect custom recordsPerPage parameter" in { + val result = service.paginate(items(10), 1, recordsPerPage = 3, baseUrl = baseUrl) + + result.items.length mustBe 3 + result.totalPages mustBe 4 + } + } +} diff --git a/test/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummarySpec.scala b/test/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummarySpec.scala new file mode 100644 index 00000000..9fd56ac6 --- /dev/null +++ b/test/viewmodels/checkAnswers/verify/SelectSubcontractorsToReverifySummarySpec.scala @@ -0,0 +1,100 @@ +/* + * 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 viewmodels.checkAnswers.verify + +import base.SpecBase +import models.{CheckMode, UserAnswers} +import org.scalatest.OptionValues._ +import org.scalatest.matchers.must.Matchers +import pages.verify.SelectSubcontractorsToReverifyPage +import play.api.i18n.{Lang, Messages, MessagesImpl} +import play.api.test.Helpers.stubMessagesApi +import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist._ + +class SelectSubcontractorsToReverifySummarySpec extends SpecBase with Matchers { + + private val messagesApi = stubMessagesApi() + private implicit val messages: Messages = MessagesImpl(Lang.defaultLang, messagesApi) + + "SelectSubcontractorsToReverifySummary.row" - { + + "must return a summary row with multiple selected subcontractors" in { + + val answers: UserAnswers = + emptyUserAnswers + .set(SelectSubcontractorsToReverifyPage, Set("Grantalan", "Hammondhouse")) + .success + .value + + val result = SelectSubcontractorsToReverifySummary.row(answers) + + result mustBe defined + + val row = result.value + + row.key.content.asHtml.toString must include( + messages("verify.selectSubcontractorsToReverify.checkYourAnswersLabel") + ) + + val valueHtml = row.value.content.asHtml.toString + + valueHtml must include(messages("verify.selectSubcontractorsToReverify.Grantalan")) + valueHtml must include(messages("verify.selectSubcontractorsToReverify.Hammondhouse")) + valueHtml must include("
    ") + + row.actions mustBe defined + + val action = row.actions.value.items.head + + action.href mustBe controllers.verify.routes.SelectSubcontractorsToReverifyController + .onPageLoad(CheckMode) + .url + + action.content.asHtml.toString must include(messages("site.change")) + + action.visuallyHiddenText mustBe Some( + messages("verify.selectSubcontractorsToReverify.change.hidden") + ) + + action.attributes must contain( + "id" -> "select-subcontractors-to-reverify" + ) + } + + "must return a summary row with a single selected subcontractor" in { + + val answers: UserAnswers = + emptyUserAnswers + .set(SelectSubcontractorsToReverifyPage, Set("Ingenresearch")) + .success + .value + + val result = SelectSubcontractorsToReverifySummary.row(answers) + + result mustBe defined + + val valueHtml = result.value.value.content.asHtml.toString + + valueHtml must include(messages("verify.selectSubcontractorsToReverify.Ingenresearch")) + valueHtml must not include "
    " + } + + "must return None when no subcontractors are selected" in { + SelectSubcontractorsToReverifySummary.row(emptyUserAnswers) mustBe None + } + } +} diff --git a/test/views/verify/SelectSubcontractorsToReverifyViewSpec.scala b/test/views/verify/SelectSubcontractorsToReverifyViewSpec.scala new file mode 100644 index 00000000..121944a6 --- /dev/null +++ b/test/views/verify/SelectSubcontractorsToReverifyViewSpec.scala @@ -0,0 +1,170 @@ +/* + * 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 views.verify + +import base.SpecBase +import models.{Mode, NormalMode} +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.scalatest.matchers.must.Matchers +import forms.verify.SelectSubcontractorsToReverifyFormProvider +import play.api.Application +import play.api.i18n.{Lang, Messages, MessagesApi, MessagesImpl} +import play.api.test.FakeRequest +import play.twirl.api.HtmlFormat +import viewmodels.govuk.PaginationFluency._ +import viewmodels.verify.{SubcontractorReverifyData, SubcontractorReverifyRow} +import views.html.verify.SelectSubcontractorsToReverifyView + +class SelectSubcontractorsToReverifyViewSpec extends SpecBase with Matchers { + + "SelectSubcontractorsToReverifyView" - { + + "must render heading, hint, table, pagination and continue button" in new Setup { + + val html: HtmlFormat.Appendable = + view( + form, + mode, + rows, + pagination, + page = 1, + startIndex = 1, + totalCount = rows.size + ) + + val doc: Document = Jsoup.parse(html.body) + + doc.title must include(messages("verify.selectSubcontractorsToReverify.title")) + + doc.select("h1").text mustBe messages("verify.selectSubcontractorsToReverify.heading") + + doc.select(".govuk-body").text must include( + messages("verify.selectSubcontractorsToReverify.hint") + ) + + doc.select("#subcontractor-table tbody tr").size() must be > 0 + + doc.select("input[type=checkbox]").size() mustBe rows.size + + doc.select("button").text must include(messages("site.continue")) + } + + "must render showing results text when pagination exists" in new Setup { + + val html = view( + form, + mode, + rows, + pagination, + page = 1, + startIndex = 1, + totalCount = rows.size + ) + + val doc = Jsoup.parse(html.body) + + doc.text must include( + s"1 to ${rows.size} of ${rows.size}" + ) + } + + "must NOT render showing results text when pagination is empty" in new Setup { + + val html = view( + form, + mode, + rows, + PaginationViewModel(), + page = 1, + startIndex = 1, + totalCount = rows.size + ) + + val doc = Jsoup.parse(html.body) + + doc.select("p.govuk-body").text() must not include "of" + } + + "must render pagination when items exist" in new Setup { + + val paginationWithItems = + PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "").withCurrent(true), + PaginationItemViewModel("2", "") + ), + next = Some(PaginationLinkViewModel("").withText("site.pagination.next")) + ) + + val html = view(form, mode, rows, paginationWithItems, 1, 1, rows.size) + + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination").size() mustBe 1 + } + + "must render error summary when form has errors" in new Setup { + + val formWithError = form.bind(Map("value" -> "")) + + val html = view( + formWithError, + mode, + rows, + pagination, + 1, + 1, + rows.size + ) + + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-error-summary").size() mustBe 1 + } + } + + trait Setup { + + val app: Application = applicationBuilder().build() + + implicit val request: FakeRequest[_] = + FakeRequest() + + implicit val messages: Messages = + MessagesImpl(Lang.defaultLang, app.injector.instanceOf[MessagesApi]) + + val view: SelectSubcontractorsToReverifyView = + app.injector.instanceOf[SelectSubcontractorsToReverifyView] + + val form = new SelectSubcontractorsToReverifyFormProvider()() + + val rows: Seq[SubcontractorReverifyRow] = + SubcontractorReverifyData.rows.take(6) + + val pagination: PaginationViewModel = + PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "").withCurrent(true), + PaginationItemViewModel("2", "") + ), + next = Some(PaginationLinkViewModel("").withText("site.pagination.next")) + ) + + val mode: Mode = NormalMode + } +} From 1bae3938e1714a1ec761f1b3352f4277096b6c90 Mon Sep 17 00:00:00 2001 From: Juely Kaikade <254691220+Juely-Kaikade-HMRC@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:51:53 +0100 Subject: [PATCH 16/48] DTR-4484 - updated controller and view to save the id & name --- ...ctSubcontractorsToReverifyController.scala | 8 +- ...ectSubcontractorsToReverifyView.scala.html | 5 +- ...bcontractorsToReverifyControllerSpec.scala | 20 ++- ...ubcontractorsToReverifyViewModelSpec.scala | 114 ++++++++++++++++++ 4 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 test/models/verify/SelectSubcontractorsToReverifyViewModelSpec.scala diff --git a/app/controllers/verify/SelectSubcontractorsToReverifyController.scala b/app/controllers/verify/SelectSubcontractorsToReverifyController.scala index bb67de36..df55085c 100644 --- a/app/controllers/verify/SelectSubcontractorsToReverifyController.scala +++ b/app/controllers/verify/SelectSubcontractorsToReverifyController.scala @@ -67,7 +67,7 @@ class SelectSubcontractorsToReverifyController @Inject() ( val preparedForm = request.userAnswers .get(SelectSubcontractorsToReverifyPage) - .map(_.intersect(currentPageIds)) + .map(_.filter(v => currentPageIds.contains(v.split("\\|")(0)))) .map(form.fill) .getOrElse(form) @@ -107,7 +107,7 @@ class SelectSubcontractorsToReverifyController @Inject() ( request.userAnswers .get(SelectSubcontractorsToReverifyPage) .getOrElse(Set.empty[String]) - .diff(currentPageIds) + .filterNot(v => currentPageIds.contains(v.split("\\|").headOption.getOrElse(""))) val mergedSelections: Set[String] = previousSelections ++ currentSelections @@ -120,7 +120,6 @@ class SelectSubcontractorsToReverifyController @Inject() ( gotoPage match { - // âś… Pagination click (NO validation) case Some(targetPage) => for { updatedAnswers <- Future.fromTry( @@ -131,8 +130,7 @@ class SelectSubcontractorsToReverifyController @Inject() ( routes.SelectSubcontractorsToReverifyController.onPageLoad(mode, targetPage) ) - // âś… Continue button (WITH validation) - case None => + case None => boundForm.fold( formWithErrors => Future.successful( diff --git a/app/views/verify/SelectSubcontractorsToReverifyView.scala.html b/app/views/verify/SelectSubcontractorsToReverifyView.scala.html index 443117d2..e5f1d2e8 100644 --- a/app/views/verify/SelectSubcontractorsToReverifyView.scala.html +++ b/app/views/verify/SelectSubcontractorsToReverifyView.scala.html @@ -81,7 +81,8 @@

    val checkboxId = s"value-$idx" - val isChecked = selected.contains(row.id) + val value = s"${row.id}|${row.name}" + val isChecked = selected.contains(value) Seq( @@ -94,7 +95,7 @@