Skip to content

Commit 7c3a284

Browse files
DL-18572 Add RollNumber to Enter Details Page (#1229)
* feat: add rollnumber, add tests and add check your details page, add encrypted session details cache. rename save and fetch methods in details service
1 parent 7a15c09 commit 7c3a284

26 files changed

+1066
-234
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package pages.bankdetails
2+
3+
import forms.EnterBankAccountDetailsForm
4+
import helpers.A11ySpec
5+
import models.BankAccountDetails
6+
import play.api.data.Form
7+
import views.html.bankdetails.EnterBankAccountDetails
8+
9+
class EnterBankAccountDetailsA11ySpec extends A11ySpec {
10+
11+
val view: EnterBankAccountDetails = app.injector.instanceOf[EnterBankAccountDetails]
12+
val form: Form[BankAccountDetails] = EnterBankAccountDetailsForm.form
13+
14+
"the Enter Bank Account Details page" when {
15+
"there are no form errors" must {
16+
"pass all a11y checks" in {
17+
view(form).body must passAccessibilityChecks
18+
}
19+
}
20+
"there are form errors" must {
21+
"pass all a11y checks" in {
22+
view(form.bind(Map("" -> ""))).body must passAccessibilityChecks
23+
}
24+
}
25+
}
26+
27+
}

a11y/pages/bankdetails/UkBankAccountDetailsA11ySpec.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package pages.bankdetails
33

44
import helpers.A11ySpec
55
import views.html.bankdetails.EnterCompanyBankAccountDetails
6-
import forms.EnterBankAccountDetailsForm
6+
import forms.EnterCompanyBankAccountDetailsForm
7+
import models.BankAccountDetails
8+
import play.api.data.Form
79

810
class UkBankAccountDetailsA11ySpec extends A11ySpec {
911

10-
val view = app.injector.instanceOf[EnterCompanyBankAccountDetails]
11-
val form = EnterBankAccountDetailsForm.form
12+
val view: EnterCompanyBankAccountDetails = app.injector.instanceOf[EnterCompanyBankAccountDetails]
13+
val form: Form[BankAccountDetails] = EnterCompanyBankAccountDetailsForm.form
1214

1315
"the Enter Company Bank Account Details page" when {
1416
"there are no form errors" must {

app/config/startup/VerifyCrypto.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package config.startup
1818

19-
import uk.gov.hmrc.crypto.ApplicationCrypto
19+
import uk.gov.hmrc.play.bootstrap.frontend.filters.crypto.ApplicationCrypto
2020

2121
import javax.inject.{Inject, Singleton}
2222

app/controllers/bankdetails/ChooseAccountTypeController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ChooseAccountTypeController @Inject() (val authConnector: AuthClientConnec
3939

4040
def show: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile =>
4141
if (isEnabled(UseNewBarsVerify)) {
42-
bankAccountDetailsService.fetchBankAccountDetails.map { bankDetails =>
42+
bankAccountDetailsService.getBankAccount.map { bankDetails =>
4343
val filledForm = bankDetails
4444
.flatMap(_.bankAccountType)
4545
.fold(ChooseAccountTypeForm.form)(ChooseAccountTypeForm.form.fill)

app/controllers/bankdetails/HasBankAccountController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class HasBankAccountController @Inject()(val authConnector: AuthClientConnector,
4545
Future.successful(Redirect(controllers.flatratescheme.routes.JoinFlatRateSchemeController.show))
4646
case _ =>
4747
for {
48-
bankDetails <- bankAccountDetailsService.fetchBankAccountDetails
48+
bankDetails <- bankAccountDetailsService.getBankAccount
4949
filledForm = bankDetails.map(_.isProvided).fold(hasBankAccountForm)(hasBankAccountForm.fill)
5050
} yield Ok(view(filledForm))
5151
}

app/controllers/bankdetails/NoUKBankAccountController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class NoUKBankAccountController @Inject()(noUKBankAccountView: NoUkBankAccount,
4040
implicit request =>
4141
implicit profile =>
4242
for {
43-
optBankAccountDetails <- bankAccountDetailsService.fetchBankAccountDetails
43+
optBankAccountDetails <- bankAccountDetailsService.getBankAccount
4444
form = optBankAccountDetails.flatMap(_.reason).fold(NoUKBankAccountForm.form)(NoUKBankAccountForm.form.fill)
4545
} yield Ok(noUKBankAccountView(form))
4646
}

app/controllers/bankdetails/UkBankAccountDetailsController.scala

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,49 +18,80 @@ package controllers.bankdetails
1818

1919
import config.{AuthClientConnector, BaseControllerComponents, FrontendAppConfig}
2020
import controllers.BaseController
21+
import featuretoggle.FeatureSwitch.UseNewBarsVerify
22+
import featuretoggle.FeatureToggleSupport.isEnabled
23+
import forms.EnterCompanyBankAccountDetailsForm
24+
import forms.EnterCompanyBankAccountDetailsForm.{form => enterBankAccountDetailsForm}
2125
import forms.EnterBankAccountDetailsForm
22-
import forms.EnterBankAccountDetailsForm.{form => enterBankAccountDetailsForm}
26+
import models.BankAccountDetails
27+
import models.bars.BankAccountDetailsSessionFormat
28+
import play.api.libs.json.Format
2329
import play.api.mvc.{Action, AnyContent}
30+
import play.api.Configuration
2431
import services.{BankAccountDetailsService, SessionService}
32+
import uk.gov.hmrc.crypto.SymmetricCryptoFactory
2533
import views.html.bankdetails.EnterCompanyBankAccountDetails
34+
import views.html.bankdetails.EnterBankAccountDetails
2635

2736
import javax.inject.Inject
2837
import scala.concurrent.{ExecutionContext, Future}
2938

30-
class UkBankAccountDetailsController @Inject()(val authConnector: AuthClientConnector,
31-
val bankAccountDetailsService: BankAccountDetailsService,
32-
val sessionService: SessionService,
33-
view: EnterCompanyBankAccountDetails)
34-
(implicit appConfig: FrontendAppConfig,
35-
val executionContext: ExecutionContext,
36-
baseControllerComponents: BaseControllerComponents) extends BaseController {
39+
class UkBankAccountDetailsController @Inject() (
40+
val authConnector: AuthClientConnector,
41+
val bankAccountDetailsService: BankAccountDetailsService,
42+
val sessionService: SessionService,
43+
configuration: Configuration,
44+
newBarsView: EnterBankAccountDetails,
45+
oldView: EnterCompanyBankAccountDetails
46+
)(implicit appConfig: FrontendAppConfig, val executionContext: ExecutionContext, baseControllerComponents: BaseControllerComponents)
47+
extends BaseController {
3748

38-
def show: Action[AnyContent] = isAuthenticatedWithProfile {
39-
implicit request =>
40-
implicit profile =>
41-
for {
42-
bankDetails <- bankAccountDetailsService.fetchBankAccountDetails
43-
filledForm = bankDetails.flatMap(_.details).fold(enterBankAccountDetailsForm)(enterBankAccountDetailsForm.fill)
44-
} yield Ok(view(filledForm))
49+
private val encrypter =
50+
SymmetricCryptoFactory.aesCryptoFromConfig("json.encryption", configuration.underlying)
51+
52+
private implicit val encryptedFormat: Format[BankAccountDetails] =
53+
BankAccountDetailsSessionFormat.format(encrypter)
54+
55+
private val sessionKey = "bankAccountDetails"
56+
57+
def show: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile =>
58+
if (isEnabled(UseNewBarsVerify)) {
59+
val newBarsForm = EnterBankAccountDetailsForm.form
60+
sessionService.fetchAndGet[BankAccountDetails](sessionKey).map {
61+
case Some(details) => Ok(newBarsView(newBarsForm.fill(details)))
62+
case None => Ok(newBarsView(newBarsForm))
63+
}
64+
} else {
65+
for {
66+
bankDetails <- bankAccountDetailsService.getBankAccount
67+
filledForm = bankDetails.flatMap(_.details).fold(enterBankAccountDetailsForm)(enterBankAccountDetailsForm.fill)
68+
} yield Ok(oldView(filledForm))
69+
}
4570
}
4671

47-
def submit: Action[AnyContent] = isAuthenticatedWithProfile {
48-
implicit request =>
49-
implicit profile =>
50-
enterBankAccountDetailsForm.bindFromRequest().fold(
51-
formWithErrors =>
52-
Future.successful(BadRequest(view(formWithErrors))),
72+
def submit: Action[AnyContent] = isAuthenticatedWithProfile { implicit request => implicit profile =>
73+
if (isEnabled(UseNewBarsVerify)) {
74+
val newBarsForm = EnterBankAccountDetailsForm.form
75+
newBarsForm
76+
.bindFromRequest()
77+
.fold(
78+
formWithErrors => Future.successful(BadRequest(newBarsView(formWithErrors))),
5379
accountDetails =>
54-
bankAccountDetailsService.saveEnteredBankAccountDetails(accountDetails).map { accountDetailsValid =>
55-
if (accountDetailsValid) {
56-
Redirect(controllers.routes.TaskListController.show.url)
57-
}
58-
else {
59-
val invalidDetails = EnterBankAccountDetailsForm.formWithInvalidAccountReputation.fill(accountDetails)
60-
BadRequest(view(invalidDetails))
61-
}
80+
sessionService.cache[BankAccountDetails](sessionKey, accountDetails).map { _ =>
81+
Redirect(controllers.routes.TaskListController.show.url)
6282
}
6383
)
84+
} else {
85+
enterBankAccountDetailsForm
86+
.bindFromRequest()
87+
.fold(
88+
formWithErrors => Future.successful(BadRequest(oldView(formWithErrors))),
89+
accountDetails =>
90+
bankAccountDetailsService.saveEnteredBankAccountDetails(accountDetails).map {
91+
case true => Redirect(controllers.routes.TaskListController.show.url)
92+
case false => BadRequest(oldView(EnterCompanyBankAccountDetailsForm.formWithInvalidAccountReputation.fill(accountDetails)))
93+
}
94+
)
95+
}
6496
}
65-
66-
}
97+
}

app/forms/BankAccountDetailsForms.scala

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,31 @@ object ChooseAccountTypeForm {
4040

4141
val form: Form[BankAccountType] = Form(
4242
single(
43-
BUSINESS_OR_PERSONAL_ACCOUNT_RADIO -> default(text, "").verifying(stopOnFail(
44-
mandatory(errorMsg)
45-
)).transform[BankAccountType](
46-
s => BankAccountType.fromString(s).get,
47-
_.asBars
48-
)
43+
BUSINESS_OR_PERSONAL_ACCOUNT_RADIO -> default(text, "")
44+
.verifying(
45+
stopOnFail(
46+
mandatory(errorMsg)
47+
))
48+
.transform[BankAccountType](
49+
s => BankAccountType.fromString(s).get,
50+
_.asBars
51+
)
4952
)
5053
)
5154
}
5255

53-
object EnterBankAccountDetailsForm {
56+
object EnterCompanyBankAccountDetailsForm {
5457

5558
val ACCOUNT_NAME = "accountName"
5659
val ACCOUNT_NUMBER = "accountNumber"
5760
val SORT_CODE = "sortCode"
5861

59-
val accountNameEmptyKey = "validation.companyBankAccount.name.missing"
62+
val accountNameEmptyKey = "validation.companyBankAccount.name.missing.old"
6063
val accountNameMaxLengthKey = "validation.companyBankAccount.name.maxLength"
61-
val accountNameInvalidKey = "validation.companyBankAccount.name.invalid"
64+
val accountNameInvalidKey = "validation.companyBankAccount.name.invalid.old"
6265
val accountNumberEmptyKey = "validation.companyBankAccount.number.missing"
6366
val accountNumberInvalidKey = "validation.companyBankAccount.number.invalid"
64-
val sortCodeEmptyKey = "validation.companyBankAccount.sortCode.missing"
67+
val sortCodeEmptyKey = "validation.companyBankAccount.sortCode.missing.old"
6568
val sortCodeInvalidKey = "validation.companyBankAccount.sortCode.invalid"
6669

6770
val invalidAccountReputationKey = "sortCodeAndAccountGroup"
@@ -95,12 +98,72 @@ object EnterBankAccountDetailsForm {
9598
matchesRegex(sortCodeRegex, sortCodeInvalidKey)
9699
))
97100
)((accountName, accountNumber, sortCode) => BankAccountDetails.apply(accountName, accountNumber, sortCode, None))(bankAccountDetails =>
98-
BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, _) =>
101+
BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, _, _) =>
99102
(accountName, accountNumber, sortCode)
100103
})
101104
)
102-
103105
val formWithInvalidAccountReputation: Form[BankAccountDetails] =
104106
form.withError(invalidAccountReputationKey, invalidAccountReputationMessage)
107+
}
105108

109+
object EnterBankAccountDetailsForm {
110+
111+
val ACCOUNT_NAME = "accountName"
112+
val ACCOUNT_NUMBER = "accountNumber"
113+
val SORT_CODE = "sortCode"
114+
val ROLL_NUMBER = "rollNumber"
115+
116+
val accountNameEmptyKey = "validation.companyBankAccount.name.missing.new"
117+
val accountNameMaxLengthKey = "validation.companyBankAccount.name.maxLength"
118+
val accountNameInvalidKey = "validation.companyBankAccount.name.invalid.new"
119+
val accountNumberEmptyKey = "validation.companyBankAccount.number.missing"
120+
val accountNumberFormatKey = "validation.companyBankAccount.number.format"
121+
val accountNumberInvalidKey = "validation.companyBankAccount.number.invalid"
122+
val sortCodeEmptyKey = "validation.companyBankAccount.sortCode.missing.new"
123+
val sortCodeLengthKey = "validation.companyBankAccount.sortCode.length"
124+
val sortCodeFormatKey = "validation.companyBankAccount.sortCode.format"
125+
val rollNumberInvalidKey = "validation.companyBankAccount.rollNumber.invalid"
126+
127+
private val accountNameRegex = """^[A-Za-z0-9 '\-./]{1,60}$""".r
128+
private val accountNameMaxLength = 60
129+
private val rollNumberMaxLength = 25
130+
private val accountNumberDigitRegex = """^[0-9]+$""".r
131+
private val accountNumberLengthRegex = """^.{6,8}$""".r
132+
private val sortCodeDigitRegex = """^[0-9]+$""".r
133+
private val sortCodeLengthRegex = """^.{6}$""".r
134+
135+
val form: Form[BankAccountDetails] = Form[BankAccountDetails](
136+
mapping(
137+
ACCOUNT_NAME -> text.verifying(
138+
stopOnFail(
139+
mandatory(accountNameEmptyKey),
140+
maxLength(accountNameMaxLength, accountNameMaxLengthKey),
141+
matchesRegex(accountNameRegex, accountNameInvalidKey)
142+
)),
143+
ACCOUNT_NUMBER -> text
144+
.transform(removeSpaces, identity[String])
145+
.verifying(stopOnFail(
146+
mandatory(accountNumberEmptyKey),
147+
matchesRegex(accountNumberDigitRegex, accountNumberFormatKey),
148+
matchesRegex(accountNumberLengthRegex, accountNumberInvalidKey)
149+
)),
150+
SORT_CODE -> text
151+
.transform(removeSpaces, identity[String])
152+
.verifying(
153+
stopOnFail(
154+
mandatory(sortCodeEmptyKey),
155+
matchesRegex(sortCodeDigitRegex, sortCodeFormatKey),
156+
matchesRegex(sortCodeLengthRegex, sortCodeLengthKey)
157+
)),
158+
ROLL_NUMBER -> optional(
159+
text
160+
.transform(removeSpaces, identity[String])
161+
.verifying(maxLength(rollNumberMaxLength, rollNumberInvalidKey))
162+
)
163+
)((accountName, accountNumber, sortCode, rollNumber) => BankAccountDetails.apply(accountName, accountNumber, sortCode, rollNumber))(
164+
bankAccountDetails =>
165+
BankAccountDetails.unapply(bankAccountDetails).map { case (accountName, accountNumber, sortCode, rollNumber, _) =>
166+
(accountName, accountNumber, sortCode, rollNumber)
167+
})
168+
)
106169
}

app/models/BankAccountDetails.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ case class BankAccount(isProvided: Boolean,
2626
reason: Option[NoUKBankAccount],
2727
bankAccountType: Option[BankAccountType] = None)
2828

29-
case class BankAccountDetails(name: String, number: String, sortCode: String, status: Option[BankAccountDetailsStatus] = None)
29+
case class BankAccountDetails(name: String,
30+
number: String,
31+
sortCode: String,
32+
rollNumber: Option[String] = None,
33+
status: Option[BankAccountDetailsStatus] = None)
3034

3135
object BankAccountDetails {
3236
implicit val accountReputationWrites: OWrites[BankAccountDetails] = new OWrites[BankAccountDetails] {
@@ -42,10 +46,11 @@ object BankAccountDetails {
4246

4347
def bankSeq(bankAccount: BankAccountDetails): Seq[String] =
4448
Seq(
45-
bankAccount.name,
46-
bankAccount.number,
47-
bankAccount.sortCode
48-
)
49+
Some(bankAccount.name),
50+
Some(bankAccount.number),
51+
Some(bankAccount.sortCode),
52+
bankAccount.rollNumber
53+
).flatten
4954
}
5055

5156
object BankAccount {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2026 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package models.bars
18+
19+
import models.api.BankAccountDetailsStatus
20+
import models.BankAccountDetails
21+
import play.api.libs.functional.syntax._
22+
import play.api.libs.json._
23+
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}
24+
import uk.gov.hmrc.crypto.Sensitive.SensitiveString
25+
import uk.gov.hmrc.crypto.json.JsonEncryption
26+
27+
object BankAccountDetailsSessionFormat {
28+
29+
def format(encrypter: Encrypter with Decrypter): Format[BankAccountDetails] = {
30+
implicit val crypto: Encrypter with Decrypter = encrypter
31+
32+
implicit val sensitiveStringFormat: Format[SensitiveString] =
33+
JsonEncryption.sensitiveEncrypterDecrypter(SensitiveString.apply)
34+
35+
(
36+
(__ \ "name").format[String] and
37+
(__ \ "sortCode").format[SensitiveString]
38+
.bimap(_.decryptedValue, SensitiveString.apply) and
39+
(__ \ "number").format[SensitiveString]
40+
.bimap(_.decryptedValue, SensitiveString.apply) and
41+
(__ \ "rollNumber").formatNullable[String] and
42+
(__ \ "status").formatNullable[BankAccountDetailsStatus]
43+
)(BankAccountDetails.apply, unlift(BankAccountDetails.unapply))
44+
}
45+
}

0 commit comments

Comments
 (0)