Repository: hmrc/cbcr-frontend
Pull request #672: DLS11367: Update phone number error messages
Author: login: ShivangiShah42
Created: 2025-03-12T11:02:05Z | Merged: 2025-03-25T12:03:47Z



Diff:
diff --git a/app/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionController.scala b/app/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionController.scala
index b8084ac6..d2a8fcd6 100644
--- a/app/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionController.scala
+++ b/app/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionController.scala
@@ -36,6 +36,7 @@ import uk.gov.hmrc.cbcrfrontend.form.SubmitterInfoForm.submitterInfoForm
 import uk.gov.hmrc.cbcrfrontend.model._
 import uk.gov.hmrc.cbcrfrontend.repositories.CBCSessionCache
 import uk.gov.hmrc.cbcrfrontend.services._
+import uk.gov.hmrc.cbcrfrontend.util.CBCRMapping.ukPhoneNumberConstraint
 import uk.gov.hmrc.cbcrfrontend.views.Views
 import uk.gov.hmrc.http.HeaderCarrier
 import uk.gov.hmrc.play.audit.http.connector.AuditConnector
@@ -361,9 +362,15 @@ class SubmissionController @Inject() (
       form <- cache.readOption[SubmitterInfo].map { osi =>
                 (osi.map(_.fullName), osi.map(_.contactPhone), osi.map(_.email))
                   .mapN { (name, phone, email) =>
-                    submitterInfoForm.bind(Map("fullName" -> name, "contactPhone" -> phone, "email" -> email.value))
+                    submitterInfoForm(
+                      ukPhoneNumberConstraint
+                    ).bind(Map("fullName" -> name, "contactPhone" -> phone, "email" -> email.value))
                   }
-                  .getOrElse(submitterInfoForm)
+                  .getOrElse(
+                    submitterInfoForm(
+                      ukPhoneNumberConstraint
+                    )
+                  )
               }
       fileDetails <- cache.read[FileDetails].getOrElse(throw new RuntimeException("Missing file upload details"))
 
@@ -393,7 +400,7 @@ class SubmissionController @Inject() (
 
   val submitSubmitterInfo: Action[Map[String, Seq[String]]] = Action.async(parse.formUrlEncoded) { implicit request =>
     authorised().retrieve(Retrievals.affinityGroup) { userType =>
-      submitterInfoForm
+      submitterInfoForm(ukPhoneNumberConstraint)
         .bindFromRequest()
         .fold(
           formWithErrors =>
diff --git a/app/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionController.scala b/app/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionController.scala
index eb67aa37..d271712e 100644
--- a/app/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionController.scala
+++ b/app/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionController.scala
@@ -16,9 +16,9 @@
 
 package uk.gov.hmrc.cbcrfrontend.controllers
 
-import cats.syntax.apply._
 import cats.data.EitherT
 import cats.implicits.{catsStdInstancesForFuture, catsSyntaxEitherId, toShow}
+import cats.syntax.apply._
 import play.api.Logging
 import play.api.libs.json.{JsString, Json}
 import play.api.mvc._
@@ -31,6 +31,7 @@ import uk.gov.hmrc.cbcrfrontend.form.SubscriptionDataForm._
 import uk.gov.hmrc.cbcrfrontend.model._
 import uk.gov.hmrc.cbcrfrontend.repositories.CBCSessionCache
 import uk.gov.hmrc.cbcrfrontend.services._
+import uk.gov.hmrc.cbcrfrontend.util.CBCRMapping.{phoneNumberConstraint, ukPhoneNumberConstraint}
 import uk.gov.hmrc.cbcrfrontend.views.Views
 import uk.gov.hmrc.cbcrfrontend.{errorRedirect, _}
 import uk.gov.hmrc.http.HeaderCarrier
@@ -64,7 +65,9 @@ class SubscriptionController @Inject() (
   def submitSubscriptionData: Action[AnyContent] = Action.async { implicit request =>
     authorised(AffinityGroup.Organisation and User).retrieve(Retrievals.credentials) { creds =>
       logger.debug("Country by Country: Generate CBCId and Store Data")
-      subscriptionDataForm
+      subscriptionDataForm(
+        ukPhoneNumberConstraint
+      )
         .bindFromRequest()
         .fold(
           errors => BadRequest(views.contactInfoSubscriber(errors)),
@@ -132,7 +135,7 @@ class SubscriptionController @Inject() (
 
   def contactInfoSubscriber: Action[AnyContent] = Action.async { implicit request =>
     authorised(AffinityGroup.Organisation and User) {
-      Ok(views.contactInfoSubscriber(subscriptionDataForm))
+      Ok(views.contactInfoSubscriber(subscriptionDataForm(phoneNumberConstraint)))
     }
   }
 
@@ -153,14 +156,15 @@ class SubscriptionController @Inject() (
           case error                  => errorRedirect(error, views.notAuthorisedIndividual, views.errorTemplate)
         },
         { case subData -> cbcId =>
-          val prepopulatedForm = subscriptionDataForm.bind(
-            Map(
-              "firstName"   -> subData.names.name1,
-              "lastName"    -> subData.names.name2,
-              "email"       -> subData.contact.email.value,
-              "phoneNumber" -> subData.contact.phoneNumber
+          val prepopulatedForm = subscriptionDataForm(phoneNumberConstraint)
+            .bind(
+              Map(
+                "firstName"   -> subData.names.name1,
+                "lastName"    -> subData.names.name2,
+                "email"       -> subData.contact.email.value,
+                "phoneNumber" -> subData.contact.phoneNumber
+              )
             )
-          )
           Ok(views.updateContactInfoSubscriber(prepopulatedForm, cbcId))
         }
       )
@@ -176,7 +180,9 @@ class SubscriptionController @Inject() (
                    )
         } yield cbcId
 
-        subscriptionDataForm
+        subscriptionDataForm(
+          ukPhoneNumberConstraint
+        )
           .bindFromRequest()
           .fold(
             errors =>
diff --git a/app/uk/gov/hmrc/cbcrfrontend/form/SubmitterInfoForm.scala b/app/uk/gov/hmrc/cbcrfrontend/form/SubmitterInfoForm.scala
index ae20ef84..08758926 100644
--- a/app/uk/gov/hmrc/cbcrfrontend/form/SubmitterInfoForm.scala
+++ b/app/uk/gov/hmrc/cbcrfrontend/form/SubmitterInfoForm.scala
@@ -18,20 +18,19 @@ package uk.gov.hmrc.cbcrfrontend.form
 
 import play.api.data.Form
 import play.api.data.Forms.{mapping, text}
+import play.api.data.validation.Constraint
 import uk.gov.hmrc.cbcrfrontend.emailaddress.{EmailAddress, EmailAddressValidation}
 import uk.gov.hmrc.cbcrfrontend.form.SubscriptionDataForm.condTrue
 import uk.gov.hmrc.cbcrfrontend.model.SubmitterInfo
+import uk.gov.hmrc.cbcrfrontend.util.CBCRMapping.validatePhoneNumber
 
 object SubmitterInfoForm {
-  val submitterInfoForm: Form[SubmitterInfo] = Form(
+  def submitterInfoForm(constraint: Constraint[String]): Form[SubmitterInfo] = Form(
     mapping(
       "fullName" -> text.verifying("submitterInfo.fullName.error", _.trim != ""),
-      "contactPhone" -> text
-        .verifying("submitterInfo.phoneNumber.error.empty", _.trim != "")
-        .verifying(
-          "submitterInfo.phoneNumber.error.invalid",
-          x => condTrue(x.trim != "", x.matches("""^[0-9 )/(-*#]{1,24}$"""))
-        ),
+      "contactPhone" -> validatePhoneNumber(
+        constraint
+      ),
       "email" -> text
         .verifying("submitterInfo.emailAddress.error.empty", _.trim != "")
         .verifying(
diff --git a/app/uk/gov/hmrc/cbcrfrontend/form/SubscriptionDataForm.scala b/app/uk/gov/hmrc/cbcrfrontend/form/SubscriptionDataForm.scala
index 5093edcf..f294194f 100644
--- a/app/uk/gov/hmrc/cbcrfrontend/form/SubscriptionDataForm.scala
+++ b/app/uk/gov/hmrc/cbcrfrontend/form/SubscriptionDataForm.scala
@@ -18,22 +18,21 @@ package uk.gov.hmrc.cbcrfrontend.form
 
 import play.api.data.Form
 import play.api.data.Forms.{mapping, text}
+import play.api.data.validation.Constraint
 import uk.gov.hmrc.cbcrfrontend.emailaddress.{EmailAddress, EmailAddressValidation}
 import uk.gov.hmrc.cbcrfrontend.model.SubscriberContact
+import uk.gov.hmrc.cbcrfrontend.util.CBCRMapping.validatePhoneNumber
 
 object SubscriptionDataForm {
   def condTrue(condition: Boolean, statement: Boolean): Boolean = if (condition) statement else true
 
-  val subscriptionDataForm: Form[SubscriberContact] = Form(
+  def subscriptionDataForm(constraint: Constraint[String]): Form[SubscriberContact] = Form(
     mapping(
       "firstName" -> text.verifying("contactInfoSubscriber.firstName.error", _.trim != ""),
       "lastName"  -> text.verifying("contactInfoSubscriber.lastName.error", _.trim != ""),
-      "phoneNumber" -> text
-        .verifying("contactInfoSubscriber.phoneNumber.error.empty", _.trim != "")
-        .verifying(
-          "contactInfoSubscriber.phoneNumber.error.invalid",
-          x => condTrue(x.trim != "", x.matches("""^[0-9 )/(-*#]{1,24}$"""))
-        ),
+      "phoneNumber" -> validatePhoneNumber(
+        constraint
+      ),
       "email" -> text
         .verifying("contactInfoSubscriber.emailAddress.error.empty", _.trim != "")
         .verifying(
diff --git a/app/uk/gov/hmrc/cbcrfrontend/util/CBCRMapping.scala b/app/uk/gov/hmrc/cbcrfrontend/util/CBCRMapping.scala
new file mode 100644
index 00000000..0a6af9db
--- /dev/null
+++ b/app/uk/gov/hmrc/cbcrfrontend/util/CBCRMapping.scala
@@ -0,0 +1,47 @@
+/*
+ * 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 uk.gov.hmrc.cbcrfrontend.util
+
+import play.api.data.Forms.text
+import play.api.data.Mapping
+import play.api.data.validation.{Constraint, Invalid, Valid}
+
+object CBCRMapping {
+
+  def validatePhoneNumber(constraint: Constraint[String]): Mapping[String] =
+    text.verifying(constraint)
+
+  val ukPhoneNumberConstraint: Constraint[String] =
+    Constraint {
+      case text if text.isBlank       => Invalid("submitterInfo.phoneNumber.error.empty")
+      case text if text.contains("+") => Invalid("submitterInfo.phoneNumber.error.invalid.plus.sign")
+      case text if text.exists(c => "@!$%^&_=~{}[]:".contains(c)) =>
+        Invalid("submitterInfo.phoneNumber.error.invalid.forbidden.char")
+      case text if !text.matches("^[A-Z0-9 )/(-*#]+$") =>
+        Invalid("submitterInfo.phoneNumber.error.invalid")
+      case _ => Valid
+    }
+
+  val phoneNumberConstraint: Constraint[String] =
+    Constraint {
+      case text if text.isBlank => Invalid("contactInfoSubscriber.phoneNumber.error.empty")
+      case text if !text.matches("^[0-9 )/(-*#]{1,24}$") =>
+        Invalid("contactInfoSubscriber.phoneNumber.error.invalid")
+      case _ => Valid
+    }
+
+}
diff --git a/build.sbt b/build.sbt
index 2baa66e1..25f6d584 100644
--- a/build.sbt
+++ b/build.sbt
@@ -13,7 +13,7 @@ lazy val microservice = Project(appName, file("."))
   .settings(scalafmtOnCompile := true)
   .settings(
     majorVersion := 1,
-    scalaVersion := "2.13.12",
+    scalaVersion := "2.13.16",
     libraryDependencies ++= AppDependencies.compile ++ AppDependencies.test()
   )
   .settings(
diff --git a/conf/messages b/conf/messages
index 1eb3cd33..2be6f530 100644
--- a/conf/messages
+++ b/conf/messages
@@ -125,12 +125,14 @@ contactInfoSubscriber.lastName.label=Last name
 contactInfoSubscriber.lastName.error=Enter the last name
 contactInfoSubscriber.lastName.error.header=You have not entered your last name
 contactInfoSubscriber.lastName.hint=The last name of the person we should contact
-contactInfoSubscriber.phoneNumber.label=Phone number
+contactInfoSubscriber.phoneNumber.label=UK telephone number
 contactInfoSubscriber.phoneNumber.hint=Enter numbers only
 contactInfoSubscriber.phoneNumber.error.invalid.header=We did not recognise the phone number
 contactInfoSubscriber.phoneNumber.error.invalid.header.l1=You must only include numbers
-contactInfoSubscriber.phoneNumber.error.invalid=Enter a valid phone number
-contactInfoSubscriber.phoneNumber.error.empty=Enter the phone number
+contactInfoSubscriber.phoneNumber.error.invalid=Enter a UK telephone number in the right format
+contactInfoSubscriber.phoneNumber.error.empty=Enter a UK telephone number
+contactInfoSubscriber.phoneNumber.error.invalid.forbidden.char=Telephone number must only include letters a to z, numbers 0 to 9, round brackets, forward slashes, hyphens, spaces, asterisks and hash signs
+contactInfoSubscriber.phoneNumber.error.invalid.plus.sign=Enter your telephone number without a plus sign
 contactInfoSubscriber.emailAddress.label=Email address
 contactInfoSubscriber.emailAdress.hint=The email address of the person we should contact
 contactInfoSubscriber.emailAddress.error.invalid.header=We did not recognise the email address
@@ -242,7 +244,7 @@ submitSummary.companyDetails.filedBy=Sent by
 submitSummary.contactDetails.heading=Contact details
 submitSummary.contactDetails.change=Change
 submitSummary.contactDetails.name=Your name
-submitSummary.contactDetails.phone=Your phone number
+submitSummary.contactDetails.phone=UK telephone number
 submitSummary.contactDetails.email=Your email address
 submitSummary.reportDetails.heading=Report details
 submitSummary.reportDetails.filename=Filename
@@ -272,13 +274,15 @@ submitterInfo.title=Enter your contact details, as the sender of the report
 submitterInfo.fullName=Your name
 submitterInfo.fullName.error=Enter your name
 submitterInfo.fullName.error.header=You have not entered your full name
-submitterInfo.phoneNumber=Your phone number
+submitterInfo.phoneNumber=UK telephone number
 submitterInfo.phoneNumber.hint=Enter numbers only
-submitterInfo.phoneNumber.error.invalid=Enter a valid phone number
+submitterInfo.phoneNumber.error.invalid=Enter a UK telephone number in the right format
 submitterInfo.phoneNumber.error.invalid.header=We did not recognise the phone number
 submitterInfo.phoneNumber.error.invalid.header.l1=Check the phone number and try again
-submitterInfo.phoneNumber.error.empty=Enter your phone number
+submitterInfo.phoneNumber.error.empty=Enter a UK telephone number
 submitterInfo.phoneNumber.error.empty.header=You have not entered your phone number
+submitterInfo.phoneNumber.error.invalid.forbidden.char=Telephone number must only include letters a to z, numbers 0 to 9, round brackets, forward slashes, hyphens, spaces, asterisks and hash signs
+submitterInfo.phoneNumber.error.invalid.plus.sign=Enter your telephone number without a plus sign
 submitterInfo.emailAddress=Your email address
 submitterInfo.emailAddress.error.empty=Enter your email address
 submitterInfo.emailAddress.error.empty.header=You have not entered your email address
diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala
index fb2fb838..a61e3400 100644
--- a/project/AppDependencies.scala
+++ b/project/AppDependencies.scala
@@ -2,9 +2,9 @@ import play.sbt.PlayImport.ws
 import sbt.*
 
 object AppDependencies {
-  val hmrcBootstrapVersion = "9.10.0"
+  val hmrcBootstrapVersion = "9.11.0"
   val mockitoScalaVersion = "1.17.37"
-  val hmrcMongoVersion = "2.5.0"
+  val hmrcMongoVersion = "2.6.0"
   val playVersion = "play-30"
 
   val compile: Seq[ModuleID] = Seq(
@@ -23,10 +23,10 @@ object AppDependencies {
   )
 
   def test(scope: String = "test"): Seq[ModuleID] = Seq(
-    "uk.gov.hmrc"       %% s"bootstrap-test-$playVersion"  % hmrcBootstrapVersion % scope,
-    "uk.gov.hmrc.mongo" %% s"hmrc-mongo-test-$playVersion" % hmrcMongoVersion     % scope,
-    "org.mockito"       %% "mockito-scala"                 % mockitoScalaVersion  % scope,
-    "org.mockito"       %% "mockito-scala-cats"            % mockitoScalaVersion  % scope,
-    "xerces"             % "xercesImpl"                    % "2.12.2"             % scope
+    "uk.gov.hmrc"           %% s"bootstrap-test-$playVersion"  % hmrcBootstrapVersion % scope,
+    "uk.gov.hmrc.mongo"     %% s"hmrc-mongo-test-$playVersion" % hmrcMongoVersion     % scope,
+    "org.mockito"           %% "mockito-scala"                 % mockitoScalaVersion  % scope,
+    "org.mockito"           %% "mockito-scala-cats"            % mockitoScalaVersion  % scope,
+    "xerces"                 % "xercesImpl"                    % "2.12.2"             % scope
   )
 }
diff --git a/project/build.properties b/project/build.properties
index 04267b14..e97b2722 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.9.9
+sbt.version=1.10.10
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 95976c52..f6afbc72 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -11,7 +11,7 @@ addSbtPlugin("uk.gov.hmrc"        %% "sbt-distributables"    % "2.6.0")
 
 addSbtPlugin("org.playframework"  %% "sbt-plugin"            % "3.0.6")
 
-addSbtPlugin("org.scoverage"      %% "sbt-scoverage"         % "2.0.9")
+addSbtPlugin("org.scoverage"      %% "sbt-scoverage"         % "2.3.0")
 
 addSbtPlugin("org.scalameta"      %% "sbt-scalafmt"          % "2.5.2")
 
diff --git a/test/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionSpec.scala b/test/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionSpec.scala
index 15a7b956..615fda48 100644
--- a/test/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionSpec.scala
+++ b/test/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionSpec.scala
@@ -42,6 +42,7 @@ import uk.gov.hmrc.cbcrfrontend.form.SubmitterInfoForm
 import uk.gov.hmrc.cbcrfrontend.model._
 import uk.gov.hmrc.cbcrfrontend.repositories.CBCSessionCache
 import uk.gov.hmrc.cbcrfrontend.services._
+import uk.gov.hmrc.cbcrfrontend.util.CBCRMapping.ukPhoneNumberConstraint
 import uk.gov.hmrc.cbcrfrontend.views.Views
 import uk.gov.hmrc.http.HeaderCarrier
 import uk.gov.hmrc.mongo.cache.CacheItem
@@ -318,6 +319,88 @@ class SubmissionSpec
       status(result) shouldBe Status.BAD_REQUEST
     }
 
+    "return 400 when Contact Phone is empty" in {
+      val submitterInfo = SubmitterInfo("Fullname", None, "", EmailAddress("abc@xyz.com"), None)
+      val dataSeq = Seq(
+        "fullName"           -> submitterInfo.fullName,
+        "contactPhone"       -> "",
+        "agencyBusinessName" -> submitterInfo.agencyBusinessName.toString,
+        "email"              -> submitterInfo.email.toString,
+        "affinityGroup"      -> submitterInfo.affinityGroup.toString
+      )
+      auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
+      cache.read[CBCId](CBCId.cbcIdFormat, *, *) raises ExpiredSession("")
+      val fakeRequestSubmit =
+        addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
+      cache.read[FileDetails](FileDetails.fileDetailsFormat, *, *) returnsF fileDetails
+      val result = call(controller.submitSubmitterInfo, fakeRequestSubmit)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include("Enter a UK telephone number")
+    }
+
+    "return 400 when Contact Phone has + sign" in {
+      val submitterInfo = SubmitterInfo("Fullname", None, "+44123456789", EmailAddress("abc@xyz.com"), None)
+      val dataSeq = Seq(
+        "fullName"           -> submitterInfo.fullName,
+        "contactPhone"       -> submitterInfo.contactPhone,
+        "agencyBusinessName" -> submitterInfo.agencyBusinessName.toString,
+        "email"              -> submitterInfo.email.toString,
+        "affinityGroup"      -> submitterInfo.affinityGroup.toString
+      )
+      auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
+      cache.read[CBCId](CBCId.cbcIdFormat, *, *) raises ExpiredSession("")
+      val fakeRequestSubmit =
+        addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
+      cache.read[FileDetails](FileDetails.fileDetailsFormat, *, *) returnsF fileDetails
+      val result = call(controller.submitSubmitterInfo, fakeRequestSubmit)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include("Enter your telephone number without a plus sign")
+    }
+
+    "return 400 when Contact Phone has forbidden character" in {
+      val submitterInfo = SubmitterInfo("Fullname", None, "$44123456789", EmailAddress("abc@xyz.com"), None)
+      val dataSeq = Seq(
+        "fullName"           -> submitterInfo.fullName,
+        "contactPhone"       -> submitterInfo.contactPhone,
+        "agencyBusinessName" -> submitterInfo.agencyBusinessName.toString,
+        "email"              -> submitterInfo.email.toString,
+        "affinityGroup"      -> submitterInfo.affinityGroup.toString
+      )
+      auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
+      cache.read[CBCId](CBCId.cbcIdFormat, *, *) raises ExpiredSession("")
+      val fakeRequestSubmit =
+        addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
+      cache.read[FileDetails](FileDetails.fileDetailsFormat, *, *) returnsF fileDetails
+      val result = call(controller.submitSubmitterInfo, fakeRequestSubmit)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include(
+        "Telephone number must only include letters a to z, numbers 0 to 9, round brackets, forward slashes, hyphens, spaces, asterisks and hash signs"
+      )
+    }
+
+    "return 400 when Contact Phone is invalid " in {
+      val submitterInfo = SubmitterInfo("Fullname", None, "01642-123456", EmailAddress("abc@xyz.com"), None)
+      val dataSeq = Seq(
+        "fullName"           -> submitterInfo.fullName,
+        "contactPhone"       -> submitterInfo.contactPhone,
+        "agencyBusinessName" -> submitterInfo.agencyBusinessName.toString,
+        "email"              -> submitterInfo.email.toString,
+        "affinityGroup"      -> submitterInfo.affinityGroup.toString
+      )
+      auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
+      cache.read[CBCId](CBCId.cbcIdFormat, *, *) raises ExpiredSession("")
+      val fakeRequestSubmit =
+        addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
+      cache.read[FileDetails](FileDetails.fileDetailsFormat, *, *) returnsF fileDetails
+      val result = call(controller.submitSubmitterInfo, fakeRequestSubmit)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include("Enter a UK telephone number in the right format")
+    }
+
     "return 400 when the all data exists but Email Address" in {
       val submitterInfo = Seq(
         "fullName"     -> "Fullname",
@@ -368,8 +451,14 @@ class SubmissionSpec
     }
 
     "return 303 when all of the data exists & valid" in {
-      val submitterInfo = SubmitterInfo("Fullname", None, "07923456708", EmailAddress("abc@xyz.com"), None)
-      val dataSeq = SubmitterInfoForm.submitterInfoForm.fill(submitterInfo).data.toSeq
+      val submitterInfo = SubmitterInfo("Fullname", None, "07 9234 5670 8", EmailAddress("abc@xyz.com"), None)
+      val dataSeq = SubmitterInfoForm
+        .submitterInfoForm(
+          ukPhoneNumberConstraint
+        )
+        .fill(submitterInfo)
+        .data
+        .toSeq
       val fakeRequestSubmit =
         addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
 
@@ -397,7 +486,13 @@ class SubmissionSpec
       "the AffinityGroup is Organisation it" should {
         "redirect to submit-summary if a cbcId exists" in {
           val submitterInfo = SubmitterInfo("Billy Bob", None, "07923456708", EmailAddress("abc@xyz.com"), None)
-          val dataSeq = SubmitterInfoForm.submitterInfoForm.fill(submitterInfo).data.toSeq
+          val dataSeq = SubmitterInfoForm
+            .submitterInfoForm(
+              ukPhoneNumberConstraint
+            )
+            .fill(submitterInfo)
+            .data
+            .toSeq
           val fakeRequestSubmit =
             addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
           auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
@@ -426,7 +521,13 @@ class SubmissionSpec
 
         "redirect to enter-cbcId if a cbcid does not exist" in {
           val submitterInfo = SubmitterInfo("Billy Bob", None, "07923456708", EmailAddress("abc@xyz.com"), None)
-          val dataSeq = SubmitterInfoForm.submitterInfoForm.fill(submitterInfo).data.toSeq
+          val dataSeq = SubmitterInfoForm
+            .submitterInfoForm(
+              ukPhoneNumberConstraint
+            )
+            .fill(submitterInfo)
+            .data
+            .toSeq
           val fakeRequestSubmit =
             addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
           auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Organisation))
@@ -457,7 +558,13 @@ class SubmissionSpec
       "the AffinityGroup is Agent it" should {
         "redirect to enter-known-facts if a cbcid does not exist" in {
           val submitterInfo = SubmitterInfo("Billy Bob", None, "07923456708", EmailAddress("abc@xyz.com"), None)
-          val dataSeq = SubmitterInfoForm.submitterInfoForm.fill(submitterInfo).data.toSeq
+          val dataSeq = SubmitterInfoForm
+            .submitterInfoForm(
+              ukPhoneNumberConstraint
+            )
+            .fill(submitterInfo)
+            .data
+            .toSeq
           val fakeRequestSubmit =
             addToken(FakeRequest("POST", "/submitSubmitterInfo").withFormUrlEncodedBody(dataSeq: _*))
           auth.authorise[Option[AffinityGroup]](*, *)(*, *) returns Future.successful(Some(AffinityGroup.Agent))
diff --git a/test/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionControllerSpec.scala b/test/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionControllerSpec.scala
index e37a1721..4a3482ba 100644
--- a/test/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionControllerSpec.scala
+++ b/test/uk/gov/hmrc/cbcrfrontend/controllers/SubscriptionControllerSpec.scala
@@ -211,7 +211,6 @@ class SubscriptionControllerSpec
       webPageAsString should include(getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.invalid"))
       webPageAsString should include("There is a problem")
       webPageAsString should not include "found some errors"
-      webPageAsString should not include getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.empty")
     }
 
     "return a custom error message when the phone number is empty" in {
@@ -229,11 +228,72 @@ class SubscriptionControllerSpec
       status(result) shouldBe Status.BAD_REQUEST
       val webPageAsString = contentAsString(result)
       webPageAsString should include(getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.empty"))
-      webPageAsString should include("Enter the phone number")
+      webPageAsString should include("Enter a UK telephone number")
       webPageAsString should not include "found some errors"
       webPageAsString should not include getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.invalid")
     }
 
+    "return a custom error message when the phone number has + sign" in {
+      auth.authorise[Option[Credentials]](*, *)(*, *) returns Future.successful(Some(Credentials("asdf", "gateway")))
+      val data = Seq(
+        "phoneNumber" -> "+44 123456789",
+        "firstName"   -> "Dave",
+        "lastName"    -> "Jones",
+        "email"       -> "blagh@blagh.com"
+      )
+      val fakeRequest = FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*)
+      val fakeRequestSubscribe =
+        addToken(FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*))
+      val result = controller.submitSubscriptionData(fakeRequestSubscribe)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include(
+        getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.invalid.plus.sign")
+      )
+      webPageAsString should include("There is a problem")
+      webPageAsString should not include "found some errors"
+    }
+
+    "return a custom error message when the phone number is Invalid" in {
+      auth.authorise[Option[Credentials]](*, *)(*, *) returns Future.successful(Some(Credentials("asdf", "gateway")))
+      val data = Seq(
+        "phoneNumber" -> "01642-123456",
+        "firstName"   -> "Dave",
+        "lastName"    -> "Jones",
+        "email"       -> "blagh@blagh.com"
+      )
+      val fakeRequest = FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*)
+      val fakeRequestSubscribe =
+        addToken(FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*))
+      val result = controller.submitSubscriptionData(fakeRequestSubscribe)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include(getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.invalid"))
+      webPageAsString should include("There is a problem")
+      webPageAsString should not include "found some errors"
+    }
+
+    "return a custom error message when the phone number has forbidden characters" in {
+      auth.authorise[Option[Credentials]](*, *)(*, *) returns Future.successful(Some(Credentials("asdf", "gateway")))
+      val data = Seq(
+        "phoneNumber" -> "$44123456789",
+        "firstName"   -> "Dave",
+        "lastName"    -> "Jones",
+        "email"       -> "blagh@blagh.com"
+      )
+      val fakeRequest = FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*)
+      val fakeRequestSubscribe =
+        addToken(FakeRequest("POST", "/submitSubscriptionData").withFormUrlEncodedBody(data: _*))
+      val result = controller.submitSubscriptionData(fakeRequestSubscribe)
+      status(result) shouldBe Status.BAD_REQUEST
+      val webPageAsString = contentAsString(result)
+      webPageAsString should include(
+        getMessages(fakeRequest)("contactInfoSubscriber.phoneNumber.error.invalid.forbidden.char")
+      )
+      webPageAsString should include("There is a problem")
+      webPageAsString should not include "found some errors"
+    }
+
     "return a custom error message when the email  is invalid" in {
       auth.authorise[Option[Credentials]](*, *)(*, *) returns Future.successful(Some(Credentials("asdf", "gateway")))
       val data = Seq(


Reviews:
user: login: mahesh-hmrc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:30:42Z
user: login: mahesh-hmrc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:16:06Z
user: login: mahesh-hmrc
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:50:19Z

Review comments:
diff_hunk: @@ -361,9 +362,29 @@ class SubmissionController @Inject() (
       form <- cache.readOption[SubmitterInfo].map { osi =>
                 (osi.map(_.fullName), osi.map(_.contactPhone), osi.map(_.email))
                   .mapN { (name, phone, email) =>
-                    submitterInfoForm.bind(Map("fullName" -> name, "contactPhone" -> phone, "email" -> email.value))
+                    submitterInfoForm(
path: app/uk/gov/hmrc/cbcrfrontend/controllers/SubmissionController.scala
user: login: mahesh-hmrc
body: It would be good to set this constraint as default in the form itself. And, we can override anything that doesn't fit this, for ex, the read handling to allow legacy datasets. As we do here https://github.com/hmrc/cbcr-frontend/pull/672/files#diff-9d78ee902afd6ebf53d147159bfacb925e0a8e235e55c7fd1bc819a715eaf007R176
created_at: 2025-03-24T10:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+/*
+ * 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 uk.gov.hmrc.cbcrfrontend.util
+
+import play.api.data.Forms.text
+import play.api.data.Mapping
+import play.api.data.validation.{Constraint, Invalid, Valid}
+
+object CBCRMapping {
+
+  def validatePhoneNumber(constraint: Constraint[String]): Mapping[String] =
+    text.verifying(constraint)
+
+  val ukPhoneNumberConstraint: Constraint[String] =
+    Constraint {
+      case text if text.isBlank       => Invalid("submitterInfo.phoneNumber.error.empty")
+      case text if text.contains("+") => Invalid("submitterInfo.phoneNumber.error.invalid.plus.sign")
+      case text if text.exists(c => "@!$%^&_=~{}[]:".contains(c)) =>
+        Invalid("submitterInfo.phoneNumber.error.invalid.forbidden.char")
+      case text if !text.matches("^[A-Z0-9 )/(-*#]+$") =>
+        Invalid("submitterInfo.phoneNumber.error.invalid")
+      case _ => Valid
+    }
+
+  def phoneNumberConstraint(emptyError: String, invalidError: String): Constraint[String] =
path: app/uk/gov/hmrc/cbcrfrontend/util/CBCRMapping.scala
user: login: mahesh-hmrc
body: For consistency and to simplify, it would be good inline the empty/invalid error messages aswell into the partial match block below. Reason being, both the usages of `phoneNumberConstraint` are sending the same messages. So, no need to have them parameterised here. Thanks
created_at: 2025-03-24T17:16:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: platops-pr-bot
created_at: 2025-03-17T14:44:08Z
body: - `uk.gov.hmrc.sbt-distributables` can be updated to `2.6.0` in [project/plugins.sbt](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/plugins.sbt#L10)
- `uk.gov.hmrc:bootstrap-frontend-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
- `uk.gov.hmrc:play-frontend-hmrc-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
- `uk.gov.hmrc.mongo:hmrc-mongo-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
- `uk.gov.hmrc:bootstrap-test-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
- `uk.gov.hmrc.mongo:hmrc-mongo-test-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
author_association: NONE
user: login: johno1985
created_at: 2025-03-17T16:56:27Z
body: Got some AT failures locally.
Also, can some of the PR bot comments be addressed?
author_association: CONTRIBUTOR
user: login: platops-pr-bot
created_at: 2025-03-20T17:03:11Z
body: - ~~`uk.gov.hmrc.sbt-distributables` can be updated to `2.6.0` in [project/plugins.sbt](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/plugins.sbt#L10)~~
- ~~`uk.gov.hmrc:bootstrap-frontend-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc:play-frontend-hmrc-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc:bootstrap-test-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-test-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- `SBT` can be updated to `1.10.10` in [project/build.properties](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/build.properties#L1)
- `Scala` can be updated from `2.13.12` to `2.13.16` in [build.sbt](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/build.sbt#L16)
- `uk.gov.hmrc.mongo:hmrc-mongo-play-30` can be updated to `2.6.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
- `uk.gov.hmrc.mongo:hmrc-mongo-test-play-30` can be updated to `2.6.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).
author_association: NONE
user: login: platops-pr-bot
created_at: 2025-03-20T17:17:25Z
body: - ~~`uk.gov.hmrc.sbt-distributables` can be updated to `2.6.0` in [project/plugins.sbt](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/plugins.sbt#L10)~~
- ~~`uk.gov.hmrc:bootstrap-frontend-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc:play-frontend-hmrc-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc:bootstrap-test-play-30` can be updated to `9.11.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-test-play-30` can be updated to `2.5.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`SBT` can be updated to `1.10.10` in [project/build.properties](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/build.properties#L1)~~
- ~~`Scala` can be updated from `2.13.12` to `2.13.16` in [build.sbt](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/build.sbt#L16)~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-play-30` can be updated to `2.6.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
- ~~`uk.gov.hmrc.mongo:hmrc-mongo-test-play-30` can be updated to `2.6.0` in [project/AppDependencies.scala](https://github.com/hmrc/cbcr-frontend/blob/Shivangi%2FDLS-11367_phone_validation/project/AppDependencies.scala#L1).~~
author_association: NONE