diff --git a/app/controllers/YourBankDetailsController.scala b/app/controllers/YourBankDetailsController.scala index 7894af90..a2c3ea91 100644 --- a/app/controllers/YourBankDetailsController.scala +++ b/app/controllers/YourBankDetailsController.scala @@ -92,6 +92,9 @@ class YourBankDetailsController @Inject() ( } } + private def normaliseSortCode(value: String): String = + value.replaceAll("[\\s-]", "") + private def startVerification( accountType: PersonalOrBusinessAccount, bankDetails: YourBankDetails, @@ -100,7 +103,10 @@ class YourBankDetailsController @Inject() ( mode: Mode )(implicit hc: HeaderCarrier, request: DataRequest[?]): Future[Result] = { - barsService.barsVerification(accountType.toString, bankDetails).flatMap { + val bankDetailsForBars = + bankDetails.copy(sortCode = normaliseSortCode(bankDetails.sortCode)) + + barsService.barsVerification(accountType.toString, bankDetailsForBars).flatMap { case Right((verificationResponse, bank)) => onSuccessfulVerification( userAnswers, diff --git a/app/forms/YourBankDetailsFormProvider.scala b/app/forms/YourBankDetailsFormProvider.scala index 10b0dd3c..ffc585ce 100644 --- a/app/forms/YourBankDetailsFormProvider.scala +++ b/app/forms/YourBankDetailsFormProvider.scala @@ -20,6 +20,7 @@ import forms.mappings.Mappings import models.YourBankDetails import play.api.data.Form import play.api.data.Forms.* +import play.api.data.validation.{Constraint, Invalid, Valid} import javax.inject.Inject @@ -29,19 +30,32 @@ class YourBankDetailsFormProvider @Inject() extends Mappings { val MAX_SORT_CODE_LENGTH = 6 val MAX_ACCOUNT_NUMBER_LENGTH = 8 + private def normaliseSortCode(value: String): String = + value.replaceAll("[\\s-]", "") + + private val sortCodeConstraint: Constraint[String] = + Constraint("constraints.sortCode") { value => + val normalised = normaliseSortCode(value) + + if (normalised.length < MAX_SORT_CODE_LENGTH) { + Invalid("yourBankDetails.error.sortCode.tooShort", MAX_SORT_CODE_LENGTH) + } else if (normalised.length > MAX_SORT_CODE_LENGTH) { + Invalid("yourBankDetails.error.sortCode.length", MAX_SORT_CODE_LENGTH) + } else if (!normalised.forall(_.isDigit)) { + Invalid("yourBankDetails.error.sortCode.numericOnly") + } else { + Valid + } + } + def apply(): Form[YourBankDetails] = Form( mapping( "accountHolderName" -> text("yourBankDetails.error.accountHolderName.required") .verifying(maxLength(MAX_ACCOUNT_HOLDER_NAME_LENGTH, "yourBankDetails.error.accountHolderName.length")), "sortCode" -> text("yourBankDetails.error.sortCode.required") - .verifying( - firstError( - minLength(MAX_SORT_CODE_LENGTH, "yourBankDetails.error.sortCode.tooShort"), - maxLength(MAX_SORT_CODE_LENGTH, "yourBankDetails.error.sortCode.length"), - regexp(NumericRegex, "yourBankDetails.error.sortCode.numericOnly") - ) - ), + .verifying(sortCodeConstraint), "accountNumber" -> text("yourBankDetails.error.accountNumber.required") + .transform[String](value => value.replaceAll("\\s", ""), identity) .verifying( firstError( minLength(MAX_ACCOUNT_NUMBER_LENGTH, "yourBankDetails.error.accountNumber.tooShort"), diff --git a/app/models/requests/ChrisSubmissionRequest.scala b/app/models/requests/ChrisSubmissionRequest.scala index 73a6b28c..c4e15661 100644 --- a/app/models/requests/ChrisSubmissionRequest.scala +++ b/app/models/requests/ChrisSubmissionRequest.scala @@ -81,12 +81,16 @@ object ChrisSubmissionRequest { case Some(existingDd) => YourBankDetailsWithAuddisStatus( accountHolderName = existingDd.bankAccountName, - sortCode = existingDd.bankSortCode, + sortCode = existingDd.bankSortCode.replaceAll("[\\s-]", ""), accountNumber = existingDd.bankAccountNumber, auddisStatus = existingDd.auDdisFlag, accountVerified = true ) - case _ => required(YourBankDetailsPage) + case _ => + val existing = required(YourBankDetailsPage) + existing.copy( + sortCode = existing.sortCode.replaceAll("[\\s-]", "") + ) } ChrisSubmissionRequest( diff --git a/app/services/BarsService.scala b/app/services/BarsService.scala index a3cf2e80..43abc4f6 100644 --- a/app/services/BarsService.scala +++ b/app/services/BarsService.scala @@ -76,7 +76,6 @@ case class BarsService @Inject() ( def barsVerification(personalOrBusiness: String, bankDetails: YourBankDetails)(implicit hc: HeaderCarrier ): Future[Either[BarsErrors, (BarsVerificationResponse, Bank)]] = { - val (endpoint, requestJson) = if (personalOrBusiness.toLowerCase == "personal") { "personal" -> Json.toJson( BarsPersonalRequest( @@ -93,7 +92,6 @@ case class BarsService @Inject() ( ) } - // Call BARS and map known errors val verificationFuture: Future[Either[BarsErrors, BarsVerificationResponse]] = barsConnector.verify(endpoint, requestJson).map(Right(_)).recover { case e: UpstreamBarsException if e.status == 400 && e.errorCode.contains("SORT_CODE_ON_DENY_LIST") => diff --git a/app/viewmodels/checkAnswers/YourBankDetailsSortCodeSummary.scala b/app/viewmodels/checkAnswers/YourBankDetailsSortCodeSummary.scala index 9ea367fe..4d5d87a2 100644 --- a/app/viewmodels/checkAnswers/YourBankDetailsSortCodeSummary.scala +++ b/app/viewmodels/checkAnswers/YourBankDetailsSortCodeSummary.scala @@ -35,7 +35,7 @@ object YourBankDetailsSortCodeSummary { SummaryListRowViewModel( key = "bankDetailsCheckYourAnswer.account.sort.code", - value = ValueViewModel(HtmlContent(value)), + value = ValueViewModel(HtmlContent(value.replaceAll("[\\s-]", ""))), actions = Seq( ActionItemViewModel("site.change", routes.YourBankDetailsController.onPageLoad(CheckMode).url + "#sortCode") .withVisuallyHiddenText(messages("bankDetailsCheckYourAnswer.account.sort.code")) diff --git a/test/forms/YourBankDetailsFormProviderSpec.scala b/test/forms/YourBankDetailsFormProviderSpec.scala index 91060477..7986fa98 100644 --- a/test/forms/YourBankDetailsFormProviderSpec.scala +++ b/test/forms/YourBankDetailsFormProviderSpec.scala @@ -81,11 +81,51 @@ class YourBankDetailsFormProviderSpec extends StringFieldBehaviours { result.errors must contain only FormError(fieldName, tooShortKey, Seq(maxLength)) } + "bind valid sort code with multiple hyphens" in { + val bound = form.bind( + Map( + "accountHolderName" -> "John Doe", + "sortCode" -> "20--71--02", + "accountNumber" -> "12345678" + ) + ) + + bound.errors mustBe empty + bound.value.get.sortCode mustBe "20--71--02" + } + "not bind non-numeric input" in { val result = form.bind(Map(fieldName -> "12A456")).apply(fieldName) result.errors.exists(_.message == numericOnlyKey) mustBe true } + "bind valid sort code with spaces or hyphens" in { + val bound = form.bind( + Map( + "accountHolderName" -> "John Doe", + "sortCode" -> "12-34-56", + "accountNumber" -> "12345678" + ) + ) + + bound.errors mustBe empty + bound.value.get.sortCode mustBe "12-34-56" + } + + "bind valid sort code with spaces" in { + val bound = form.bind( + Map( + "accountHolderName" -> "John Doe", + "sortCode" -> " 12 34 56 ", + "accountNumber" -> "12345678" + ) + ) + + bound.errors mustBe empty + // Form now preserves raw input (including surrounding spaces) + bound.value.get.sortCode mustBe " 12 34 56 " + } + behave like mandatoryField( form, fieldName, @@ -125,6 +165,19 @@ class YourBankDetailsFormProviderSpec extends StringFieldBehaviours { result.errors.exists(_.message == numericOnlyKey) mustBe true } + "bind valid account number with spaces" in { + val bound = form.bind( + Map( + "accountHolderName" -> "John Doe", + "sortCode" -> "123456", + "accountNumber" -> " 12 34 56 78 " + ) + ) + + bound.errors mustBe empty + bound.value.get.accountNumber mustBe "12345678" + } + behave like mandatoryField( form, fieldName,