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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions a11y/pages/bankdetails/BarsBankAccountDetailsA11ySpec.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}

}
2 changes: 1 addition & 1 deletion app/config/startup/VerifyCrypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/bankdetails/HasBankAccountController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
91 changes: 61 additions & 30 deletions app/controllers/bankdetails/UkBankAccountDetailsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
)
}
}

}
}
79 changes: 71 additions & 8 deletions app/forms/BankAccountDetailsForms.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
)
}
Expand Down Expand Up @@ -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)
})
)
}
15 changes: 10 additions & 5 deletions app/models/BankAccountDetails.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand All @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions app/models/bars/BankAccountDetailsSessionFormat.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading