diff --git a/a11y/pages/bankdetails/BarsBankAccountDetailsA11ySpec.scala b/a11y/pages/bankdetails/BarsBankAccountDetailsA11ySpec.scala new file mode 100644 index 000000000..f59b50d8f --- /dev/null +++ b/a11y/pages/bankdetails/BarsBankAccountDetailsA11ySpec.scala @@ -0,0 +1,28 @@ + +package pages.bankdetails + +import forms.EnterBankAccountDetailsForm +import helpers.A11ySpec +import models.BankAccountDetails +import play.api.data.Form +import views.html.bankdetails.EnterBankAccountDetails + +class BarsBankAccountDetailsA11ySpec extends A11ySpec { + + val view: EnterBankAccountDetails = app.injector.instanceOf[EnterBankAccountDetails] + val form: Form[BankAccountDetails] = EnterBankAccountDetailsForm.form + + "the Enter Company Bank Account Details page" when { + "there are no form errors" must { + "pass all a11y checks" in { + view(form).body must passAccessibilityChecks + } + } + "there are form errors" must { + "pass all a11y checks" in { + view(form.bind(Map("" -> ""))).body must passAccessibilityChecks + } + } + } + +} \ No newline at end of file diff --git a/app/config/startup/VerifyCrypto.scala b/app/config/startup/VerifyCrypto.scala index c074d4215..e0ee6cc0b 100644 --- a/app/config/startup/VerifyCrypto.scala +++ b/app/config/startup/VerifyCrypto.scala @@ -16,7 +16,7 @@ package config.startup -import uk.gov.hmrc.crypto.ApplicationCrypto +import uk.gov.hmrc.play.bootstrap.frontend.filters.crypto.ApplicationCrypto import javax.inject.{Inject, Singleton} diff --git a/app/controllers/bankdetails/ChooseAccountTypeController.scala b/app/controllers/bankdetails/ChooseAccountTypeController.scala index c0d76ead0..1de0d142d 100644 --- a/app/controllers/bankdetails/ChooseAccountTypeController.scala +++ b/app/controllers/bankdetails/ChooseAccountTypeController.scala @@ -39,7 +39,7 @@ class ChooseAccountTypeController @Inject() (val authConnector: AuthClientConnec def show: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile => if (isEnabled(UseNewBarsVerify)) { - bankAccountDetailsService.fetchBankAccountDetails.map { bankDetails => + bankAccountDetailsService.getBankAccount.map { bankDetails => val filledForm = bankDetails .flatMap(_.bankAccountType) .fold(ChooseAccountTypeForm.form)(ChooseAccountTypeForm.form.fill) diff --git a/app/controllers/bankdetails/HasBankAccountController.scala b/app/controllers/bankdetails/HasBankAccountController.scala index d843e2615..704846c66 100644 --- a/app/controllers/bankdetails/HasBankAccountController.scala +++ b/app/controllers/bankdetails/HasBankAccountController.scala @@ -45,7 +45,7 @@ class HasBankAccountController @Inject()(val authConnector: AuthClientConnector, Future.successful(Redirect(controllers.flatratescheme.routes.JoinFlatRateSchemeController.show)) case _ => for { - bankDetails <- bankAccountDetailsService.fetchBankAccountDetails + bankDetails <- bankAccountDetailsService.getBankAccount filledForm = bankDetails.map(_.isProvided).fold(hasBankAccountForm)(hasBankAccountForm.fill) } yield Ok(view(filledForm)) } diff --git a/app/controllers/bankdetails/NoUKBankAccountController.scala b/app/controllers/bankdetails/NoUKBankAccountController.scala index 24d230e51..0a2639ee9 100644 --- a/app/controllers/bankdetails/NoUKBankAccountController.scala +++ b/app/controllers/bankdetails/NoUKBankAccountController.scala @@ -40,7 +40,7 @@ class NoUKBankAccountController @Inject()(noUKBankAccountView: NoUkBankAccount, implicit request => implicit profile => for { - optBankAccountDetails <- bankAccountDetailsService.fetchBankAccountDetails + optBankAccountDetails <- bankAccountDetailsService.getBankAccount form = optBankAccountDetails.flatMap(_.reason).fold(NoUKBankAccountForm.form)(NoUKBankAccountForm.form.fill) } yield Ok(noUKBankAccountView(form)) } diff --git a/app/controllers/bankdetails/UkBankAccountDetailsController.scala b/app/controllers/bankdetails/UkBankAccountDetailsController.scala index 4a7b2c48e..042c75138 100644 --- a/app/controllers/bankdetails/UkBankAccountDetailsController.scala +++ b/app/controllers/bankdetails/UkBankAccountDetailsController.scala @@ -18,49 +18,80 @@ package controllers.bankdetails import config.{AuthClientConnector, BaseControllerComponents, FrontendAppConfig} import controllers.BaseController +import featuretoggle.FeatureSwitch.UseNewBarsVerify +import featuretoggle.FeatureToggleSupport.isEnabled import forms.EnterBankAccountDetailsForm import forms.EnterBankAccountDetailsForm.{form => enterBankAccountDetailsForm} +import forms.EnterBankAccountDetailsNewBarsForm +import models.BankAccountDetails +import models.bars.BankAccountDetailsSessionFormat +import play.api.libs.json.Format import play.api.mvc.{Action, AnyContent} +import play.api.Configuration import services.{BankAccountDetailsService, SessionService} +import uk.gov.hmrc.crypto.SymmetricCryptoFactory import views.html.bankdetails.EnterCompanyBankAccountDetails +import views.html.bankdetails.EnterBankAccountDetails import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} -class UkBankAccountDetailsController @Inject()(val authConnector: AuthClientConnector, - val bankAccountDetailsService: BankAccountDetailsService, - val sessionService: SessionService, - view: EnterCompanyBankAccountDetails) - (implicit appConfig: FrontendAppConfig, - val executionContext: ExecutionContext, - baseControllerComponents: BaseControllerComponents) extends BaseController { +class UkBankAccountDetailsController @Inject() ( + val authConnector: AuthClientConnector, + val bankAccountDetailsService: BankAccountDetailsService, + val sessionService: SessionService, + configuration: Configuration, + newBarsView: EnterBankAccountDetails, + oldView: EnterCompanyBankAccountDetails + )(implicit appConfig: FrontendAppConfig, val executionContext: ExecutionContext, baseControllerComponents: BaseControllerComponents) + extends BaseController { - def show: Action[AnyContent] = isAuthenticatedWithProfile { - implicit request => - implicit profile => - for { - bankDetails <- bankAccountDetailsService.fetchBankAccountDetails - filledForm = bankDetails.flatMap(_.details).fold(enterBankAccountDetailsForm)(enterBankAccountDetailsForm.fill) - } yield Ok(view(filledForm)) + private val encrypter = + SymmetricCryptoFactory.aesCryptoFromConfig("json.encryption", configuration.underlying) + + private implicit val encryptedFormat: Format[BankAccountDetails] = + BankAccountDetailsSessionFormat.format(encrypter) + + private val sessionKey = "bankAccountDetails" + + def show: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile => + if (isEnabled(UseNewBarsVerify)) { + val newBarsForm = EnterBankAccountDetailsNewBarsForm.form + sessionService.fetchAndGet[BankAccountDetails](sessionKey).map { + case Some(details) => Ok(newBarsView(newBarsForm.fill(details))) + case None => Ok(newBarsView(newBarsForm)) + } + } else { + for { + bankDetails <- bankAccountDetailsService.getBankAccount + filledForm = bankDetails.flatMap(_.details).fold(enterBankAccountDetailsForm)(enterBankAccountDetailsForm.fill) + } yield Ok(oldView(filledForm)) + } } - def submit: Action[AnyContent] = isAuthenticatedWithProfile { - implicit request => - implicit profile => - enterBankAccountDetailsForm.bindFromRequest().fold( - formWithErrors => - Future.successful(BadRequest(view(formWithErrors))), + def submit: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile => + if (isEnabled(UseNewBarsVerify)) { + val newBarsForm = EnterBankAccountDetailsNewBarsForm.form + newBarsForm + .bindFromRequest() + .fold( + formWithErrors => Future.successful(BadRequest(newBarsView(formWithErrors))), accountDetails => - bankAccountDetailsService.saveEnteredBankAccountDetails(accountDetails).map { accountDetailsValid => - if (accountDetailsValid) { - Redirect(controllers.routes.TaskListController.show.url) - } - else { - val invalidDetails = EnterBankAccountDetailsForm.formWithInvalidAccountReputation.fill(accountDetails) - BadRequest(view(invalidDetails)) - } + sessionService.cache[BankAccountDetails](sessionKey, accountDetails).map { _ => + Redirect(controllers.routes.TaskListController.show.url) } ) + } else { + enterBankAccountDetailsForm + .bindFromRequest() + .fold( + formWithErrors => Future.successful(BadRequest(oldView(formWithErrors))), + accountDetails => + bankAccountDetailsService.saveEnteredBankAccountDetails(accountDetails).map { + case true => Redirect(controllers.routes.TaskListController.show.url) + case false => BadRequest(oldView(EnterBankAccountDetailsForm.formWithInvalidAccountReputation.fill(accountDetails))) + } + ) + } } - -} +} \ No newline at end of file diff --git a/app/forms/BankAccountDetailsForms.scala b/app/forms/BankAccountDetailsForms.scala index bfe9a8f77..35054cfaa 100644 --- a/app/forms/BankAccountDetailsForms.scala +++ b/app/forms/BankAccountDetailsForms.scala @@ -40,12 +40,15 @@ object ChooseAccountTypeForm { val form: Form[BankAccountType] = Form( single( - BUSINESS_OR_PERSONAL_ACCOUNT_RADIO -> default(text, "").verifying(stopOnFail( - mandatory(errorMsg) - )).transform[BankAccountType]( - s => BankAccountType.fromString(s).get, - _.asBars - ) + BUSINESS_OR_PERSONAL_ACCOUNT_RADIO -> default(text, "") + .verifying( + stopOnFail( + mandatory(errorMsg) + )) + .transform[BankAccountType]( + s => BankAccountType.fromString(s).get, + _.asBars + ) ) ) } @@ -95,12 +98,72 @@ object EnterBankAccountDetailsForm { matchesRegex(sortCodeRegex, sortCodeInvalidKey) )) )((accountName, accountNumber, sortCode) => BankAccountDetails.apply(accountName, accountNumber, sortCode, None))(bankAccountDetails => - BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, _) => + BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, _, _) => (accountName, accountNumber, sortCode) }) ) - val formWithInvalidAccountReputation: Form[BankAccountDetails] = form.withError(invalidAccountReputationKey, invalidAccountReputationMessage) +} + +object EnterBankAccountDetailsNewBarsForm { + val ACCOUNT_NAME = "accountName" + val ACCOUNT_NUMBER = "accountNumber" + val SORT_CODE = "sortCode" + val ROLL_NUMBER = "rollNumber" + + val accountNameEmptyKey = "validation.companyBankAccount.name.missing.new" + val accountNameMaxLengthKey = "validation.companyBankAccount.name.maxLength" + val accountNameInvalidKey = "validation.companyBankAccount.name.invalid.new" + val accountNumberEmptyKey = "validation.companyBankAccount.number.missing" + val accountNumberDigitErrorKey = "validation.companyBankAccount.number.digit.error" + val accountNumberInvalidKey = "validation.companyBankAccount.number.invalid" + val sortCodeEmptyKey = "validation.companyBankAccount.sortCode.missing" + val sortCodeInvalidKey = "validation.companyBankAccount.sortCode.invalid" + val sortCodeDigitErrorKey = "validation.companyBankAccount.sortCode.digit.error" + val rollNumberInvalidKey = "validation.companyBankAccount.rollNumber.invalid" + + private val accountNameRegex = """^[A-Za-z0-9 '\-./]{1,60}$""".r + private val accountNameMaxLength = 60 + private val rollNumberMaxLength = 25 + private val accountNumberDigitRegex = """^[0-9]+$""".r + private val accountNumberLengthRegex = """^.{6,8}$""".r + private val sortCodeDigitRegex = """^[0-9]+$""".r + private val sortCodeLengthRegex = """^.{6}$""".r + + val form: Form[BankAccountDetails] = Form[BankAccountDetails]( + mapping( + ACCOUNT_NAME -> text.verifying( + stopOnFail( + mandatory(accountNameEmptyKey), + maxLength(accountNameMaxLength, accountNameMaxLengthKey), + matchesRegex(accountNameRegex, accountNameInvalidKey) + )), + ACCOUNT_NUMBER -> text + .transform(removeSpaces, identity[String]) + .verifying(stopOnFail( + mandatory(accountNumberEmptyKey), + matchesRegex(accountNumberDigitRegex, accountNumberDigitErrorKey), + matchesRegex(accountNumberLengthRegex, accountNumberInvalidKey) + )), + SORT_CODE -> text + .transform(removeSpaces, identity[String]) + .verifying( + stopOnFail( + mandatory(sortCodeEmptyKey), + matchesRegex(sortCodeDigitRegex, sortCodeDigitErrorKey), + matchesRegex(sortCodeLengthRegex, sortCodeInvalidKey) + )), + ROLL_NUMBER -> optional( + text + .transform(removeSpaces, identity[String]) + .verifying(maxLength(rollNumberMaxLength, rollNumberInvalidKey)) + ) + )((accountName, accountNumber, sortCode, rollNumber) => BankAccountDetails.apply(accountName, accountNumber, sortCode, rollNumber))( + bankAccountDetails => + BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, rollNumber, _) => + (accountName, accountNumber, sortCode, rollNumber) + }) + ) } diff --git a/app/models/BankAccountDetails.scala b/app/models/BankAccountDetails.scala index 45dae9ea0..2551f4c1d 100644 --- a/app/models/BankAccountDetails.scala +++ b/app/models/BankAccountDetails.scala @@ -26,7 +26,11 @@ case class BankAccount(isProvided: Boolean, reason: Option[NoUKBankAccount], bankAccountType: Option[BankAccountType] = None) -case class BankAccountDetails(name: String, number: String, sortCode: String, status: Option[BankAccountDetailsStatus] = None) +case class BankAccountDetails(name: String, + number: String, + sortCode: String, + rollNumber: Option[String] = None, + status: Option[BankAccountDetailsStatus] = None) object BankAccountDetails { implicit val accountReputationWrites: OWrites[BankAccountDetails] = new OWrites[BankAccountDetails] { @@ -42,10 +46,11 @@ object BankAccountDetails { def bankSeq(bankAccount: BankAccountDetails): Seq[String] = Seq( - bankAccount.name, - bankAccount.number, - bankAccount.sortCode - ) + Some(bankAccount.name), + Some(bankAccount.number), + Some(bankAccount.sortCode), + bankAccount.rollNumber + ).flatten } object BankAccount { diff --git a/app/models/bars/BankAccountDetailsSessionFormat.scala b/app/models/bars/BankAccountDetailsSessionFormat.scala new file mode 100644 index 000000000..96a86316f --- /dev/null +++ b/app/models/bars/BankAccountDetailsSessionFormat.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.bars + +import models.api.BankAccountDetailsStatus +import models.BankAccountDetails +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import uk.gov.hmrc.crypto.{Decrypter, Encrypter} +import uk.gov.hmrc.crypto.Sensitive.SensitiveString +import uk.gov.hmrc.crypto.json.JsonEncryption + +object BankAccountDetailsSessionFormat { + + def format(encrypter: Encrypter with Decrypter): Format[BankAccountDetails] = { + implicit val crypto: Encrypter with Decrypter = encrypter + + implicit val sensitiveStringFormat: Format[SensitiveString] = + JsonEncryption.sensitiveEncrypterDecrypter(SensitiveString.apply) + + ( + (__ \ "name").format[String] and + (__ \ "sortCode").format[SensitiveString] + .bimap(_.decryptedValue, SensitiveString.apply) and + (__ \ "number").format[SensitiveString] + .bimap(_.decryptedValue, SensitiveString.apply) and + (__ \ "rollNumber").formatNullable[String] and + (__ \ "status").formatNullable[BankAccountDetailsStatus] + )(BankAccountDetails.apply, unlift(BankAccountDetails.unapply)) + } +} \ No newline at end of file diff --git a/app/services/BankAccountDetailsService.scala b/app/services/BankAccountDetailsService.scala index 4cf669d95..63b0b736e 100644 --- a/app/services/BankAccountDetailsService.scala +++ b/app/services/BankAccountDetailsService.scala @@ -18,30 +18,29 @@ package services import connectors.RegistrationApiConnector import models._ -import models.bars.BankAccountType import models.api.{IndeterminateStatus, InvalidStatus, ValidStatus} +import models.bars.BankAccountType +import play.api.mvc.Request import uk.gov.hmrc.http.HeaderCarrier import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} -import play.api.mvc.Request @Singleton -class BankAccountDetailsService @Inject()(val regApiConnector: RegistrationApiConnector, - val bankAccountRepService: BankAccountReputationService) { +class BankAccountDetailsService @Inject() (val regApiConnector: RegistrationApiConnector, val bankAccountRepService: BankAccountReputationService) { - def fetchBankAccountDetails(implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[Option[BankAccount]] = { + def getBankAccount(implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[Option[BankAccount]] = regApiConnector.getSection[BankAccount](profile.registrationId) - } - def saveBankAccountDetails(bankAccount: BankAccount) - (implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[BankAccount] = { + def saveBankAccount(bankAccount: BankAccount)(implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[BankAccount] = regApiConnector.replaceSection[BankAccount](profile.registrationId, bankAccount) - } - def saveHasCompanyBankAccount(hasBankAccount: Boolean) - (implicit hc: HeaderCarrier, profile: CurrentProfile, ex: ExecutionContext, request: Request[_]): Future[BankAccount] = { - val bankAccount = fetchBankAccountDetails map { + def saveHasCompanyBankAccount(hasBankAccount: Boolean)(implicit + hc: HeaderCarrier, + profile: CurrentProfile, + ex: ExecutionContext, + request: Request[_]): Future[BankAccount] = { + val bankAccount = getBankAccount map { case Some(BankAccount(oldHasBankAccount, _, _, _)) if oldHasBankAccount != hasBankAccount => BankAccount(hasBankAccount, None, None, None) case Some(bankAccountDetails) => @@ -50,43 +49,49 @@ class BankAccountDetailsService @Inject()(val regApiConnector: RegistrationApiCo BankAccount(hasBankAccount, None, None, None) } - bankAccount flatMap saveBankAccountDetails + bankAccount flatMap saveBankAccount } - def saveEnteredBankAccountDetails(accountDetails: BankAccountDetails) - (implicit hc: HeaderCarrier, profile: CurrentProfile, ex: ExecutionContext, request: Request[_]): Future[Boolean] = { + def saveEnteredBankAccountDetails(accountDetails: BankAccountDetails)(implicit + hc: HeaderCarrier, + profile: CurrentProfile, + ex: ExecutionContext, + request: Request[_]): Future[Boolean] = for { - existing <- fetchBankAccountDetails - result <- bankAccountRepService.validateBankDetails(accountDetails).flatMap { - case status@(ValidStatus | IndeterminateStatus) => + existing <- getBankAccount + result <- bankAccountRepService.validateBankDetails(accountDetails).flatMap { + case status @ (ValidStatus | IndeterminateStatus) => val bankAccount = BankAccount( isProvided = true, details = Some(accountDetails.copy(status = Some(status))), reason = None, bankAccountType = existing.flatMap(_.bankAccountType) ) - saveBankAccountDetails(bankAccount) map (_ => true) + saveBankAccount(bankAccount) map (_ => true) case InvalidStatus => Future.successful(false) } } yield result - } - def saveNoUkBankAccountDetails(reason: NoUKBankAccount) - (implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[BankAccount] = { + def saveNoUkBankAccountDetails( + reason: NoUKBankAccount)(implicit hc: HeaderCarrier, profile: CurrentProfile, request: Request[_]): Future[BankAccount] = { val bankAccount = BankAccount( isProvided = false, details = None, reason = Some(reason), bankAccountType = None ) - saveBankAccountDetails(bankAccount) + saveBankAccount(bankAccount) } - def saveBankAccountType(bankAccountType: BankAccountType) - (implicit hc: HeaderCarrier, profile: CurrentProfile, ex: ExecutionContext, request: Request[_]): Future[BankAccount] = { - fetchBankAccountDetails.map { - case Some(existing) => existing.copy(bankAccountType = Some(bankAccountType)) - case None => BankAccount(isProvided = true, details = None, reason = None, bankAccountType = Some(bankAccountType)) - }.flatMap(saveBankAccountDetails) - } + def saveBankAccountType(bankAccountType: BankAccountType)(implicit + hc: HeaderCarrier, + profile: CurrentProfile, + ex: ExecutionContext, + request: Request[_]): Future[BankAccount] = + getBankAccount + .map { + case Some(existing) => existing.copy(bankAccountType = Some(bankAccountType)) + case None => BankAccount(isProvided = true, details = None, reason = None, bankAccountType = Some(bankAccountType)) + } + .flatMap(saveBankAccount) } diff --git a/app/views/bankdetails/EnterBankAccountDetails.scala.html b/app/views/bankdetails/EnterBankAccountDetails.scala.html new file mode 100644 index 000000000..7dc220d1c --- /dev/null +++ b/app/views/bankdetails/EnterBankAccountDetails.scala.html @@ -0,0 +1,83 @@ +@* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +@import models.BankAccountDetails +@import play.api.data.Form +@import config.FrontendAppConfig + + +@this( + layout: layouts.layout, + h1: components.h1, + p: components.p, + button: components.button, + formWithCSRF: FormWithCSRF, + errorSummary: components.errorSummary, + panelIndent: components.panelIndent, + inputText: components.inputText +) + +@(bankDetailsForm: Form[BankAccountDetails])(implicit request: Request[_], messages: Messages, appConfig: FrontendAppConfig) + +@layout(pageTitle = Some(title(bankDetailsForm, messages("pages.bankDetails.heading.new")))) { + + @errorSummary(errors = bankDetailsForm.errors) + + @h1("pages.bankDetails.heading.new") + @p { + @messages("pages.bankDetails.p1.new") + } + + @panelIndent { + @messages("pages.bankDetails.info") + } + + @formWithCSRF(action = controllers.bankdetails.routes.UkBankAccountDetailsController.submit) { + @inputText(form = bankDetailsForm, + id = "accountName", + name = "accountName", + label = messages("pages.bankDetails.accountName.label.new"), + classes = Some("govuk-input--width-20"), + isPageHeading = false, + hint = Some(Html(messages("pages.bankDetails.accountName.hint")))) + + @inputText(form = bankDetailsForm, + id = "accountNumber", + name = "accountNumber", + label = messages("pages.bankDetails.accountNumber.label"), + classes = Some("govuk-input--width-10"), + isPageHeading = false, + hint = Some(Html(messages("pages.bankDetails.accountNumber.hint")))) + + @inputText(form = bankDetailsForm, + id = "sortCode", + name = "sortCode", + label = messages("pages.bankDetails.sortCode.label"), + classes = Some("govuk-input--width-5"), + isPageHeading = false, + hint = Some(Html(messages("pages.bankDetails.sortCode.hint")))) + + @inputText(form = bankDetailsForm, + id = "rollNumber", + name = "rollNumber", + label = messages("pages.bankDetails.rollNumber.label"), + classes = Some("govuk-input--width-20"), + isPageHeading = false, + hint = Some(Html(messages("pages.bankDetails.rollNumber.hint")))) + + @button("app.common.continue") + } +} \ No newline at end of file diff --git a/app/views/bankdetails/EnterCompanyBankAccountDetails.scala.html b/app/views/bankdetails/EnterCompanyBankAccountDetails.scala.html index f93cb3b06..b7aedb067 100644 --- a/app/views/bankdetails/EnterCompanyBankAccountDetails.scala.html +++ b/app/views/bankdetails/EnterCompanyBankAccountDetails.scala.html @@ -31,12 +31,12 @@ @(bankDetailsForm: Form[BankAccountDetails])(implicit request: Request[_], messages: Messages, appConfig: FrontendAppConfig) -@layout(pageTitle = Some(title(bankDetailsForm, messages("pages.bankDetails.heading"))), backLink = true) { +@layout(pageTitle = Some(title(bankDetailsForm, messages("pages.bankDetails.heading.old")))) { @errorSummary(errors = bankDetailsForm.errors) - @h1("pages.bankDetails.heading") - @p { @messages("pages.bankDetails.p1") } + @h1("pages.bankDetails.heading.old") + @p { @messages("pages.bankDetails.p1.old") } @panelIndent { @messages("pages.bankDetails.info") @@ -46,7 +46,7 @@ @inputText(form = bankDetailsForm, id = "accountName", name = "accountName", - label = messages("pages.bankDetails.accountName.label"), + label = messages("pages.bankDetails.accountName.label.old"), classes = Some("govuk-input--width-10"), isPageHeading = false, hint = Some(Html(messages("")))) diff --git a/conf/app.routes b/conf/app.routes index 7254020a3..d66ea6c59 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -130,7 +130,7 @@ POST /cannot-provide-bank-account-details controller GET /choose-account-type controllers.bankdetails.ChooseAccountTypeController.show POST /choose-account-type controllers.bankdetails.ChooseAccountTypeController.submit -## BANK DETAILS (ACCOUNT NAME, NUMBER, SORT CODE) +## ENTER BANK DETAILS GET /account-details controllers.bankdetails.UkBankAccountDetailsController.show POST /account-details controllers.bankdetails.UkBankAccountDetailsController.submit diff --git a/conf/application.conf b/conf/application.conf index 986e61536..bc1f88a86 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -259,6 +259,11 @@ Test { } } +json.encryption { + key = "MTIzNDU2Nzg5MDEyMzQ1Ng==" + previousKeys = [] +} + controllers.internal.RegistrationController = { needsLogging = true needsAuditing = false diff --git a/conf/messages b/conf/messages index dd8a986f1..924b7f4ea 100644 --- a/conf/messages +++ b/conf/messages @@ -973,7 +973,6 @@ pages.hasCompanyBankAccount.bullet3 = in the UK pages.hasCompanyBankAccount.bullet4 = able to receive BACS payments validation.hasCompanyBankAccount.missing = Select yes if you are able to provide bank or building society details for the business - # Bank Account Type Page pages.chooseAccountType.heading = What kind of bank or building society account will you use for VAT repayments? pages.chooseAccountType.option.business = Business account @@ -991,22 +990,33 @@ pages.noUKBankAccount.dontWantToProvide = I do not want to provide pages.noUKBankAccount.error = Select why you cannot provide bank account details for the business # Bank Details page -pages.bankDetails.heading = What are the business’s bank or building society account details? -pages.bankDetails.p1 = HMRC VAT will only use this account to send VAT repayments. We will not take money from it. -pages.bankDetails.info = You must tell us if your account details change. -pages.bankDetails.accountName.label = Account name -pages.bankDetails.accountNumber.label = Account number -pages.bankDetails.accountNumber.hint = Must be between 6 and 8 digits long -pages.bankDetails.sortCode.label = Sort code -pages.bankDetails.sortCode.hint = Must be 6 digits long -validation.companyBankAccount.name.missing = Enter the account name -validation.companyBankAccount.name.maxLength = The account name must be 60 characters or less -validation.companyBankAccount.name.invalid = The account name must only include numbers, letters a to z, and special characters such as hyphens, apostrophes and brackets -validation.companyBankAccount.number.missing = Enter the account number -validation.companyBankAccount.number.invalid = Account number must be between 6 and 8 digits -validation.companyBankAccount.sortCode.missing = Enter the account sort code -validation.companyBankAccount.sortCode.invalid = Enter a valid account sort code -validation.companyBankAccount.invalidCombination = Enter a valid bank account number and sort code +pages.bankDetails.heading.old = What are the business’s bank or building society account details? +pages.bankDetails.heading.new = What are the business’s account details? +pages.bankDetails.p1.old = HMRC VAT will only use this account to send VAT repayments. We will not take money from it. +pages.bankDetails.p1.new = HMRC VAT will only use this information to send VAT repayments. Money will not be taken from the account you supply. +pages.bankDetails.info = You must tell us if your account details change. +pages.bankDetails.accountName.label.old = Account name +pages.bankDetails.accountName.label.new = Name on the account +pages.bankDetails.accountName.hint = Enter the name as it appears on bank statements and in your VAT account +pages.bankDetails.accountNumber.label = Account number +pages.bankDetails.accountNumber.hint = Must be between 6 and 8 digits long +pages.bankDetails.sortCode.label = Sort code +pages.bankDetails.sortCode.hint = Must be 6 digits long +pages.bankDetails.rollNumber.label = Building society roll number (if you have one) +pages.bankDetails.rollNumber.hint = You can find it on your card, statement or passbook +validation.companyBankAccount.name.missing.old = Enter the account name +validation.companyBankAccount.name.missing.new = Enter the name on the account +validation.companyBankAccount.name.maxLength = The account name must be 60 characters or less +validation.companyBankAccount.name.invalid.old = The account name must only include numbers, letters a to z, and special characters such as hyphens, apostrophes and brackets +validation.companyBankAccount.name.invalid.new = Name on the account must not use special characters +validation.companyBankAccount.number.missing = Enter the account number +validation.companyBankAccount.number.invalid = Account number must be between 6 and 8 digits +validation.companyBankAccount.number.digit.error = Account number must only contain numbers +validation.companyBankAccount.sortCode.missing = Enter the account sort code +validation.companyBankAccount.sortCode.invalid = Enter a valid account sort code +validation.companyBankAccount.sortCode.digit.error = Sort code must only contain numbers +validation.companyBankAccount.invalidCombination = Enter a valid bank account number and sort code +validation.companyBankAccount.rollNumber.invalid = Building society roll number must be shorter # Main Business Activity Page pages.mainBusinessActivity.heading = Which activity is the business’s main source of income? diff --git a/conf/messages.cy b/conf/messages.cy index 421a3e15e..4d9bdddab 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -989,22 +989,30 @@ pages.chooseAccountType.option.business = Cyfrif busnes pages.chooseAccountType.option.personal = Cyfrif personol # Bank Details page -pages.bankDetails.heading = Beth yw manylion cyfrif banc neu gymdeithas adeiladu’r busnes? -pages.bankDetails.p1 = Dim ond i anfon ad-daliadau TAW y bydd tîm TAW CThEM yn defnyddio’r cyfrif hwn. Ni fyddwn yn cymryd arian ohono. +pages.bankDetails.heading = Beth yw manylion cyswllt y busnes? +pages.bankDetails.p1 = Dim ond i anfon ad-daliadau TAW y bydd tîm TAW CThEF yn defnyddio’r cyfrif hwn. Ni fydd arian yn cael ei gymryd o’r cyfrif rydych chi’n ei ddarparu. pages.bankDetails.info = Mae’n rhaid i chi roi gwybod i ni os bydd manylion eich cyfrif yn newid. -pages.bankDetails.accountName.label = Enw’r cyfrif +pages.bankDetails.accountName.label = Yr enw sydd ar y cyfrif +pages.bankDetails.accountName.hint = Nodwch yr enw fel y mae’n ymddangos ar gyfriflenni banc ac yn eich cyfrif TAW pages.bankDetails.accountNumber.label = Rhif y cyfrif pages.bankDetails.accountNumber.hint = Mae’n rhaid iddo fod rhwng 6 ac 8 digid o hyd pages.bankDetails.sortCode.label = Cod didoli pages.bankDetails.sortCode.hint = Mae’n rhaid iddo fod yn 6 digid o hyd -validation.companyBankAccount.name.missing = Nodwch enw’r cyfrif +pages.bankDetails.rollNumber.label = Rhif rôl y gymdeithas adeiladu (os oes gennych un) +pages.bankDetails.rollNumber.hint = Bydd hwn i’w weld ar eich cerdyn, cyfriflen neu baslyfr +validation.companyBankAccount.name.missing.old = Nodwch enw’r cyfrif +validation.companyBankAccount.name.missing.new = Nodwch yr enw sydd ar y cyfrif validation.companyBankAccount.name.maxLength = Mae’n rhaid i enw’r cyfrif fod yn 60 o gymeriadau neu lai -validation.companyBankAccount.name.invalid = Mae’n rhaid i enw’r cyfrif gynnwys rhifau, y llythrennau a i z, a chymeriadau arbennig megis cysylltnodau, collnodau a chromfachau yn unig +validation.companyBankAccount.name.invalid.old = Mae’n rhaid i enw’r cyfrif gynnwys rhifau, y llythrennau a i z, a chymeriadau arbennig megis cysylltnodau, collnodau a chromfachau yn unig +validation.companyBankAccount.name.invalid.new = Enw sydd ar y cyfrif: mae’n rhaid i hyn beidio cynnwys cymeriadau arbennig validation.companyBankAccount.number.missing = Nodwch rif y cyfrif -validation.companyBankAccount.number.invalid = Mae’n rhaid i rif y cyfrif fod rhwng 6 ac 8 digid +validation.companyBankAccount.number.invalid = Rhif y cyfrif: mae’n rhaid i hyn fod rhwng 6 ac 8 digid +validation.companyBankAccount.number.digit.error = Rhif y cyfrif: mae’n rhaid i hyn gynnwys rhifau yn unig validation.companyBankAccount.sortCode.missing = Nodwch god didoli’r cyfrif -validation.companyBankAccount.sortCode.invalid = Nodwch god didoli dilys ar gyfer y cyfrif +validation.companyBankAccount.sortCode.invalid = Cod didoli: mae’n rhaid i hyn fod yn 6 digid +validation.companyBankAccount.sortCode.digit.error = Cod didoli: mae’n rhaid i hyn gynnwys rhifau yn unig validation.companyBankAccount.invalidCombination = Nodwch rif cyfrif banc a chod didoli dilys +validation.companyBankAccount.rollNumber.invalid = Mae’n rhaid i rif rôl y gymdeithas adeiladu fod yn fyrrach # Main Business Activity Page pages.mainBusinessActivity.heading = Pa weithgaredd yw prif ffynhonnell incwm y busnes? diff --git a/it/test/controllers/bankdetails/UKBankAccountDetailsControllerISpec.scala b/it/test/controllers/bankdetails/UKBankAccountDetailsControllerISpec.scala index e90c8a62f..f4364727c 100644 --- a/it/test/controllers/bankdetails/UKBankAccountDetailsControllerISpec.scala +++ b/it/test/controllers/bankdetails/UKBankAccountDetailsControllerISpec.scala @@ -16,10 +16,11 @@ package controllers.bankdetails +import featuretoggle.FeatureSwitch.UseNewBarsVerify import itFixtures.ITRegistrationFixtures import itutil.ControllerISpec -import models.api.EligibilitySubmissionData import models.{BankAccount, TransferOfAGoingConcern} +import models.api.EligibilitySubmissionData import org.jsoup.Jsoup import org.jsoup.nodes.Document import play.api.libs.ws.WSResponse @@ -30,109 +31,232 @@ class UKBankAccountDetailsControllerISpec extends ControllerISpec with ITRegistr val url = "/account-details" - "GET /account-details" must { - "return OK with a blank form if the VAT scheme doesn't contain bank details" in new Setup { - given() - .user.isAuthorised() - .registrationApi.getSection[BankAccount](None) + "GET /account-details" when { - insertCurrentProfileIntoDb(currentProfile, sessionString) + "UseNewBarsVerify is disabled" must { - val res: WSResponse = await(buildClient(url).get()) + "return OK with a blank form if the VAT scheme doesn't contain bank details" in new Setup { + disable(UseNewBarsVerify) + given().user.isAuthorised().registrationApi.getSection[BankAccount](None) - res.status mustBe OK - } - "return OK with a pre-populated form from backend" in new Setup { - given() - .user.isAuthorised() - .registrationApi.getSection[BankAccount](Some(bankAccount)) + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await(buildClient(url).get()) + val doc: Document = Jsoup.parse(res.body) + + res.status mustBe OK + doc.select("input[id=accountName]").size() mustBe 1 + doc.select("input[id=accountNumber]").size() mustBe 1 + doc.select("input[id=sortCode]").size() mustBe 1 + doc.select("input[id=rollNumber]").size() mustBe 0 + } - insertCurrentProfileIntoDb(currentProfile, sessionString) + "return OK with a form pre-populated from the backend when bank details exist" in new Setup { + disable(UseNewBarsVerify) + given().user.isAuthorised().registrationApi.getSection[BankAccount](Some(bankAccount)) - val res: WSResponse = await(buildClient(url).get()) - val doc: Document = Jsoup.parse(res.body) + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await(buildClient(url).get()) + val doc: Document = Jsoup.parse(res.body) - res.status mustBe OK - doc.select("input[id=accountName]").`val`() mustBe testBankName - doc.select("input[id=accountNumber]").`val`() mustBe testAccountNumber - doc.select("input[id=sortCode]").`val`() mustBe testSortCode + res.status mustBe OK + doc.select("input[id=accountName]").`val`() mustBe testBankName + doc.select("input[id=accountNumber]").`val`() mustBe testAccountNumber + doc.select("input[id=sortCode]").`val`() mustBe testSortCode + doc.select("input[id=rollNumber]").size() mustBe 0 + } } - "return OK with a pre-populated form from the backend" in new Setup { - given() - .user.isAuthorised() - .registrationApi.getSection[BankAccount](Some(bankAccount)) - insertCurrentProfileIntoDb(currentProfile, sessionString) + "UseNewBarsVerify is enabled" must { + + "return OK with a blank form when session is empty" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() - val res: WSResponse = await(buildClient(url).get()) - val doc: Document = Jsoup.parse(res.body) + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await(buildClient(url).get()) + res.status mustBe OK + val doc: Document = Jsoup.parse(res.body) + doc.select("input[id=accountName]").`val`() mustBe "" + } - res.status mustBe OK - doc.select("input[id=accountName]").`val`() mustBe testBankName - doc.select("input[id=accountNumber]").`val`() mustBe testAccountNumber - doc.select("input[id=sortCode]").`val`() mustBe testSortCode + "return OK with a form pre-populated from session when session contains bank details" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() + + insertCurrentProfileIntoDb(currentProfile, sessionString) + + await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> testAccountNumber, + "sortCode" -> testSortCode, + "rollNumber" -> testRollNumber + ))) + + val res: WSResponse = await(buildClient(url).get()) + val doc: Document = Jsoup.parse(res.body) + + res.status mustBe OK + doc.select("input[id=accountName]").`val`() mustBe testBankName + doc.select("input[id=accountNumber]").`val`() mustBe testAccountNumber + doc.select("input[id=sortCode]").`val`() mustBe testSortCode + doc.select("input[id=rollNumber]").`val`() mustBe testRollNumber + } } } "POST /account-details" when { - "bank details and Bank Account Reputation states are invalid" must { - "return BAD_REQUEST" in new Setup { - given() - .user.isAuthorised() - .bankAccountReputation.fails - .registrationApi.getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) - .registrationApi.getSection[EligibilitySubmissionData](Some(testEligibilitySubmissionData.copy(registrationReason = TransferOfAGoingConcern))) + + "UseNewBarsVerify is disabled" must { + + "redirect to the Task List when valid bank details" in new Setup { + disable(UseNewBarsVerify) + given().user + .isAuthorised() + .bankAccountReputation + .passes + .registrationApi + .getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) + .registrationApi + .replaceSection[BankAccount](bankAccount) + .registrationApi + .getSection[EligibilitySubmissionData](Some(testEligibilitySubmissionData.copy(registrationReason = TransferOfAGoingConcern))) insertCurrentProfileIntoDb(currentProfile, sessionString) - val res: WSResponse = await(buildClient(url).post(Map( - "accountName" -> testBankName, - "accountNumber" -> testAccountNumber, - "sortCode" -> testSortCode - ))) + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> testAccountNumber, + "sortCode" -> testSortCode + ))) + + res.status mustBe SEE_OTHER + res.header(HeaderNames.LOCATION) mustBe Some(controllers.routes.TaskListController.show.url) + } + + "return BAD_REQUEST when valid bank details fail BARS verification" in new Setup { + disable(UseNewBarsVerify) + given().user + .isAuthorised() + .bankAccountReputation + .fails + .registrationApi + .getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) + .registrationApi + .getSection[EligibilitySubmissionData](Some(testEligibilitySubmissionData.copy(registrationReason = TransferOfAGoingConcern))) + + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> testAccountNumber, + "sortCode" -> testSortCode + ))) res.status mustBe BAD_REQUEST } - } - "redirect to the Tasklist if the account details are valid" in new Setup { - given() - .user.isAuthorised() - .bankAccountReputation.passes - .registrationApi.getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) - .registrationApi.replaceSection[BankAccount](bankAccount) - .registrationApi.getSection[EligibilitySubmissionData](Some(testEligibilitySubmissionData.copy(registrationReason = TransferOfAGoingConcern))) + "return BAD_REQUEST when form fields are empty" in new Setup { + disable(UseNewBarsVerify) + given().user + .isAuthorised() + .registrationApi + .getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) - insertCurrentProfileIntoDb(currentProfile, sessionString) + insertCurrentProfileIntoDb(currentProfile, sessionString) - val res: WSResponse = await(buildClient(url).post(Map( - "accountName" -> testBankName, - "accountNumber" -> testAccountNumber, - "sortCode" -> testSortCode - ))) + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> "", + "accountNumber" -> "", + "sortCode" -> "" + ))) - res.status mustBe SEE_OTHER - res.header(HeaderNames.LOCATION) mustBe Some(controllers.routes.TaskListController.show.url) + res.status mustBe BAD_REQUEST + } } - "bank details are incorrect" must { - "return BAD_REQUEST" in new Setup { - given() - .user.isAuthorised() - .bankAccountReputation.fails - .registrationApi.getSection[BankAccount](Some(BankAccount(isProvided = true, None, None))) + "UseNewBarsVerify is enabled" must { + + "save bank details to session and redirect to Task List when form is valid" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() + + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> testAccountNumber, + "sortCode" -> testSortCode + ))) + + res.status mustBe SEE_OTHER + res.header(HeaderNames.LOCATION) mustBe Some(controllers.routes.TaskListController.show.url) + } + + "save bank details including roll number to session and redirect to Task List" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() + + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> testAccountNumber, + "sortCode" -> testSortCode, + "rollNumber" -> testRollNumber + ))) + + res.status mustBe SEE_OTHER + res.header(HeaderNames.LOCATION) mustBe Some(controllers.routes.TaskListController.show.url) + } + + "return BAD_REQUEST without calling BARS when form fields are empty" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() insertCurrentProfileIntoDb(currentProfile, sessionString) - val res: WSResponse = await(buildClient(url).post(Map( - "accountName" -> "", - "accountNumber" -> "", - "sortCode" -> "" - ))) + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> "", + "accountNumber" -> "", + "sortCode" -> "" + ))) + + res.status mustBe BAD_REQUEST + } + + "return BAD_REQUEST without calling BARS when account number is invalid" in new Setup { + enable(UseNewBarsVerify) + given().user.isAuthorised() + + insertCurrentProfileIntoDb(currentProfile, sessionString) + + val res: WSResponse = await( + buildClient(url).post( + Map( + "accountName" -> testBankName, + "accountNumber" -> "invalid", + "sortCode" -> testSortCode + ))) res.status mustBe BAD_REQUEST } } } - } diff --git a/it/test/itFixtures/ITRegistrationFixtures.scala b/it/test/itFixtures/ITRegistrationFixtures.scala index 47accec61..6386ce51d 100644 --- a/it/test/itFixtures/ITRegistrationFixtures.scala +++ b/it/test/itFixtures/ITRegistrationFixtures.scala @@ -46,7 +46,8 @@ trait ITRegistrationFixtures extends ApplicantDetailsFixture { val testBankName = "testName" val testSortCode = "123456" val testAccountNumber = "12345678" - val testUkBankDetails: BankAccountDetails = BankAccountDetails(testBankName, testAccountNumber, testSortCode, Some(ValidStatus)) + val testRollNumber = "AB/121212" + val testUkBankDetails: BankAccountDetails = BankAccountDetails(testBankName, testAccountNumber, testSortCode, None, Some(ValidStatus)) val bankAccount: BankAccount = BankAccount(isProvided = true, Some(testUkBankDetails), None) val emptyBankAccount: BankAccount = BankAccount(isProvided = true, None, None) val bankAccountNotProvidedNoReason: BankAccount = BankAccount(isProvided = false, None, None) diff --git a/test/fixtures/VatRegistrationFixture.scala b/test/fixtures/VatRegistrationFixture.scala index 93071d347..fa72960fe 100644 --- a/test/fixtures/VatRegistrationFixture.scala +++ b/test/fixtures/VatRegistrationFixture.scala @@ -397,7 +397,7 @@ trait VatRegistrationFixture extends BaseFixture with FlatRateFixtures with Appl val testBankName = "testName" - val testUkBankDetails: BankAccountDetails = BankAccountDetails(testBankName, testAccountNumber, testSortCode, Some(ValidStatus)) + val testUkBankDetails: BankAccountDetails = BankAccountDetails(testBankName, testAccountNumber, testSortCode, None, Some(ValidStatus)) val bankAccount: BankAccount = BankAccount(isProvided = true, Some(testUkBankDetails), None) diff --git a/test/forms/BankAccountDetailsFormSpec.scala b/test/forms/BankAccountDetailsFormSpec.scala index fb2b8e877..0f5a60140 100644 --- a/test/forms/BankAccountDetailsFormSpec.scala +++ b/test/forms/BankAccountDetailsFormSpec.scala @@ -16,26 +16,27 @@ package forms -import forms.EnterBankAccountDetailsForm._ import models.BankAccountDetails import org.scalatestplus.play.PlaySpec class BankAccountDetailsFormSpec extends PlaySpec { - "EnterBankAccountDetailsForm" should { + val numStr = 60 + val validAccountName = s"${numStr}testAccountName" + val validAccountNumber = "12345678" + val validSortCode = "123456" + val validRollNumber = "AB/121212" - val form = EnterBankAccountDetailsForm.form + "EnterBankAccountDetailsForm (useBarsVerify OFF)" should { + import forms.EnterBankAccountDetailsForm._ - val numStr = 60 - val validAccountName = s"${numStr}testAccountName" - val validAccountNumber = "12345678" - val validSortCode = "123456" + val form = EnterBankAccountDetailsForm.form "successfully bind data to the form with no errors and allow the return of a valid BankAccountDetails case class" in { val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val validBankAccountDetails = BankAccountDetails(validAccountName, validAccountNumber, validSortCode) @@ -44,15 +45,23 @@ class BankAccountDetailsFormSpec extends PlaySpec { boundForm.get mustBe validBankAccountDetails } + "successfully bind an account name containing all valid characters" in { + val formData = Map( + ACCOUNT_NAME -> "O'Brien-Smith J. A/B", + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode + ) + form.bind(formData).errors mustBe empty + } + "return a FormError when binding an empty account name to the form" in { val formData = Map( - ACCOUNT_NAME -> "", + ACCOUNT_NAME -> "", ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) - boundForm.errors.size mustBe 1 boundForm.errors.head.key mustBe ACCOUNT_NAME boundForm.errors.head.message mustBe accountNameEmptyKey @@ -62,9 +71,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val exceedMaxLength = "AlPacinoLimitedAlPacinoLimitedAlPacinoLimitedAlPacinoLimitedAlPacinoLimited" val formData = Map( - ACCOUNT_NAME -> exceedMaxLength, + ACCOUNT_NAME -> exceedMaxLength, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -78,9 +87,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val invalidAccountName = "123#@~" val formData = Map( - ACCOUNT_NAME -> invalidAccountName, + ACCOUNT_NAME -> invalidAccountName, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -92,9 +101,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { "return a FormError when binding an empty account number to the form" in { val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> "", - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -108,9 +117,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val invalidAccountNumber = "ABCDE" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> invalidAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -124,9 +133,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val invalidAccountNumber = "12345" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> invalidAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -140,9 +149,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val invalidAccountNumber = "123456789" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> invalidAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -156,9 +165,9 @@ class BankAccountDetailsFormSpec extends PlaySpec { val validAccountNumber = "123 456" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> validSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) @@ -166,14 +175,25 @@ class BankAccountDetailsFormSpec extends PlaySpec { boundForm.errors.size mustBe 0 } + "return a FormError when sort code is empty" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> "" + ) + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe SORT_CODE + boundForm.errors.head.message mustBe sortCodeEmptyKey + } "return a FormError when binding an invalid sort code part to the form" in { val invalidSortCode = "ABCDEF" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> invalidSortCode + SORT_CODE -> invalidSortCode ) val boundForm = form.bind(formData) @@ -182,34 +202,217 @@ class BankAccountDetailsFormSpec extends PlaySpec { boundForm.errors.head.key mustBe SORT_CODE boundForm.errors.head.message mustBe sortCodeInvalidKey } - "return No FormError when binding a sort code with spaces ( ) part to the form" in { - val invalidSortCode = "02 03 06" + } + + "EnterBankAccountDetailsNewBarsForm (useBarsVerify ON)" should { + import forms.EnterBankAccountDetailsNewBarsForm._ + val form = EnterBankAccountDetailsNewBarsForm.form + + "successfully bind valid data" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.get mustBe BankAccountDetails(validAccountName, validAccountNumber, validSortCode) + } + "return a FormError with the new missing account name error message when account name is empty" in { val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> "", ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> invalidSortCode + SORT_CODE -> validSortCode ) val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NAME + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNameEmptyKey + } - boundForm.errors.size mustBe 0 + "return a FormError when account name exceeds 60 characters" in { + val exceedMaxLength = "AlPacinoLimitedAlPacinoLimitedAlPacinoLimitedAlPacinoLimitedAlPacinoLimited" + + val formData = Map( + ACCOUNT_NAME -> exceedMaxLength, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NAME + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNameMaxLengthKey + } + + "return a FormError with the new invalid account name error message when account name contains invalid characters" in { + val invalidAccountName = "123#@~" + + val formData = Map( + ACCOUNT_NAME -> invalidAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NAME + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNameInvalidKey + } + + "return a FormError when account number is empty" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> "", + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NUMBER + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNumberEmptyKey + } + + "return a FormError with the length error message when account number is fewer than 6 digits" in { + val invalidAccountNumber = "12345" + + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> invalidAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NUMBER + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNumberInvalidKey + } + + "return a FormError with the invalid characters error message when account number is 8 characters but contains letters" in { + val invalidAccountNumber = "1234567A" + + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> invalidAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ACCOUNT_NUMBER + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.accountNumberDigitErrorKey + } + + "return a FormError when sort code is empty" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> "" + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe SORT_CODE + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.sortCodeEmptyKey } - "return a single FormError when the sort code is missing" in { - val emptySortCode = "" + "return a FormError with the length error message when sort code is fewer than 6 digits" in { + val invalidSortCode = "12345" val formData = Map( - ACCOUNT_NAME -> validAccountName, + ACCOUNT_NAME -> validAccountName, ACCOUNT_NUMBER -> validAccountNumber, - SORT_CODE -> emptySortCode + SORT_CODE -> invalidSortCode ) val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe SORT_CODE + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.sortCodeInvalidKey + } + + "return a FormError with the invalid characters error message when sort code is 6 characters but contains letters" in { + val invalidSortCode = "ABCDEF" + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> invalidSortCode + ) + + val boundForm = form.bind(formData) boundForm.errors.size mustBe 1 boundForm.errors.head.key mustBe SORT_CODE - boundForm.errors.head.message mustBe sortCodeEmptyKey + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.sortCodeDigitErrorKey + } + + "successfully bind with a valid roll number" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode, + ROLL_NUMBER -> validRollNumber + ) + + val boundForm = form.bind(formData) + boundForm.errors mustBe empty + boundForm.get.rollNumber mustBe Some(validRollNumber) + } + + "successfully bind when roll number is not provided" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode + ) + + val boundForm = form.bind(formData) + boundForm.errors mustBe empty + boundForm.get.rollNumber mustBe None + } + + "successfully bind when roll number is an empty string" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode, + ROLL_NUMBER -> "" + ) + + val boundForm = form.bind(formData) + boundForm.errors mustBe empty + boundForm.get.rollNumber mustBe None + } + + "return a FormError when roll number exceeds 25 characters" in { + val longRollNumber = "A" * 26 + + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode, + ROLL_NUMBER -> longRollNumber + ) + + val boundForm = form.bind(formData) + boundForm.errors.size mustBe 1 + boundForm.errors.head.key mustBe ROLL_NUMBER + boundForm.errors.head.message mustBe EnterBankAccountDetailsNewBarsForm.rollNumberInvalidKey + } + + "successfully bind and strip spaces from roll number before storing" in { + val formData = Map( + ACCOUNT_NAME -> validAccountName, + ACCOUNT_NUMBER -> validAccountNumber, + SORT_CODE -> validSortCode, + ROLL_NUMBER -> "AB 121 212" + ) + + val boundForm = form.bind(formData) + boundForm.errors mustBe empty + boundForm.get.rollNumber mustBe Some("AB121212") } } } diff --git a/test/models/BankAccountDetailsSpec.scala b/test/models/BankAccountDetailsSpec.scala index de6bc9bd5..3c3467c8f 100644 --- a/test/models/BankAccountDetailsSpec.scala +++ b/test/models/BankAccountDetailsSpec.scala @@ -16,8 +16,8 @@ package models -import play.api.libs.json.{Json, JsSuccess, JsError} import models.bars.BankAccountType +import play.api.libs.json.{JsError, Json, JsSuccess} import testHelpers.VatRegSpec class BankAccountDetailsSpec extends VatRegSpec { @@ -32,6 +32,11 @@ class BankAccountDetailsSpec extends VatRegSpec { Json.toJson(expected).validate[BankAccount] mustBe JsSuccess(expected) } + "parse successfully from Json when rollNumber is present in details" in { + val expected = validUkBankAccount.copy(details = Some(BankAccountDetails("testName", "12-34-56", "12345678", rollNumber = Some("AB/121212")))) + Json.toJson(expected).validate[BankAccount] mustBe JsSuccess(expected) + } + "parse successfully from Json when neither details or reason are present" in { val expected = validUkBankAccount.copy(details = None, reason = None) Json.toJson(expected).validate[BankAccount] mustBe JsSuccess(expected) @@ -85,5 +90,18 @@ class BankAccountDetailsSpec extends VatRegSpec { ) json.validate[BankAccount].map(_.bankAccountType) mustBe a[JsError] } + + "write rollNumber when present" in { + val json = Json.obj( + "isProvided" -> true, + "details" -> Json.obj( + "name" -> "testName", + "sortCode" -> "12-34-56", + "number" -> "12345678", + "rollNumber" -> "AB/121212" + ) + ) + json.validate[BankAccount].map(_.details.flatMap(_.rollNumber)) mustBe JsSuccess(Some("AB/121212")) + } } } diff --git a/test/models/bars/BankAccountDetailsSessionFormatSpec.scala b/test/models/bars/BankAccountDetailsSessionFormatSpec.scala new file mode 100644 index 000000000..aaa1c80e2 --- /dev/null +++ b/test/models/bars/BankAccountDetailsSessionFormatSpec.scala @@ -0,0 +1,107 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.bars + +import models.BankAccountDetails +import models.api.ValidStatus +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import play.api.libs.json.{Format, Json} +import uk.gov.hmrc.crypto.SymmetricCryptoFactory + +class BankAccountDetailsSessionFormatSpec extends AnyWordSpec with Matchers { + + private val testEncryptionKey = "MTIzNDU2Nzg5MDEyMzQ1Ng==" + private val encrypter = SymmetricCryptoFactory.aesCryptoFromConfig( + baseConfigKey = "json.encryption", + config = com.typesafe.config.ConfigFactory.parseString( + s"""json.encryption.key="$testEncryptionKey"""" + ) + ) + + private implicit val format: Format[BankAccountDetails] = + BankAccountDetailsSessionFormat.format(encrypter) + + val details: BankAccountDetails = BankAccountDetails( + name = "Test Account", + sortCode = "123456", + number = "12345678", + rollNumber = None, + status = None + ) + + "BankAccountDetailsSessionFormat" should { + + "encrypt sortCode and number when writing to Json" in { + val json = Json.toJson(details) + (json \ "sortCode").as[String] mustNot equal("123456") + (json \ "number").as[String] mustNot equal("12345678") + } + + "not encrypt name when writing to Json" in { + val json = Json.toJson(details) + (json \ "name").as[String] mustBe "Test Account" + } + + "not encrypt rollNumber when writing to Json" in { + val withRollNumber = details.copy(rollNumber = Some("AB/121212")) + val json = Json.toJson(withRollNumber) + (json \ "rollNumber").as[String] mustBe "AB/121212" + } + + "rollNumber is not included from Json when None" in { + val json = Json.toJson(details) + (json \ "rollNumber").toOption mustBe None + } + + "status not included from Json when None" in { + val json = Json.toJson(details) + (json \ "status").toOption mustBe None + } + + "read and write BankAccountDetails with all fields" in { + val full = BankAccountDetails( + name = "Test Account", + sortCode = "123456", + number = "12345678", + rollNumber = Some("AB/121212"), + status = Some(ValidStatus) + ) + val json = Json.toJson(full) + val result = Json.fromJson[BankAccountDetails](json).get + result mustBe full + } + + "fail to decrypt with a different key" in { + val json = Json.toJson(details) + + val differentKey = "ZGlmZmVyZW50a2V5MTIzNA==" + val differentEncrypter = SymmetricCryptoFactory.aesCryptoFromConfig( + baseConfigKey = "json.encryption", + config = com.typesafe.config.ConfigFactory.parseString( + s"""json.encryption.key="$differentKey"""" + ) + ) + val differentFormat: Format[BankAccountDetails] = + BankAccountDetailsSessionFormat.format(differentEncrypter) + + intercept[SecurityException] { + Json.fromJson[BankAccountDetails](json)(differentFormat).get + } + } + } +} \ No newline at end of file diff --git a/test/services/BankAccountDetailsServiceSpec.scala b/test/services/BankAccountDetailsServiceSpec.scala index ff1bb4e63..58da16c15 100644 --- a/test/services/BankAccountDetailsServiceSpec.scala +++ b/test/services/BankAccountDetailsServiceSpec.scala @@ -40,14 +40,14 @@ class BankAccountDetailsServiceSpec extends VatSpec with MockRegistrationApiConn implicit val request: Request[_] = FakeRequest() - "fetchBankAccountDetails" should { + "getBankAccountDetails" should { val bankAccount = BankAccount(isProvided = true, Some(BankAccountDetails("testName", "testCode", "testAccNumber")), None) "return a BankAccount" in new Setup { mockGetSection[BankAccount](testRegId, Some(bankAccount)) - val result: Option[BankAccount] = await(service.fetchBankAccountDetails) + val result: Option[BankAccount] = await(service.getBankAccount) result mustBe Some(bankAccount) } @@ -55,19 +55,19 @@ class BankAccountDetailsServiceSpec extends VatSpec with MockRegistrationApiConn "return None if a BankAccount isn't found" in new Setup { mockGetSection[BankAccount](testRegId, None) - val result: Option[BankAccount] = await(service.fetchBankAccountDetails) + val result: Option[BankAccount] = await(service.getBankAccount) result mustBe None } } - "saveBankAccountDetails" should { + "saveBankAccount" should { "return a BankAccount and save to the backend" in new Setup { val fullBankAccount: BankAccount = BankAccount(isProvided = true, Some(BankAccountDetails("testName", "testCode", "testAccNumber")), None) mockReplaceSection[BankAccount](testRegId, fullBankAccount) - val result: BankAccount = await(service.saveBankAccountDetails(fullBankAccount)) + val result: BankAccount = await(service.saveBankAccount(fullBankAccount)) result mustBe fullBankAccount verify(mockRegistrationApiConnector, times(1)) diff --git a/test/views/bankdetails/EnterBankDetailsViewSpec.scala b/test/views/bankdetails/EnterBankDetailsViewSpec.scala new file mode 100644 index 000000000..d85ae58ca --- /dev/null +++ b/test/views/bankdetails/EnterBankDetailsViewSpec.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package views.bankdetails + +import featuretoggle.FeatureSwitch.UseNewBarsVerify +import featuretoggle.FeatureToggleSupport._ +import forms.EnterBankAccountDetailsNewBarsForm +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import views.VatRegViewSpec +import views.html.bankdetails.EnterBankAccountDetails + +class EnterBankDetailsViewSpec extends VatRegViewSpec { + + val view: EnterBankAccountDetails = app.injector.instanceOf[EnterBankAccountDetails] + + val title = "What are the business’s account details?" + val heading = "What are the business’s account details?" + val p1 = "HMRC VAT will only use this information to send VAT repayments. Money will not be taken from the account you supply." + val panelText = "You must tell us if your account details change." + val accountName = "Name on the account" + val accountNumber = "Account number" + val accountNumberHint = "Must be between 6 and 8 digits long" + val sortCode = "Sort code" + val sortCodeHint = "Must be 6 digits long" + val rollNumber = "Building society roll number (if you have one)" + val rollNumberHint = "You can find it on your card, statement or passbook" + val buttonText = "Save and continue" + + implicit lazy val doc: Document = Jsoup.parse(view(EnterBankAccountDetailsNewBarsForm.form).body) + + "Company Bank Details Page common elements" should { + disable(UseNewBarsVerify) + + "have the correct title" in new ViewSetup { + doc.title must include(title) + } + + "have the correct heading" in new ViewSetup { + doc.heading mustBe Some(heading) + } + + "have the correct p1" in new ViewSetup { + doc.para(1) mustBe Some(p1) + } + + "have the correct panel text" in new ViewSetup { + doc.panelIndent(1) mustBe Some(panelText) + } + + "have the correct Account Name label text" in { + doc.select(Selectors.label).get(0).text mustBe accountName + } + + "have the correct Account Number label text" in { + doc.select(Selectors.label).get(1).text mustBe accountNumber + } + + "have the correct Account Number Hint text" in new ViewSetup { + doc.hintWithMultiple(2) mustBe Some(accountNumberHint) + } + + "have the correct Sort Code label text" in { + doc.select(Selectors.label).get(2).text mustBe sortCode + } + + "have the correct Sort Code Hint text" in new ViewSetup { + doc.hintWithMultiple(3) mustBe Some(sortCodeHint) + } + + "have Roll Number label text" in new ViewSetup { + doc.select(Selectors.label).get(3).text mustBe rollNumber + } + + "have the correct Roll Number Hint text" in new ViewSetup { + doc.hintWithMultiple(4) mustBe Some(rollNumberHint) + } + + "have the correct continue button" in new ViewSetup { + doc.submitButton mustBe Some(buttonText) + } + } +}