Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions app/controllers/purchaser/RegistrationNumberController.scala
Original file line number Diff line number Diff line change
@@ -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)))
}
}
}

47 changes: 47 additions & 0 deletions app/forms/mappings/Constraints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}

}
35 changes: 35 additions & 0 deletions app/forms/purchaser/RegistrationNumberFormProvider.scala
Original file line number Diff line number Diff line change
@@ -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"))

)
}
4 changes: 3 additions & 1 deletion app/navigation/Navigator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()
}

Expand Down
27 changes: 27 additions & 0 deletions app/pages/purchaser/RegistrationNumberPage.scala
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 1 addition & 2 deletions app/services/purchaser/PurchaserService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"))
)
)
}
}
52 changes: 52 additions & 0 deletions app/views/purchaser/RegistrationNumberView.scala.html
Original file line number Diff line number Diff line change
@@ -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))
}
<span class="govuk-caption-l">@messages("purchaser.registrationNumber.caption")</span>

@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"))
)
}
}
5 changes: 5 additions & 0 deletions conf/app.routes
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion conf/messages.en
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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
Loading