Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
)
}
}

Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final case class RaldUserAnswers(
hasAnotherRentPeriod: Option[Boolean] = None,
whatYourRentIncludes: Option[WhatYourRentIncludes] = None,
doesYourRentIncludeParking: Option[Boolean] = None,
howManyParkingSpacesOrGaragesIncludedInRent: Option[HowManyParkingSpacesOrGarages] = None
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
10 changes: 10 additions & 0 deletions app/uk/gov/hmrc/ngrraldfrontend/repo/RaldRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading