diff --git a/app/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingController.scala b/app/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingController.scala index 4068d82d..2ccaf5bd 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingController.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingController.scala @@ -68,7 +68,7 @@ class DoesYourRentIncludeParkingController @Inject()(doesYourRentIncludeParking radioValue = radioValue.radio ) if (radioValue.radio == "Yes") { - Future.successful(Redirect(routes.CheckRentFreePeriodController.show.url)) + Future.successful(Redirect(routes.HowManyParkingSpacesOrGaragesIncludedInRentController.show.url)) } else { //TODO Future.successful(Redirect(routes.CheckRentFreePeriodController.show.url)) diff --git a/app/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentController.scala b/app/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentController.scala new file mode 100644 index 00000000..9eae0d07 --- /dev/null +++ b/app/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentController.scala @@ -0,0 +1,107 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.controllers + +import play.api.data.{Form, FormError, Forms} +import play.api.i18n.{I18nSupport, Messages} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import play.twirl.api.HtmlFormat +import uk.gov.hmrc.http.NotFoundException +import uk.gov.hmrc.ngrraldfrontend.actions.{AuthRetrievals, PropertyLinkingAction} +import uk.gov.hmrc.ngrraldfrontend.config.AppConfig +import uk.gov.hmrc.ngrraldfrontend.models.forms.HowManyParkingSpacesOrGaragesIncludedInRentForm +import uk.gov.hmrc.ngrraldfrontend.models.forms.HowManyParkingSpacesOrGaragesIncludedInRentForm.form +import uk.gov.hmrc.ngrraldfrontend.models.registration.CredId +import uk.gov.hmrc.ngrraldfrontend.repo.RaldRepo +import uk.gov.hmrc.ngrraldfrontend.views.html.HowManyParkingSpacesOrGaragesIncludedInRentView +import uk.gov.hmrc.ngrraldfrontend.views.html.components.InputText +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendController + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class HowManyParkingSpacesOrGaragesIncludedInRentController @Inject()(howManyParkingSpacesOrGaragesIncludedInRentView: HowManyParkingSpacesOrGaragesIncludedInRentView, + authenticate: AuthRetrievals, + inputText: InputText, + hasLinkedProperties: PropertyLinkingAction, + raldRepo: RaldRepo, + mcc: MessagesControllerComponents)(implicit appConfig: AppConfig, ec: ExecutionContext) + extends FrontendController(mcc) with I18nSupport { + + def generateInputText(form: Form[HowManyParkingSpacesOrGaragesIncludedInRentForm], inputFieldName: String)(implicit messages: Messages): HtmlFormat.Appendable = { + inputText( + form = form, + id = inputFieldName, + name = inputFieldName, + label = messages(s"howManyParkingSpacesOrGaragesIncludedInRent.$inputFieldName.label"), + headingMessageArgs = Seq("govuk-fieldset__legend govuk-fieldset__legend--s"), + isPageHeading = true, + isVisible = true, + classes = Some("govuk-input govuk-input--width-5"), + ) + } + + + def show: Action[AnyContent] = { + (authenticate andThen hasLinkedProperties).async { implicit request => + request.propertyLinking.map(property => + Future.successful(Ok(howManyParkingSpacesOrGaragesIncludedInRentView( + form = form, + propertyAddress = property.addressFull, + uncoveredSpaces = generateInputText(form, "uncoveredSpaces"), + coveredSpaces = generateInputText(form, "coveredSpaces"), + garages = generateInputText(form, "garages"), + )))).getOrElse(throw new NotFoundException("Couldn't find property in mongo")) + } + } + + def submit: Action[AnyContent] = + (authenticate andThen hasLinkedProperties).async { implicit request => + form.bindFromRequest().fold( + formWithErrors => { + + val uncoveredSpaces = FormError(key = "uncoveredSpaces", message = "howManyParkingSpacesOrGaragesIncludedInRent.error.required") + val coveredSpaces = FormError(key = "coveredSpaces", message = "howManyParkingSpacesOrGaragesIncludedInRent.error.required") + val garages = FormError(key = "garages", message = "howManyParkingSpacesOrGaragesIncludedInRent.error.required") + + val validationCheck = formWithErrors.errors.head match { + case value if value.key.isEmpty && value.messages.contains("howManyParkingSpacesOrGaragesIncludedInRent.error.required") => formWithErrors.copy(errors = Seq(uncoveredSpaces, coveredSpaces, garages)) + case _ => formWithErrors + } + + val formWithCorrectedErrors = validationCheck + request.propertyLinking.map(property => + Future.successful(BadRequest(howManyParkingSpacesOrGaragesIncludedInRentView( + form = formWithCorrectedErrors, + propertyAddress = property.addressFull, + uncoveredSpaces = generateInputText(formWithCorrectedErrors, "uncoveredSpaces"), + coveredSpaces = generateInputText(formWithCorrectedErrors, "coveredSpaces"), + garages = generateInputText(formWithCorrectedErrors, "garages") + )))).getOrElse(throw new NotFoundException("Couldn't find property in mongo")) + }, + rentAmount => + raldRepo.insertHowManyParkingSpacesOrGaragesIncludedInRent( + credId = CredId(request.credId.getOrElse("")), + uncoveredSpaces = rentAmount.uncoveredSpaces, + coveredSpaces = rentAmount.coveredSpaces, + garages = rentAmount.garages + ) + Future.successful(Redirect(routes.CheckRentFreePeriodController.show.url)) + ) + } +} + diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/HowManyParkingSpacesOrGarages.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/HowManyParkingSpacesOrGarages.scala new file mode 100644 index 00000000..23f82640 --- /dev/null +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/HowManyParkingSpacesOrGarages.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.models + +import play.api.libs.json.{Json, OFormat} + +case class HowManyParkingSpacesOrGarages( + uncoveredSpaces: String, + coveredSpaces: String, + garages: String, + ) + +object HowManyParkingSpacesOrGarages { + implicit val format: OFormat[HowManyParkingSpacesOrGarages] = Json.format[HowManyParkingSpacesOrGarages] +} \ No newline at end of file diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/RaldUserAnswers.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/RaldUserAnswers.scala index 0f737763..f8887264 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/models/RaldUserAnswers.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/RaldUserAnswers.scala @@ -40,6 +40,7 @@ final case class RaldUserAnswers( hasAnotherRentPeriod: Option[Boolean] = None, whatYourRentIncludes: Option[WhatYourRentIncludes] = None, doesYourRentIncludeParking: Option[Boolean] = None, + howManyParkingSpacesOrGaragesIncludedInRent: Option[HowManyParkingSpacesOrGarages] = None ) diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/CommonFormValidators.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/CommonFormValidators.scala index 7e22ef90..ea0a9696 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/CommonFormValidators.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/CommonFormValidators.scala @@ -38,6 +38,25 @@ trait CommonFormValidators { Invalid(errorKey, maximum) } + protected def maximumValue[A](maximum: A, errorKey: String)(implicit ev: Ordering[A]): Constraint[A] = + Constraint { + input => + import ev.* + if (input <= maximum) { + Valid + } else { + Invalid(errorKey, maximum) + } + } + + protected def isNotEmpty(value: String, errorKey: String): Constraint[String] = + Constraint { + case str if str.trim.nonEmpty => + Valid + case _ => + Invalid(errorKey, value) + } + protected def isDateEmpty[A](errorKeys: Map[DateErrorKeys, String]): Constraint[A] = Constraint((input: A) => dateEmptyValidation(input.asInstanceOf[NGRDate], errorKeys) diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentForm.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentForm.scala new file mode 100644 index 00000000..345565c6 --- /dev/null +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentForm.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.models.forms + +import play.api.data.Form +import play.api.data.Forms.{mapping, optional} +import play.api.data.validation.{Constraint, Invalid, Valid} +import play.api.libs.json.{Json, OFormat} +import uk.gov.hmrc.ngrraldfrontend.models.forms.AgreementVerbalForm.firstError +import uk.gov.hmrc.ngrraldfrontend.models.forms.mappings.Mappings + +final case class HowManyParkingSpacesOrGaragesIncludedInRentForm( + uncoveredSpaces: Int, + coveredSpaces: Int, + garages: Int, + ) + +object HowManyParkingSpacesOrGaragesIncludedInRentForm extends CommonFormValidators with Mappings { + implicit val format: OFormat[HowManyParkingSpacesOrGaragesIncludedInRentForm] = Json.format[HowManyParkingSpacesOrGaragesIncludedInRentForm] + + private lazy val fieldRequired = "howManyParkingSpacesOrGaragesIncludedInRent.error.required" + private lazy val uncoveredSpacesWholeNumError = "howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.wholeNum.error" + private lazy val coveredSpacesWholeNumError = "howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces.wholeNum.error" + private lazy val garagesSpacesWholeNumError = "howManyParkingSpacesOrGaragesIncludedInRent.garages.wholeNum.error" + private lazy val uncoveredSpacesTooHighError = "howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.tooHigh.error" + private lazy val coveredSpacesTooHighError = "howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces.tooHigh.error" + private lazy val garagesTooHighError = "howManyParkingSpacesOrGaragesIncludedInRent.garages.tooHigh.error" + private lazy val allFieldsRequiredError = "howManyParkingSpacesOrGaragesIncludedInRent.allFields.error.required" + private val maxValue = 9999 + + def unapply(howManyParkingSpacesOrGaragesIncludedInRentForm: HowManyParkingSpacesOrGaragesIncludedInRentForm): Option[(Int, Int, Int)] = + Some( + howManyParkingSpacesOrGaragesIncludedInRentForm.uncoveredSpaces, + howManyParkingSpacesOrGaragesIncludedInRentForm.coveredSpaces, + howManyParkingSpacesOrGaragesIncludedInRentForm.garages, + ) + + private def isParkingSpacesEmpty[A]: + Constraint[A] = + Constraint((input: A) => { + val formData = input.asInstanceOf[HowManyParkingSpacesOrGaragesIncludedInRentForm] + val totalSpaces = Seq(formData.uncoveredSpaces, formData.coveredSpaces, formData.garages).sum + if (totalSpaces > 0) + Valid + else + Invalid(fieldRequired) + }) + + val form: Form[HowManyParkingSpacesOrGaragesIncludedInRentForm] = { + Form( + mapping( + "uncoveredSpaces" -> int( + requiredKey = allFieldsRequiredError, + wholeNumberKey = uncoveredSpacesWholeNumError, + nonNumericKey = uncoveredSpacesWholeNumError, + ).verifying( + maximumValue(9999, uncoveredSpacesTooHighError)), + "coveredSpaces" -> + int( + requiredKey = allFieldsRequiredError, + wholeNumberKey = coveredSpacesWholeNumError, + nonNumericKey = coveredSpacesWholeNumError, + ).verifying( + maximumValue(9999, coveredSpacesTooHighError) + ), + "garages" -> + int( + requiredKey = allFieldsRequiredError, + wholeNumberKey = garagesSpacesWholeNumError, + nonNumericKey = garagesSpacesWholeNumError, + ).verifying( + maximumValue(9999, garagesTooHighError) + ) + ) + (HowManyParkingSpacesOrGaragesIncludedInRentForm.apply)(HowManyParkingSpacesOrGaragesIncludedInRentForm.unapply) + .verifying( + firstError( + isParkingSpacesEmpty + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Formatters.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Formatters.scala index c6e8f236..c550f297 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Formatters.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Formatters.scala @@ -19,6 +19,8 @@ package uk.gov.hmrc.ngrraldfrontend.models.forms.mappings import play.api.data.FormError import play.api.data.format.Formatter +import scala.util.control.Exception.nonFatalCatch + trait Formatters { private[mappings] def stringFormatter(errorKey: String, args: Seq[String] = Seq.empty): Formatter[String] = new Formatter[String] { @@ -33,4 +35,34 @@ trait Formatters { override def unbind(key: String, value: String): Map[String, String] = Map(key -> value) } + + private[mappings] def intFormatter( + isRequired: Boolean, + requiredKey: String, + wholeNumberKey: String, + nonNumericKey: String, + args: Seq[String] = Seq.empty + ): Formatter[Int] = new Formatter[Int] { + val decimalRegexp = """^-?(\d*\.\d*)$""" + override def bind(key: String, data: Map[String, String]) = { + val rawValue = data.get(key).map(_.trim).filter(_.nonEmpty) + rawValue match { + case None => + if (isRequired) { + Left(Seq(FormError(key, requiredKey, args))) + } else { + Left(Seq.empty) // Or Right(defaultValue) if you want to provide a default + } + + case Some(s) if s.matches(decimalRegexp) => + Left(Seq(FormError(key, wholeNumberKey, args))) + case Some(s) => + nonFatalCatch + .either(s.replace(",", "").toInt) + .left.map(_ => Seq(FormError(key, nonNumericKey, args))) + } + } + override def unbind(key: String, value: Int) = + Map(key -> value.toString) + } } diff --git a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Mappings.scala b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Mappings.scala index 9c105349..63b603fa 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Mappings.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/models/forms/mappings/Mappings.scala @@ -22,4 +22,12 @@ import play.api.data.Forms.of trait Mappings extends Formatters { protected def text(errorKey: String = "error.required", args: Seq[String] = Seq.empty): FieldMapping[String] = of(stringFormatter(errorKey, args)) + + protected def int( + isRequired: Boolean = true, + requiredKey: String = "error.required", + wholeNumberKey: String = "error.wholeNumber", + nonNumericKey: String = "error.nonNumeric", + args: Seq[String] = Seq.empty): FieldMapping[Int] = + of(intFormatter(isRequired, requiredKey, wholeNumberKey, nonNumericKey, args)) } diff --git a/app/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepo.scala b/app/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepo.scala index 4cd320cc..3a520fe3 100644 --- a/app/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepo.scala +++ b/app/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepo.scala @@ -237,6 +237,16 @@ case class RaldRepo @Inject()(mongo: MongoComponent, findAndUpdateByCredId(credId, updates: _*) } + def insertHowManyParkingSpacesOrGaragesIncludedInRent(credId: CredId, uncoveredSpaces: Int, coveredSpaces: Int, garages:Int): Future[Option[RaldUserAnswers]] = { + val updates = Seq( + Updates.set("howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces", uncoveredSpaces.toString()), + Updates.set("howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces", coveredSpaces.toString()), + Updates.set("howManyParkingSpacesOrGaragesIncludedInRent.garages", garages.toString()) + ) + findAndUpdateByCredId(credId, updates: _*) + } + + def findByCredId(credId: CredId): Future[Option[RaldUserAnswers]] = { collection.find( equal("credId.value", credId.value) diff --git a/app/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentView.scala.html b/app/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentView.scala.html new file mode 100644 index 00000000..cd69f249 --- /dev/null +++ b/app/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentView.scala.html @@ -0,0 +1,46 @@ +@* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +@import uk.gov.hmrc.govukfrontend.views.html.components._ +@import uk.gov.hmrc.govukfrontend.views.Aliases._ +@import uk.gov.hmrc.ngrraldfrontend.views.html.components._ +@import uk.gov.hmrc.ngrraldfrontend.viewmodels.govuk.all._ +@import uk.gov.hmrc.ngrraldfrontend.config.AppConfig +@import uk.gov.hmrc.ngrraldfrontend.models.forms.HowManyParkingSpacesOrGaragesIncludedInRentForm + +@this( + layout: Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + saveAndContinueButton: saveAndContinueButton +) + +@(form:Form[HowManyParkingSpacesOrGaragesIncludedInRentForm], propertyAddress: String, uncoveredSpaces: Html, coveredSpaces: Html, garages: Html)(implicit request: RequestHeader, messages: Messages, appConfig: AppConfig) + +@layout(pageTitle = Some(messages("howManyParkingSpacesOrGaragesIncludedInRent.title")), showBackLink = true, fullWidth = false) { + @formHelper(action = uk.gov.hmrc.ngrraldfrontend.controllers.routes.HowManyParkingSpacesOrGaragesIncludedInRentController.submit, Symbol("autoComplete") -> "off") { + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + @propertyAddress +

@messages("howManyParkingSpacesOrGaragesIncludedInRent.title")

+

@messages("howManyParkingSpacesOrGaragesIncludedInRent.hint")

+ @uncoveredSpaces + @coveredSpaces + @garages + @saveAndContinueButton(msg = messages("service.continue"), isStartButton = false) + } +} \ No newline at end of file diff --git a/conf/app.routes b/conf/app.routes index d6598227..9b65aefd 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -41,4 +41,6 @@ POST /rent-dates-agree-start uk.gov.hmrc.ngrraldf GET /what-rent-includes uk.gov.hmrc.ngrraldfrontend.controllers.WhatYourRentIncludesController.show POST /what-rent-includes uk.gov.hmrc.ngrraldfrontend.controllers.WhatYourRentIncludesController.submit GET /does-rent-include-parking-spaces-or-garages uk.gov.hmrc.ngrraldfrontend.controllers.DoesYourRentIncludeParkingController.show -POST /does-rent-include-parking-spaces-or-garages uk.gov.hmrc.ngrraldfrontend.controllers.DoesYourRentIncludeParkingController.submit \ No newline at end of file +POST /does-rent-include-parking-spaces-or-garages uk.gov.hmrc.ngrraldfrontend.controllers.DoesYourRentIncludeParkingController.submit +GET /how-many-parking-spaces-or-garages-included-in-rent uk.gov.hmrc.ngrraldfrontend.controllers.HowManyParkingSpacesOrGaragesIncludedInRentController.show +POST /how-many-parking-spaces-or-garages-included-in-rent uk.gov.hmrc.ngrraldfrontend.controllers.HowManyParkingSpacesOrGaragesIncludedInRentController.submit \ No newline at end of file diff --git a/conf/messages b/conf/messages index fb0235d6..6296bd7f 100644 --- a/conf/messages +++ b/conf/messages @@ -323,4 +323,20 @@ whatYourRentIncludes.radio.6.required = Select yes if your rent includes service #DoesYourRentIncludeParking doesYourRentIncludeParking.title = Does your rent include parking spaces or garages? -doesYourRentIncludeParking.empty.error = Select yes if your rent includes parking spaces or garages \ No newline at end of file +doesYourRentIncludeParking.empty.error = Select yes if your rent includes parking spaces or garages + +#HowManyParkingSpacesOrGaragesIncludedInRent +howManyParkingSpacesOrGaragesIncludedInRent.title = How many parking spaces or garages are included in your rent? +howManyParkingSpacesOrGaragesIncludedInRent.hint = If spaces are in communal car park and you do not have a set number of spaces, provide an approximate number of spaces +howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.label = Uncovered spaces +howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces.label = Covered spaces +howManyParkingSpacesOrGaragesIncludedInRent.garages.label = Garages + +howManyParkingSpacesOrGaragesIncludedInRent.error.required = Total number of uncovered spaces, covered spaces and garages included in your rent must be more than 0 +howManyParkingSpacesOrGaragesIncludedInRent.allFields.error.required = Enter how many parking spaces or garages are included in your rent +howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.wholeNum.error = Number of uncovered parking spaces included in your rent must be a number, like 9 +howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces.wholeNum.error = Number of covered parking spaces included in your rent must be a number, like 9 +howManyParkingSpacesOrGaragesIncludedInRent.garages.wholeNum.error = Number of garages included in your rent must be a number, like 9 +howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.tooHigh.error = Number of uncovered parking spaces included in your rent must be 9,999 or less +howManyParkingSpacesOrGaragesIncludedInRent.coveredSpaces.tooHigh.error = Number of covered parking spaces included in your rent must be 9,999 or less +howManyParkingSpacesOrGaragesIncludedInRent.garages.tooHigh.error = Number of garages included in your rent must be 9,999 or less diff --git a/test/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingControllerSpec.scala b/test/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingControllerSpec.scala index ede1be0b..6499a0b9 100644 --- a/test/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingControllerSpec.scala +++ b/test/uk/gov/hmrc/ngrraldfrontend/controllers/DoesYourRentIncludeParkingControllerSpec.scala @@ -64,10 +64,10 @@ class DoesYourRentIncludeParkingControllerSpec extends ControllerSpecSupport { ) .withHeaders(HeaderNames.authorisation -> "Bearer 1"), None, None, None, Some(property), credId = Some(credId.value), None, None, nino = Nino(true, Some("")))) result.map(result => { - result.header.headers.get("Location") mustBe Some("/ngr-rald-frontend/do-you-have-a-rent-free-period") + result.header.headers.get("Location") mustBe Some("/how-many-parking-spaces-or-garages-included-in-rent") }) status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.CheckRentFreePeriodController.show.url) + redirectLocation(result) mustBe Some(routes.HowManyParkingSpacesOrGaragesIncludedInRentController.show.url) } "Return OK and the correct view after submitting no" in { when(mockRaldRepo.findByCredId(any())) thenReturn (Future.successful(Some(RaldUserAnswers(credId = CredId(null), NewAgreement, selectedProperty = property)))) diff --git a/test/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentControllerSpec.scala b/test/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentControllerSpec.scala new file mode 100644 index 00000000..effa27ae --- /dev/null +++ b/test/uk/gov/hmrc/ngrraldfrontend/controllers/HowManyParkingSpacesOrGaragesIncludedInRentControllerSpec.scala @@ -0,0 +1,96 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.controllers + +import play.api.http.Status.{BAD_REQUEST, OK, SEE_OTHER} +import play.api.test.FakeRequest +import play.api.test.Helpers.{await, contentAsString, defaultAwaitTimeout, redirectLocation, status} +import uk.gov.hmrc.http.{HeaderNames, NotFoundException} +import uk.gov.hmrc.ngrraldfrontend.helpers.ControllerSpecSupport +import uk.gov.hmrc.ngrraldfrontend.views.html.HowManyParkingSpacesOrGaragesIncludedInRentView +import uk.gov.hmrc.ngrraldfrontend.views.html.components.InputText + +class HowManyParkingSpacesOrGaragesIncludedInRentControllerSpec extends ControllerSpecSupport { + val pageTitle = "How many parking spaces or garages are included in your rent?" + val view: HowManyParkingSpacesOrGaragesIncludedInRentView = inject[HowManyParkingSpacesOrGaragesIncludedInRentView] + val mockInputText: InputText = inject[InputText] + val controller: HowManyParkingSpacesOrGaragesIncludedInRentController = new HowManyParkingSpacesOrGaragesIncludedInRentController( + howManyParkingSpacesOrGaragesIncludedInRentView = view, + authenticate = mockAuthJourney, + inputText = mockInputText, + hasLinkedProperties = mockPropertyLinkingAction, + raldRepo = mockRaldRepo, + mcc = mcc)(mockConfig) + + " HowManyParkingSpacesOrGaragesIncludedInRentController" must { + "method show" must { + "Return OK and the correct view" in { + val result = controller.show()(authenticatedFakeRequest()) + status(result) mustBe OK + val content = contentAsString(result) + content must include(pageTitle) + } + "Return NotFoundException when property is not found in the mongo" in { + mockRequestWithoutProperty() + val exception = intercept[NotFoundException] { + await(controller.show(authenticatedFakeRequest())) + } + exception.getMessage contains "Couldn't find property in mongo" mustBe true + } + } + + "method submit" must { + "Return OK and the correct view" in { + val fakePostRequest = FakeRequest(routes.HowManyParkingSpacesOrGaragesIncludedInRentController.submit) + .withFormUrlEncodedBody( + "uncoveredSpaces" -> "1", + "coveredSpaces" -> "0", + "garages" -> "0" + ).withHeaders(HeaderNames.authorisation -> "Bearer 1") + + val result = controller.submit()(authenticatedFakeRequest(fakePostRequest)) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.CheckRentFreePeriodController.show.url) + } + "Return BAD_REQUEST for inputting 0 in all fields and the correct view" in { + mockRequest() + val fakePostRequest = FakeRequest(routes.HowManyParkingSpacesOrGaragesIncludedInRentController.submit) + .withFormUrlEncodedBody( + "uncoveredSpaces" -> "0", + "coveredSpaces" -> "0", + "garages" -> "0" + ).withHeaders(HeaderNames.authorisation -> "Bearer 1") + + val result = controller.submit()(authenticatedFakeRequest(fakePostRequest)) + status(result) mustBe BAD_REQUEST + } + "Return Exception if no address is in the mongo" in { + mockRequestWithoutProperty() + val fakePostRequest = FakeRequest(routes.HowManyParkingSpacesOrGaragesIncludedInRentController.submit) + .withFormUrlEncodedBody( + "uncoveredSpaces" -> "", + "coveredSpaces" -> "0", + "garages" -> "0" + ).withHeaders(HeaderNames.authorisation -> "Bearer 1") + val exception = intercept[NotFoundException] { + await(controller.submit()(authenticatedFakeRequest(fakePostRequest))) + } + exception.getMessage contains "Couldn't find property in mongo" mustBe true + } + } + } +} diff --git a/test/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentFormSpec.scala b/test/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentFormSpec.scala new file mode 100644 index 00000000..83bebafa --- /dev/null +++ b/test/uk/gov/hmrc/ngrraldfrontend/models/forms/HowManyParkingSpacesOrGaragesIncludedInRentFormSpec.scala @@ -0,0 +1,131 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.models.forms + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import play.api.data.FormError +import play.api.libs.json.Json + +import scala.collection.immutable.ArraySeq + +class HowManyParkingSpacesOrGaragesIncludedInRentFormSpec extends AnyWordSpec with Matchers { + + "HowManyParkingSpacesOrGaragesIncludedInRentForm" should { + + "bind valid input" in { + val data = Map( + "uncoveredSpaces" -> "100", + "coveredSpaces" -> "100", + "garages" -> "100" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.hasErrors shouldBe false + boundForm.value shouldBe Some(HowManyParkingSpacesOrGaragesIncludedInRentForm(uncoveredSpaces = 100, coveredSpaces = 100, garages = 100)) + } + + "fail to bind empty input" in { + val data = Map( + "uncoveredSpaces" -> "", + "coveredSpaces" -> "100", + "garages" -> "100" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.hasErrors shouldBe true + boundForm.errors should contain(FormError("uncoveredSpaces", "howManyParkingSpacesOrGaragesIncludedInRent.allFields.error.required")) + } + + "fail to bind non-numeric input" in { + val data = Map( + "uncoveredSpaces" -> "abc", + "coveredSpaces" -> "100", + "garages" -> "100" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.errors shouldBe List(FormError("uncoveredSpaces", List("howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.wholeNum.error"), List())) + } + + "fail to bind input greater than 9,999" in { + val data = Map( + "uncoveredSpaces" -> "10000", + "coveredSpaces" -> "100", + "garages" -> "100" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.hasErrors shouldBe true + boundForm.errors shouldBe List(FormError("uncoveredSpaces", List("howManyParkingSpacesOrGaragesIncludedInRent.uncoveredSpaces.tooHigh.error"), ArraySeq(9999))) + } + + "fail to bind when input fields are all 0" in { + val data = Map( + "uncoveredSpaces" -> "0", + "coveredSpaces" -> "0", + "garages" -> "0" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.hasErrors shouldBe true + boundForm.errors shouldBe List(FormError("", "howManyParkingSpacesOrGaragesIncludedInRent.error.required")) + } + + "bind edge case of exactly 9,999" in { + val data = Map( + "uncoveredSpaces" -> "9999", + "coveredSpaces" -> "100", + "garages" -> "100" + ) + val boundForm = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.bind(data) + + boundForm.hasErrors shouldBe false + boundForm.value shouldBe Some(HowManyParkingSpacesOrGaragesIncludedInRentForm(uncoveredSpaces = 9999, coveredSpaces = 100, garages = 100)) + } + } + + "serialize to JSON correctly" in { + val form = HowManyParkingSpacesOrGaragesIncludedInRentForm(10, 10, 10) + val json = Json.toJson(form) + + json shouldBe Json.obj( + "uncoveredSpaces" -> 10, + "coveredSpaces" -> 10, + "garages" -> 10 + ) + } + + "deserialize from JSON correctly" in { + val json = Json.obj( + "uncoveredSpaces" -> 10, + "coveredSpaces" -> 10, + "garages" -> 10 + ) + val result = json.validate[HowManyParkingSpacesOrGaragesIncludedInRentForm] + + result.isSuccess shouldBe true + result.get shouldBe HowManyParkingSpacesOrGaragesIncludedInRentForm(10,10,10) + } + + "fail deserialization if value is missing" in { + val json = Json.obj() + val result = json.validate[HowManyParkingSpacesOrGaragesIncludedInRentForm] + + result.isError shouldBe true + } +} diff --git a/test/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepoSpec.scala b/test/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepoSpec.scala index fa060def..fd937564 100644 --- a/test/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepoSpec.scala +++ b/test/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepoSpec.scala @@ -470,6 +470,24 @@ class RaldRepoSpec extends TestSupport with TestData )))) } + "insert how many parking spaces or garages included in rent with 1 uncoveredSpaces" in { + await(repository.insertHowManyParkingSpacesOrGaragesIncludedInRent(credId, uncoveredSpaces = 1, coveredSpaces = 0, garages = 0)) + val actual = await(repository.findByCredId(credId)) + actual shouldBe Some(RaldUserAnswers(credId, NewAgreement, property, howManyParkingSpacesOrGaragesIncludedInRent = Some(HowManyParkingSpacesOrGarages(uncoveredSpaces = "1", coveredSpaces = "0", garages = "0")))) + } + + "insert how many parking spaces or garages included in rent with 1 coveredSpaces" in { + await(repository.insertHowManyParkingSpacesOrGaragesIncludedInRent(credId, uncoveredSpaces = 0, coveredSpaces = 1, garages = 0)) + val actual = await(repository.findByCredId(credId)) + actual shouldBe Some(RaldUserAnswers(credId, NewAgreement, property, howManyParkingSpacesOrGaragesIncludedInRent = Some(HowManyParkingSpacesOrGarages(uncoveredSpaces = "0", coveredSpaces = "1", garages = "0")))) + } + + "insert how many parking spaces or garages included in rent with 1 garage" in { + await(repository.insertHowManyParkingSpacesOrGaragesIncludedInRent(credId, uncoveredSpaces = 0, coveredSpaces = 0, garages = 1)) + val actual = await(repository.findByCredId(credId)) + actual shouldBe Some(RaldUserAnswers(credId, NewAgreement, property, howManyParkingSpacesOrGaragesIncludedInRent = Some(HowManyParkingSpacesOrGarages(uncoveredSpaces = "0", coveredSpaces = "0", garages = "1")))) + } + "credId doesn't exist in mongoDB" in { val actual = await(repository.findByCredId(credId2)) actual mustBe None diff --git a/test/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentViewSpec.scala b/test/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentViewSpec.scala new file mode 100644 index 00000000..29c2300b --- /dev/null +++ b/test/uk/gov/hmrc/ngrraldfrontend/views/HowManyParkingSpacesOrGaragesIncludedInRentViewSpec.scala @@ -0,0 +1,121 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.ngrraldfrontend.views + +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import play.api.data.Form +import play.api.i18n.Messages +import play.twirl.api.HtmlFormat +import uk.gov.hmrc.ngrraldfrontend.helpers.ViewBaseSpec +import uk.gov.hmrc.ngrraldfrontend.models.forms.HowManyParkingSpacesOrGaragesIncludedInRentForm +import uk.gov.hmrc.ngrraldfrontend.views.html.HowManyParkingSpacesOrGaragesIncludedInRentView +import uk.gov.hmrc.ngrraldfrontend.views.html.components.InputText + +class HowManyParkingSpacesOrGaragesIncludedInRentViewSpec extends ViewBaseSpec { + lazy val view: HowManyParkingSpacesOrGaragesIncludedInRentView = inject[HowManyParkingSpacesOrGaragesIncludedInRentView] + + object Strings { + val heading = "How many parking spaces or garages are included in your rent?" + val hint = "If spaces are in communal car park and you do not have a set number of spaces, provide an approximate number of spaces" + val label1 = "Uncovered spaces" + val label2 = "Covered spaces" + val label3 = "Garages" + val saveAndContinue = "Continue" + } + + object Selectors { + val heading = "#main-content > div > div.govuk-grid-column-two-thirds > form > h1" + val hint = "#main-content > div > div.govuk-grid-column-two-thirds > form > p" + val label1 = "#main-content > div > div.govuk-grid-column-two-thirds > form > div:nth-child(5) > h1 > label" + val label2 = "#main-content > div > div.govuk-grid-column-two-thirds > form > div:nth-child(6) > h1 > label" + val label3 = "#main-content > div > div.govuk-grid-column-two-thirds > form > div:nth-child(7) > h1 > label" + val saveAndContinue = "#continue" + } + + val address = "5 Brixham Marina, Berry Head Road, Brixham, Devon, TQ5 9BW" + + val form = HowManyParkingSpacesOrGaragesIncludedInRentForm.form.fillAndValidate(HowManyParkingSpacesOrGaragesIncludedInRentForm(10000,0,0)) + val mockInputText: InputText = inject[InputText] + def generateInputText(form: Form[HowManyParkingSpacesOrGaragesIncludedInRentForm], inputFieldName: String)(implicit messages: Messages): HtmlFormat.Appendable = { + mockInputText( + form = form, + id = inputFieldName, + name = inputFieldName, + label = messages(s"howManyParkingSpacesOrGaragesIncludedInRent.$inputFieldName.label"), + headingMessageArgs = Seq("govuk-fieldset__legend govuk-fieldset__legend--s"), + isPageHeading = true, + isVisible = true, + classes = Some("govuk-input govuk-input--width-5"), + ) + } + + val uncoveredSpaces: HtmlFormat.Appendable = generateInputText(form, "uncoveredSpaces") + val coveredSpaces: HtmlFormat.Appendable = generateInputText(form, "coveredSpaces") + val garages: HtmlFormat.Appendable = generateInputText(form, "garages") + + "HowManyParkingSpacesOrGaragesIncludedInRentView" must { + val HowManyParkingSpacesOrGaragesIncludedInRentView = view( + form = form, + propertyAddress = address, + uncoveredSpaces = uncoveredSpaces, + coveredSpaces = coveredSpaces, + garages = garages + ) + lazy implicit val document: Document = Jsoup.parse(HowManyParkingSpacesOrGaragesIncludedInRentView.body) + val htmlApply = view.apply(form, address, uncoveredSpaces, coveredSpaces, garages).body + val htmlRender = view.render(form, address, uncoveredSpaces, coveredSpaces, garages, request, messages, mockConfig).body + lazy val htmlF = view.f(form, address, uncoveredSpaces, coveredSpaces, garages) + + "htmlF is not empty" in { + htmlF.toString() must not be empty + } + + "apply must be the same as render" in { + htmlApply mustBe htmlRender + } + + "render is not empty" in { + htmlRender must not be empty + } + + "show correct heading" in { + elementText(Selectors.heading) mustBe Strings.heading + } + + "show correct hint" in { + elementText(Selectors.hint) mustBe Strings.hint + } + + "show correct label 1" in { + elementText(Selectors.label1) mustBe Strings.label1 + } + + "show correct label 2" in { + elementText(Selectors.label2) mustBe Strings.label2 + } + + "show correct label 3" in { + elementText(Selectors.label3) mustBe Strings.label3 + } + + "show correct continue button" in { + elementText(Selectors.saveAndContinue) mustBe Strings.saveAndContinue + } + } +} +