From a30a9f36008cbd08fe026f54207fff1123d15664 Mon Sep 17 00:00:00 2001 From: "dilli.mateti" <238047753+dillimateti501@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:54:43 +0000 Subject: [PATCH] DTR-1606 vat registration final code new --- .../RegistrationNumberController.scala | 98 +++++++ app/forms/mappings/Constraints.scala | 47 ++++ .../RegistrationNumberFormProvider.scala | 35 +++ app/navigation/Navigator.scala | 4 +- .../purchaser/RegistrationNumberPage.scala | 27 ++ app/services/purchaser/PurchaserService.scala | 3 +- .../purchaser/RegistrationNumberSummary.scala | 43 +++ .../RegistrationNumberView.scala.html | 52 ++++ conf/app.routes | 5 + conf/messages.en | 15 +- .../RegistrationNumberControllerSpec.scala | 256 ++++++++++++++++++ test/forms/mappings/ConstraintsSpec.scala | 41 +++ .../RegistrationNumberFormProviderSpec.scala | 73 +++++ test/navigation/NavigatorSpec.scala | 3 + .../purchaser/PurchaserServiceSpec.scala | 2 +- .../RegistrationNumberSummarySpec.scala | 65 +++++ 16 files changed, 764 insertions(+), 5 deletions(-) create mode 100644 app/controllers/purchaser/RegistrationNumberController.scala create mode 100644 app/forms/purchaser/RegistrationNumberFormProvider.scala create mode 100644 app/pages/purchaser/RegistrationNumberPage.scala create mode 100644 app/viewmodels/checkAnswers/purchaser/RegistrationNumberSummary.scala create mode 100644 app/views/purchaser/RegistrationNumberView.scala.html create mode 100644 test/controllers/purchaser/RegistrationNumberControllerSpec.scala create mode 100644 test/forms/purchaser/RegistrationNumberFormProviderSpec.scala create mode 100644 test/viewmodels/checkAnswers/purchaser/RegistrationNumberSummarySpec.scala diff --git a/app/controllers/purchaser/RegistrationNumberController.scala b/app/controllers/purchaser/RegistrationNumberController.scala new file mode 100644 index 00000000..b7c32fb6 --- /dev/null +++ b/app/controllers/purchaser/RegistrationNumberController.scala @@ -0,0 +1,98 @@ +/* + * Copyright 2025 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.purchaser + +import controllers.actions.* +import forms.purchaser.RegistrationNumberFormProvider +import models.{Mode, NormalMode} +import models.purchaser.{NameOfPurchaser, WhoIsMakingThePurchase, PurchaserConfirmIdentity} +import navigation.Navigator +import pages.purchaser.{NameOfPurchaserPage, PurchaserConfirmIdentityPage, RegistrationNumberPage, WhoIsMakingThePurchasePage} +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import services.purchaser.PurchaserService +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.purchaser.RegistrationNumberView + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class RegistrationNumberController @Inject()( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: RegistrationNumberFormProvider, + val controllerComponents: MessagesControllerComponents, + view: RegistrationNumberView, + purchaserService: PurchaserService + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + val form = formProvider() + + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { + implicit request => + val purchaserFullName: Option[String] = request.userAnswers.get(NameOfPurchaserPage).map(_.fullName) + // val isPurchaserVATRegistered: Option[String] = Some("VATRegistrationNumber") + val isPurchaserVATRegistered: Option[PurchaserConfirmIdentity] = request.userAnswers.get(PurchaserConfirmIdentityPage) + + purchaserService.checkPurchaserTypeAndCompanyDetails( + purchaserType = WhoIsMakingThePurchase.Company, + userAnswers = request.userAnswers, + continueRoute = { + (purchaserFullName) match { + case Some(purchaserFullName) if isPurchaserVATRegistered.contains(PurchaserConfirmIdentity.VatRegistrationNumber) => + val preparedForm = request.userAnswers.get(RegistrationNumberPage) match { + case None => form + case Some(value) => form.fill(value) + } + Ok(view(preparedForm, mode, purchaserFullName)) + case None => Redirect(controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode)) + case _ => + //TODO update to more relevant path i.e. back to pr-5b + Redirect(controllers.routes.GenericErrorController.onPageLoad()) + } + } + ) + } + + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + + val purchaserFullName: Option[String] = request.userAnswers.get(NameOfPurchaserPage).map(_.fullName) + + purchaserFullName match { + case Some(purchaserFullName) => + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, mode, purchaserFullName))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(RegistrationNumberPage, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage(RegistrationNumberPage, mode, updatedAnswers)) + ) + case _ => + Future.successful(Redirect(controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(mode))) + } + } + } + diff --git a/app/forms/mappings/Constraints.scala b/app/forms/mappings/Constraints.scala index a88588be..564967dc 100644 --- a/app/forms/mappings/Constraints.scala +++ b/app/forms/mappings/Constraints.scala @@ -178,4 +178,51 @@ trait Constraints { checkSum(s"${errorKey}.invalid") ) } + + protected def vatCheckF16Validation(errorKey: String): Constraint[String] = { + + def vatF16Check(errorKey: String): Constraint[String] = { + + def validateVAT(raw: String): Boolean = Try { + // ---- Normalization requested: drop "GB" and spaces ---- + val normalized = { + val up = Option(raw).getOrElse("").trim.toUpperCase + val noGb = if (up.startsWith("GB")) up.drop(2) else up + noGb.replaceAll("\\s+", "") + } + val bodyDigits = normalized.substring(0, 7) + val checkDigitsValue = normalized.substring(7, 9).toInt + + val weights = Array(8, 7, 6, 5, 4, 3, 2) + val sum = bodyDigits + .map(_.asDigit) + .zip(weights) + .map { case (digit, weight) => digit * weight } + .sum + + val check1 = 97 - Math.floorMod(sum, 97) + val check2 = 97 - Math.floorMod(sum + 55, 97) + + checkDigitsValue >= 0 && + checkDigitsValue <= 97 && + (checkDigitsValue == check1 || checkDigitsValue == check2) + } match { + case Success(isValid) => isValid + case Failure(_) => false + } + + Constraint[String] { str => + // Only returns Valid/Invalid; no extra format/length checks here + if (validateVAT(str)) Valid else Invalid(errorKey) + } + } + + firstError( + regexp("^[0-9]*$", s"${errorKey}.regex.invalid"), + minLength(9, s"${errorKey}.length"), + maxLength(9, s"${errorKey}.length"), + vatF16Check(s"${errorKey}.invalid") + ) + } + } diff --git a/app/forms/purchaser/RegistrationNumberFormProvider.scala b/app/forms/purchaser/RegistrationNumberFormProvider.scala new file mode 100644 index 00000000..582eeb0c --- /dev/null +++ b/app/forms/purchaser/RegistrationNumberFormProvider.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.purchaser + +import forms.mappings.Mappings +import play.api.data.Form +import play.api.data.validation.Constraints.minLength +import forms.mappings.Constraints +import javax.inject.Inject + +class RegistrationNumberFormProvider @Inject() extends Mappings with Constraints{ + + private val RegistrationNumberPattern = "^[0-9]*$" + + def apply(): Form[String] = + Form( + "registrationNumber" -> text("purchaser.registrationNumber.error.required") + .verifying(vatCheckF16Validation("purchaser.registrationNumber.error")) + + ) +} diff --git a/app/navigation/Navigator.scala b/app/navigation/Navigator.scala index bb4afa52..6d4b9bd5 100644 --- a/app/navigation/Navigator.scala +++ b/app/navigation/Navigator.scala @@ -62,7 +62,7 @@ class Navigator @Inject()() { private def isPurchaserSection(page: Page): Boolean = page match { - case WhoIsMakingThePurchasePage | NameOfPurchaserPage | DoesPurchaserHaveNIPage | PurchaserNationalInsurancePage | PurchaserFormOfIdIndividualPage | AddPurchaserPhoneNumberPage | PurchaserDateOfBirthPage | EnterPurchaserPhoneNumberPage | PurchaserPartnershipUtrPage | PurchaserCorporationTaxUTRPage => true + case WhoIsMakingThePurchasePage | NameOfPurchaserPage | DoesPurchaserHaveNIPage | PurchaserNationalInsurancePage | PurchaserFormOfIdIndividualPage | AddPurchaserPhoneNumberPage | PurchaserDateOfBirthPage | EnterPurchaserPhoneNumberPage | PurchaserPartnershipUtrPage | PurchaserCorporationTaxUTRPage | RegistrationNumberPage => true case _ => false } @@ -88,6 +88,8 @@ class Navigator @Inject()() { _ => controllers.purchaser.routes.PurchaserPartnershipUtrController.onPageLoad(NormalMode) case PurchaserCorporationTaxUTRPage => //TODO: to be updated to 'type of business' (DTR-1679 pr-9 - sprint 5) _ => controllers.purchaser.routes.PurchaserCorporationTaxUTRController.onPageLoad(NormalMode) + case RegistrationNumberPage => //TODO update post pr-9 page implementation in future sprints + _ => controllers.routes.ReturnTaskListController.onPageLoad() case _ => _ => routes.IndexController.onPageLoad() } diff --git a/app/pages/purchaser/RegistrationNumberPage.scala b/app/pages/purchaser/RegistrationNumberPage.scala new file mode 100644 index 00000000..8676af6f --- /dev/null +++ b/app/pages/purchaser/RegistrationNumberPage.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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.purchaser + +import pages.QuestionPage +import play.api.libs.json.JsPath + +case object RegistrationNumberPage extends QuestionPage[String] { + + override def path: JsPath = JsPath \ "purchaserCurrent" \ toString + + override def toString: String = "registrationNumber" +} diff --git a/app/services/purchaser/PurchaserService.scala b/app/services/purchaser/PurchaserService.scala index 553ee043..96e31eb7 100644 --- a/app/services/purchaser/PurchaserService.scala +++ b/app/services/purchaser/PurchaserService.scala @@ -116,8 +116,7 @@ class PurchaserService { case PurchaserConfirmIdentity.CorporationTaxUTR => controllers.purchaser.routes.PurchaserCorporationTaxUTRController.onPageLoad(NormalMode) case PurchaserConfirmIdentity.VatRegistrationNumber => - // TODO: redirect to Vat Reg Num page - controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode) + controllers.purchaser.routes.RegistrationNumberController.onPageLoad(NormalMode) case PurchaserConfirmIdentity.AnotherFormOfID => // TODO: redirect to Another Form ID page controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode) diff --git a/app/viewmodels/checkAnswers/purchaser/RegistrationNumberSummary.scala b/app/viewmodels/checkAnswers/purchaser/RegistrationNumberSummary.scala new file mode 100644 index 00000000..c1e262be --- /dev/null +++ b/app/viewmodels/checkAnswers/purchaser/RegistrationNumberSummary.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2025 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.purchaser + +import controllers.routes +import models.{CheckMode, UserAnswers} +import pages.purchaser.RegistrationNumberPage +import play.api.i18n.Messages +import play.twirl.api.HtmlFormat +import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.govuk.summarylist.* +import viewmodels.implicits.* + +object RegistrationNumberSummary { + + def row(answers: UserAnswers)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(RegistrationNumberPage).map { + answer => + + SummaryListRowViewModel( + key = "purchaser.registrationNumber.checkYourAnswersLabel", + value = ValueViewModel(HtmlFormat.escape(answer).toString), + actions = Seq( + ActionItemViewModel("site.change", controllers.purchaser.routes.RegistrationNumberController.onPageLoad(CheckMode).url) + .withVisuallyHiddenText(messages("purchaser.registrationNumber.change.hidden")) + ) + ) + } +} diff --git a/app/views/purchaser/RegistrationNumberView.scala.html b/app/views/purchaser/RegistrationNumberView.scala.html new file mode 100644 index 00000000..8cc65648 --- /dev/null +++ b/app/views/purchaser/RegistrationNumberView.scala.html @@ -0,0 +1,52 @@ +@* + * Copyright 2025 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.InputWidth._ +@import viewmodels.LabelSize.Large + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukInput: GovukInput, + govukButton: GovukButton +) + +@(form: Form[_], mode: Mode, purchaserName: String)(implicit request: Request[_], messages: Messages) + +@layout(pageTitle = title(form, messages("purchaser.registrationNumber.title"))) { + + @formHelper(action = controllers.purchaser.routes.RegistrationNumberController.onSubmit(mode)) { + + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + @messages("purchaser.registrationNumber.caption") + + @govukInput( + InputViewModel( + field = form("registrationNumber"), + label = LabelViewModel(messages("purchaser.registrationNumber.heading",purchaserName)).asPageHeading(size = Large) + ) + .withWidth(Full) + .withHint(HintViewModel(HtmlContent(Html(messages("purchaser.registrationNumber.hint",purchaserName))))) + ) + + @govukButton( + ButtonViewModel(messages("site.save.continue")) + ) + } +} diff --git a/conf/app.routes b/conf/app.routes index 0921145e..7308e82d 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -185,6 +185,11 @@ POST /about-the-purchaser/partnership-utr GET /about-the-purchaser/partnership-utr/change controllers.purchaser.PurchaserPartnershipUtrController.onPageLoad(mode: Mode = CheckMode) POST /about-the-purchaser/partnership-utr/change controllers.purchaser.PurchaserPartnershipUtrController.onSubmit(mode: Mode = CheckMode) +GET /about-the-purchaser/vat-registration-number controllers.purchaser.RegistrationNumberController.onPageLoad(mode: Mode = NormalMode) +POST /about-the-purchaser/vat-registration-number controllers.purchaser.RegistrationNumberController.onSubmit(mode: Mode = NormalMode) +GET /about-the-purchaser/vat-registration-number/change controllers.purchaser.RegistrationNumberController.onPageLoad(mode: Mode = CheckMode) +POST /about-the-purchaser/vat-registration-number/change controllers.purchaser.RegistrationNumberController.onSubmit(mode: Mode = CheckMode) + GET /about-the-purchaser/corporation-tax-utr controllers.purchaser.PurchaserCorporationTaxUTRController.onPageLoad(mode: Mode = NormalMode) POST /about-the-purchaser/corporation-tax-utr controllers.purchaser.PurchaserCorporationTaxUTRController.onSubmit(mode: Mode = NormalMode) GET /about-the-purchaser/corporation-tax-utr/change controllers.purchaser.PurchaserCorporationTaxUTRController.onPageLoad(mode: Mode = CheckMode) diff --git a/conf/messages.en b/conf/messages.en index d713c271..e14c3be2 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -550,4 +550,17 @@ purchaser.purchaserCorporationTaxUTR.error.required = Enter Corporation Tax Uniq purchaser.purchaserCorporationTaxUTR.error.length = Corporation Tax Unique Taxpayer Reference (UTR) must be 10 characters purchaser.purchaserCorporationTaxUTR.error.regex.invalid = Enter a valid UTR purchaser.purchaserCorporationTaxUTR.error.invalid = Enter a valid UTR -purchaser.purchaserCorporationTaxUTR.change.hidden = Corporation Tax Unique Taxpayer Reference (UTR) \ No newline at end of file +purchaser.purchaserCorporationTaxUTR.change.hidden = Corporation Tax Unique Taxpayer Reference (UTR) + +purchaser.registrationNumber.title = What is the purchaser’s VAT registration number? - About the purchaser +purchaser.registrationNumber.caption = About the purchaser +purchaser.registrationNumber.heading = What is {0}’s VAT registration number? +purchaser.registrationNumber.checkYourAnswersLabel = registrationNumber +purchaser.registrationNumber.error.required = Enter VAT Registration Number +purchaser.registrationNumber.error.length = Registration Number must be 9 digits +purchaser.registrationNumber.error.invalid = Registration Number must be 9 digits +purchaser.registrationNumber.change.hidden = registrationNumber +purchaser.registrationNumber.error.regex.error = Numeric only +purchaser.registrationNumber.error.regex.invalid = Numeric only +purchaser.registrationNumber.error.invalid = Registration Number is invalid +purchaser.registrationNumber.hint = This is 9 numbers, sometimes with ‘GB’ at the start, for example 123456789 or GB123456789. You can find it on {0}’s VAT registration certificate diff --git a/test/controllers/purchaser/RegistrationNumberControllerSpec.scala b/test/controllers/purchaser/RegistrationNumberControllerSpec.scala new file mode 100644 index 00000000..f85e1d6b --- /dev/null +++ b/test/controllers/purchaser/RegistrationNumberControllerSpec.scala @@ -0,0 +1,256 @@ +/* + * Copyright 2025 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.purchaser + +import base.SpecBase +import controllers.routes +import forms.purchaser.RegistrationNumberFormProvider +import models.{NormalMode, UserAnswers} +import models.purchaser.{NameOfPurchaser, WhoIsMakingThePurchase, PurchaserConfirmIdentity} +import navigation.{FakeNavigator, Navigator} +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import pages.purchaser.{NameOfPurchaserPage, RegistrationNumberPage, WhoIsMakingThePurchasePage,PurchaserConfirmIdentityPage} +import play.api.inject.bind +import play.api.libs.json.Json +import play.api.mvc.Call +import play.api.test.FakeRequest +import play.api.test.Helpers.* +import repositories.SessionRepository +import views.html.purchaser.RegistrationNumberView + +import scala.concurrent.Future + +class RegistrationNumberControllerSpec extends SpecBase with MockitoSugar { + + def onwardRoute = Call("GET", "/foo") + + val formProvider = new RegistrationNumberFormProvider() + val form = formProvider() + + lazy val registrationNumberRoute = controllers.purchaser.routes.RegistrationNumberController.onPageLoad(NormalMode).url + + val testUserAnswers = UserAnswers( + id = "test-session-id", + storn = "test-storn-123", + returnId = Some("test-return-id"), + fullReturn = None, + data = Json.obj( + "purchaserCurrent" -> Json.obj( + "nameOfPurchaser" -> Json.obj( + "forename1" -> "John", + "forename2" -> "Middle", + "name" -> "Doe" + ), + "whoIsMakingThePurchase" -> "Company", + "purchaserConfirmIdentity" -> "vatRegistrationNumber" + + ) + ) + ) + + val testUserAnswersIndividual = UserAnswers( + id = "test-session-id", + storn = "test-storn-123", + returnId = Some("test-return-id"), + fullReturn = None, + data = Json.obj( + "purchaserCurrent" -> Json.obj( + "nameOfPurchaser" -> Json.obj( + "forename1" -> "John", + "forename2" -> "Middle", + "name" -> "Doe" + ), + "whoIsMakingThePurchase" -> "Individual" + ) + ) + ) + + val testUserAnswersNoName = UserAnswers( + id = "test-session-id", + storn = "test-storn-123", + returnId = Some("test-return-id"), + fullReturn = None, + data = Json.obj( + "purchaserCurrent" -> Json.obj( + "whoIsMakingThePurchase" -> "Company" + ) + ) + ) + val userAnswersWithPurchaserNameAndRN: UserAnswers = emptyUserAnswers + .set(NameOfPurchaserPage, NameOfPurchaser(Some("John"), None, "Smith")).success.value + .set(RegistrationNumberPage, "123456789").success.value + .set(WhoIsMakingThePurchasePage, WhoIsMakingThePurchase.Company).success.value + .set(PurchaserConfirmIdentityPage, PurchaserConfirmIdentity.VatRegistrationNumber).success.value + + "RegistrationNumber Controller" - { + + "must return OK and the correct view for a GET when purchaser name present and purchaser type is company" in { + + val application = applicationBuilder(userAnswers = Some(testUserAnswers)).build() + + running(application) { + val request = FakeRequest(GET, registrationNumberRoute) + + val result = route(application, request).value + + val view = application.injector.instanceOf[RegistrationNumberView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form, NormalMode, "John Middle Doe")(request, messages(application)).toString + } + } + + "must populate the view correctly on a GET when the question has previously been answered" in { + + val application = applicationBuilder(userAnswers = Some(userAnswersWithPurchaserNameAndRN)).build() + + running(application) { + val request = FakeRequest(GET, registrationNumberRoute) + + val view = application.injector.instanceOf[RegistrationNumberView] + + val result = route(application, request).value + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form.fill("123456789"), NormalMode, "John Smith")(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(userAnswersWithPurchaserNameAndRN)) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + val request = + FakeRequest(POST, registrationNumberRoute) + .withFormUrlEncodedBody(("registrationNumber", "438573857")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url + } + } + + + "must redirect to purchaser name page when the name is missing for a GET" in { + val application = applicationBuilder(userAnswers = Some(testUserAnswersNoName)).build() + + running(application) { + val request = FakeRequest(GET, registrationNumberRoute) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode).url + } + } + + "must redirect to purchaser name page when the name is missing for a POST" in { + val application = applicationBuilder(userAnswers = Some(testUserAnswersNoName)).build() + + running(application) { + val request = + FakeRequest(POST, registrationNumberRoute) + .withFormUrlEncodedBody(("vatRegistrationNumber" -> "answer")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode).url + } + } + + "must redirect to Generic Error if purchaser type (company vs individual) does not match for a GET" in { + val application = applicationBuilder(userAnswers = Some(testUserAnswersIndividual)).build() + + running(application) { + val request = FakeRequest(GET, registrationNumberRoute) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.routes.GenericErrorController.onPageLoad().url + } + } + + //TODO : write this test once previous page has been implemented + "must redirect to Generic Error if purchaser information does not match partnership" in {} + + "must return a Bad Request and errors when invalid data is submitted" in { + + val application = applicationBuilder(userAnswers = Some(testUserAnswers)).build() + + running(application) { + val request = + FakeRequest(POST, registrationNumberRoute) + .withFormUrlEncodedBody("vatRegistrationNumber" -> "123") + + val boundForm = form.bind(Map("vatRegistrationNumber" -> "123")) + + val view = application.injector.instanceOf[RegistrationNumberView] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + contentAsString(result) mustEqual view(boundForm, NormalMode, "John Middle Doe")(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, registrationNumberRoute) + + 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, registrationNumberRoute) + .withFormUrlEncodedBody(("value", "answer")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + } + } + } +} \ No newline at end of file diff --git a/test/forms/mappings/ConstraintsSpec.scala b/test/forms/mappings/ConstraintsSpec.scala index cda0cdac..352fc619 100644 --- a/test/forms/mappings/ConstraintsSpec.scala +++ b/test/forms/mappings/ConstraintsSpec.scala @@ -285,4 +285,45 @@ class ConstraintsSpec extends AnyFreeSpec with Matchers with ScalaCheckPropertyC result mustBe Invalid("error.utr.regex.invalid", "^[0-9]*$") } } + + "vatChecksumMod97Len9" - { + val validVATString: Seq[String] = Seq("438573857","438573857", "438573857") + val invalidVatStrings: Seq[String] = Seq("123456789","987654321") + val invalidVatLength: Seq[String] = Seq("12134", "2343243", "3123123123123123", "42342342342342342323423423") + val invalidVatRegex: Seq[String] = Seq("12345678H", "ZXCVBNMLK", "12-------", " ") + val invalidVatMultipleErrors: String = "12ab" + + "must return Valid for a valid vat number" in { + validVATString.foreach { vat => + val result = vatCheckF16Validation("purchaser.registrationNumber.error")(vat) + result mustBe Valid + } + } + + "must return Invalid for an invalid vat number" in { + invalidVatStrings.foreach{ vat => + val result = vatCheckF16Validation("purchaser.registrationNumber.error")(vat) + result mustBe Invalid("purchaser.registrationNumber.error.invalid") + } + } + + "must return Invalid for numbers more or less than 9 digits" in { + invalidVatLength.foreach { vat => + val result = vatCheckF16Validation("purchaser.registrationNumber.error")(vat) + result mustBe Invalid("purchaser.registrationNumber.error.length", 9) + } + } + + "must return Invalid for VAT values with no digits" in { + invalidVatRegex.foreach { vat => + val result = vatCheckF16Validation("purchaser.registrationNumber.error")(vat) + result mustBe Invalid("purchaser.registrationNumber.error.regex.invalid", "^[0-9]*$") + } + } + + "must return Invalid and first error message for VAT values with multiple errors" in { + val result = validUtr("purchaser.registrationNumber.error")(invalidVatMultipleErrors) + result mustBe Invalid("purchaser.registrationNumber.error.regex.invalid", "^[0-9]*$") + } + } } diff --git a/test/forms/purchaser/RegistrationNumberFormProviderSpec.scala b/test/forms/purchaser/RegistrationNumberFormProviderSpec.scala new file mode 100644 index 00000000..6da347a8 --- /dev/null +++ b/test/forms/purchaser/RegistrationNumberFormProviderSpec.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2025 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.purchaser + +import forms.behaviours.StringFieldBehaviours +import forms.purchaser.RegistrationNumberFormProvider +import org.scalacheck.Gen +import play.api.data.FormError + +class RegistrationNumberFormProviderSpec extends StringFieldBehaviours { + + val requiredKey = "purchaser.registrationNumber.error.required" + val lengthKey = "purchaser.registrationNumber.error.length" + val exactLength = 9 + val invalidKey = "purchaser.registrationNumber.error.regex.error" + + val form = new RegistrationNumberFormProvider()() + val fieldName = "registrationNumber" + + // Generator: exactly 9 numeric characters + private val numericStringsOfExactLengthNine: Gen[String] = + Gen.listOfN(exactLength, Gen.numChar).map(_.mkString) + + ".value" - { + + "bind successfully when pass correct registration number" in { + val result = form.bind(Map(fieldName -> "438573857")) + result.errors mustBe Nil + } + + "bind successfully when pass incorrect registration number" in { + val result = form.bind(Map(fieldName -> "")) + result.errors.map(_.message) must contain only "purchaser.registrationNumber.error.required" + } + + "bind successfully when leading zeros are present" in { + val result = form.bind(Map(fieldName -> "438573857")) + result.errors mustBe Nil + } + + + "fail with length error when fewer than exact length" in { + val tooShortValues = Seq("1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "2345678") + tooShortValues.foreach { v => + val result = form.bind(Map(fieldName -> v)) + result.errors must contain only FormError(fieldName, lengthKey, Seq(exactLength)) + } + + } + "fail with regex error for non-numeric input (ignoring args)" in { + val badValues = Seq("ABCDEFGHI", "12345ABCD", "12 345678", "1234-5678") + badValues.foreach { v => + val result = form.bind(Map(fieldName -> v)) + result.errors.map(_.message) must contain only "purchaser.registrationNumber.error.regex.invalid" + } + } + } + } + diff --git a/test/navigation/NavigatorSpec.scala b/test/navigation/NavigatorSpec.scala index 06a46215..bd240bac 100644 --- a/test/navigation/NavigatorSpec.scala +++ b/test/navigation/NavigatorSpec.scala @@ -167,6 +167,9 @@ class NavigatorSpec extends SpecBase { navigator.nextPage(NameOfPurchaserPage, NormalMode, userAnswers) mustBe controllers.purchaser.routes.PurchaserAddressController.redirectToAddressLookupPurchaser() + //TODO update post pr-9 page implementation in future sprints + navigator.nextPage(RegistrationNumberPage, NormalMode, userAnswers) mustBe + controllers.routes.ReturnTaskListController.onPageLoad() } "must return false for non-purchaser section pages" in { diff --git a/test/services/purchaser/PurchaserServiceSpec.scala b/test/services/purchaser/PurchaserServiceSpec.scala index 5579fb2d..9e2fb2cd 100644 --- a/test/services/purchaser/PurchaserServiceSpec.scala +++ b/test/services/purchaser/PurchaserServiceSpec.scala @@ -77,7 +77,7 @@ class PurchaserServiceSpec extends SpecBase { "must return NameOfPurchaser for VatRegistrationNumber" in { val result = service.confirmIdentityNextPage(PurchaserConfirmIdentity.VatRegistrationNumber, NormalMode) - result mustEqual controllers.purchaser.routes.NameOfPurchaserController.onPageLoad(NormalMode) + result mustEqual controllers.purchaser.routes.RegistrationNumberController.onPageLoad(NormalMode) } "must return NameOfPurchaser for AnotherFormOfID" in { diff --git a/test/viewmodels/checkAnswers/purchaser/RegistrationNumberSummarySpec.scala b/test/viewmodels/checkAnswers/purchaser/RegistrationNumberSummarySpec.scala new file mode 100644 index 00000000..0bfcff32 --- /dev/null +++ b/test/viewmodels/checkAnswers/purchaser/RegistrationNumberSummarySpec.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2025 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.purchaser + +import base.SpecBase +import models.{CheckMode, UserAnswers} +import models.purchaser.NameOfPurchaser +import pages.purchaser.{RegistrationNumberPage, NameOfPurchaserPage} +import play.api.i18n.Messages +import play.api.libs.json.{JsNull, Json} +import play.api.test.Helpers.running +import uk.gov.hmrc.govukfrontend.views.viewmodels.content.{HtmlContent, Text} + +class RegistrationNumberSummarySpec extends SpecBase { + + val purchaserName = NameOfPurchaser(None, None, "Samsung") + + "RegistrationNumberSummarySpec" - { + + "when purchaser name is present" - { + + "must return a summary list row with surname only" in { + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + running(application) { + implicit val msgs: Messages = messages(application) + + val userAnswers = emptyUserAnswers + .set(RegistrationNumberPage, "123456789").success.value + + val result = RegistrationNumberSummary.row(userAnswers).getOrElse(fail("Failed to get summary list row")) + + result.key.content.asHtml.toString() mustEqual msgs("purchaser.registrationNumber.checkYourAnswersLabel") + + val htmlContent = + result.value.content match { + case Text(answer) => answer + case HtmlContent(html) => html.toString + } + htmlContent mustEqual "123456789" + + result.actions.get.items.size mustEqual 1 + result.actions.get.items.head.href mustEqual controllers.purchaser.routes.RegistrationNumberController.onPageLoad(CheckMode).url + result.actions.get.items.head.content.asHtml.toString() must include(msgs("site.change")) + result.actions.get.items.head.visuallyHiddenText.value mustEqual msgs("purchaser.registrationNumber.change.hidden") + } + } + + + } + } +} \ No newline at end of file