diff --git a/app/controllers/lettingHistory/CompletedLettingsController.scala b/app/controllers/lettingHistory/CompletedLettingsController.scala index fbb646dca..e8e80a5a5 100644 --- a/app/controllers/lettingHistory/CompletedLettingsController.scala +++ b/app/controllers/lettingHistory/CompletedLettingsController.scala @@ -52,17 +52,17 @@ class CompletedLettingsController @Inject ( hasCompletedLettings <- lettingHistory.hasCompletedLettings yield freshForm.fill(hasCompletedLettings) - Ok(theView(filledForm.getOrElse(freshForm), previousRentPeriod, backLinkUrl)) + Ok(theView(filledForm.getOrElse(freshForm), previousRentalPeriod, backLinkUrl)) } def submit: Action[AnyContent] = (Action andThen sessionRefiner).async { implicit request => continueOrSaveAsDraft[AnswersYesNo]( theForm, - theFormWithErrors => successful(BadRequest(theView(theFormWithErrors, previousRentPeriod, backLinkUrl))), + theFormWithErrors => successful(BadRequest(theView(theFormWithErrors, previousRentalPeriod, backLinkUrl))), hasCompletedLettings => given Session = request.sessionData - for updatedSession <- repository.saveOrUpdateSession(withCompletedLettings(hasCompletedLettings)) - yield navigator.redirect(fromPage = CompletedLettingsPageId, updatedSession) + for savedSession <- repository.saveOrUpdateSession(withCompletedLettings(hasCompletedLettings)) + yield navigator.redirect(currentPage = CompletedLettingsPageId, savedSession) ) } diff --git a/app/controllers/lettingHistory/OccupierDetailController.scala b/app/controllers/lettingHistory/OccupierDetailController.scala new file mode 100644 index 000000000..aef0b98e9 --- /dev/null +++ b/app/controllers/lettingHistory/OccupierDetailController.scala @@ -0,0 +1,77 @@ +/* + * 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 controllers.lettingHistory + +import actions.{SessionRequest, WithSessionRefiner} +import controllers.FORDataCaptureController +import form.lettingHistory.OccupierDetailForm.theForm +import models.Session +import models.submissions.lettingHistory.LettingHistory.byAddingOccupierNameAndAddress +import models.submissions.lettingHistory.OccupierDetail +import navigation.LettingHistoryNavigator +import navigation.identifiers.OccupierDetailPageId +import play.api.i18n.I18nSupport +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepo +import views.html.lettingHistory.occupierDetail as OccupierDetailView + +import javax.inject.{Inject, Named} +import scala.concurrent.ExecutionContext +import scala.concurrent.Future.successful + +class OccupierDetailController @Inject ( + mcc: MessagesControllerComponents, + navigator: LettingHistoryNavigator, + theView: OccupierDetailView, + sessionRefiner: WithSessionRefiner, + @Named("session") repository: SessionRepo +)(using ec: ExecutionContext) + extends FORDataCaptureController(mcc) + with WelshJourneySupport + with I18nSupport: + + def show(maybeIndex: Option[Int] = None): Action[AnyContent] = (Action andThen sessionRefiner).apply { + implicit request => + val completedLettings = ( + for lettingHistory <- request.sessionData.lettingHistory.toList + yield lettingHistory.completedLettings + ).flatten + + val freshForm = theForm + val filledForm = + for + index <- maybeIndex + occupierDetail <- completedLettings.lift(index) + yield freshForm.fill(occupierDetail) + + Ok(theView(filledForm.getOrElse(freshForm), previousRentalPeriod, backLinkUrl)) + } + + def submit: Action[AnyContent] = (Action andThen sessionRefiner).async { implicit request => + continueOrSaveAsDraft[OccupierDetail]( + theForm, + theFormWithErrors => successful(BadRequest(theView(theFormWithErrors, previousRentalPeriod, backLinkUrl))), + occupierDetail => + given Session = request.sessionData + val updatedSession = byAddingOccupierNameAndAddress(occupierDetail.name, occupierDetail.address) + for savedSession <- repository.saveOrUpdateSession(updatedSession) + yield navigator.redirect(currentPage = OccupierDetailPageId, savedSession) + ) + } + + private def backLinkUrl(using request: SessionRequest[AnyContent]): Option[String] = + navigator.backLinkUrl(ofPage = OccupierDetailPageId) diff --git a/app/controllers/lettingHistory/PermanentResidentsController.scala b/app/controllers/lettingHistory/PermanentResidentsController.scala index f4a5f0f83..c9ca7dff7 100644 --- a/app/controllers/lettingHistory/PermanentResidentsController.scala +++ b/app/controllers/lettingHistory/PermanentResidentsController.scala @@ -61,8 +61,8 @@ class PermanentResidentsController @Inject() ( theFormWithErrors => successful(BadRequest(theView(theFormWithErrors, backLinkUrl))), hasPermanentResidents => given Session = request.sessionData - for updatedSession <- repository.saveOrUpdateSession(withPermanentResidents(hasPermanentResidents)) - yield navigator.redirect(fromPage = PermanentResidentsPageId, updatedSession) + for savedSession <- repository.saveOrUpdateSession(withPermanentResidents(hasPermanentResidents)) + yield navigator.redirect(currentPage = PermanentResidentsPageId, savedSession) ) } diff --git a/app/controllers/lettingHistory/ResidentDetailController.scala b/app/controllers/lettingHistory/ResidentDetailController.scala index 2e77789d6..75f2adb6f 100644 --- a/app/controllers/lettingHistory/ResidentDetailController.scala +++ b/app/controllers/lettingHistory/ResidentDetailController.scala @@ -43,16 +43,21 @@ class ResidentDetailController @Inject() ( extends FORDataCaptureController(mcc) with I18nSupport: - def show(index: Option[Int] = None): Action[AnyContent] = (Action andThen sessionRefiner).apply { implicit request => - val freshForm = theForm - val filledForm = - for - idx <- index - lettingHistory <- request.sessionData.lettingHistory - residentDetail <- lettingHistory.permanentResidents.lift(idx) - yield freshForm.fill(residentDetail) + def show(maybeIndex: Option[Int] = None): Action[AnyContent] = (Action andThen sessionRefiner).apply { + implicit request => + val permanentResidents = ( + for lettingHistory <- request.sessionData.lettingHistory.toList + yield lettingHistory.permanentResidents + ).flatten - Ok(theView(filledForm.getOrElse(freshForm), backLinkUrl)) + val freshForm = theForm + val filledForm = + for + index <- maybeIndex + residentDetail <- permanentResidents.lift(index) + yield freshForm.fill(residentDetail) + + Ok(theView(filledForm.getOrElse(freshForm), backLinkUrl)) } def submit: Action[AnyContent] = (Action andThen sessionRefiner).async { implicit request => @@ -61,8 +66,8 @@ class ResidentDetailController @Inject() ( theFormWithErrors => successful(BadRequest(theView(theFormWithErrors, backLinkUrl))), residentDetail => given Session = request.sessionData - for updatedSession <- repository.saveOrUpdateSession(byAddingPermanentResident(residentDetail)) - yield navigator.redirect(fromPage = ResidentDetailPageId, updatedSession) + for savedSession <- repository.saveOrUpdateSession(byAddingPermanentResident(residentDetail)) + yield navigator.redirect(currentPage = ResidentDetailPageId, savedSession) ) } diff --git a/app/controllers/lettingHistory/ResidentListController.scala b/app/controllers/lettingHistory/ResidentListController.scala index 5d46a3920..8909b1636 100644 --- a/app/controllers/lettingHistory/ResidentListController.scala +++ b/app/controllers/lettingHistory/ResidentListController.scala @@ -22,10 +22,10 @@ import form.confirmableActionForm.confirmableActionForm as theRemoveConfirmation import form.lettingHistory.ResidentListForm.theForm as theListForm import models.Session import models.submissions.common.{AnswerYes, AnswersYesNo} -import models.submissions.lettingHistory.LettingHistory.byRemovingPermanentResidentAt +import models.submissions.lettingHistory.LettingHistory.{byRemovingPermanentResidentAt, permanentResidents} import models.submissions.lettingHistory.{LettingHistory, ResidentDetail} import navigation.LettingHistoryNavigator -import navigation.identifiers.ResidentListPageId +import navigation.identifiers.{ResidentListPageId, ResidentRemovePageId} import play.api.data.Form import play.api.i18n.I18nSupport import play.api.mvc.{Action, AnyContent, MessagesControllerComponents, Result} @@ -49,30 +49,34 @@ class ResidentListController @Inject() ( with I18nSupport: def show: Action[AnyContent] = (Action andThen sessionRefiner).apply { implicit request => - Ok(theListView(theListForm, backLinkUrl, LettingHistory.permanentResidents(request.sessionData))) + Ok(theListView(theListForm, permanentResidents(request.sessionData), backLinkUrl)) } def remove(index: Int): Action[AnyContent] = (Action andThen sessionRefiner).async { implicit request => - this.foldResidentDetailAt(index) { residentialDetail => + withResidentDetailAt(index) { residentialDetail => successful(Ok(renderTheConfirmationViewWith(theRemoveConfirmationForm, residentialDetail, index))) } } def performRemove(index: Int): Action[AnyContent] = (Action andThen sessionRefiner).async { implicit request => - this.foldResidentDetailAt(index) { residentialDetail => + withResidentDetailAt(index) { residentialDetail => theRemoveConfirmationForm .bindFromRequest() .fold( formWithErrors => successful(BadRequest(renderTheConfirmationViewWith(formWithErrors, residentialDetail, index))), answer => - if answer == AnswerYes then - given Session = request.sessionData - for updatedSession <- repository.saveOrUpdateSession(byRemovingPermanentResidentAt(index)) - yield Redirect(routes.ResidentListController.show) - else - // AnswerNo - successful(Redirect(routes.ResidentListController.show)) + val eventuallySavedSession = + if answer == AnswerYes then + given Session = request.sessionData + for savedSession <- repository.saveOrUpdateSession(byRemovingPermanentResidentAt(index)) + yield savedSession + else + // AnswerNo + successful(request.sessionData) + + for savedSession <- eventuallySavedSession + yield navigator.redirect(currentPage = ResidentRemovePageId, savedSession) ) } } @@ -83,13 +87,13 @@ class ResidentListController @Inject() ( theFormWithErrors => successful( BadRequest( - theListView(theFormWithErrors, backLinkUrl, LettingHistory.permanentResidents(request.sessionData)) + theListView(theFormWithErrors, permanentResidents(request.sessionData), backLinkUrl) ) ), hasMoreResidents => successful( navigator.redirect( - fromPage = ResidentListPageId, + currentPage = ResidentListPageId, updatedSession = request.sessionData, navigationData = Map("hasMoreResidents" -> hasMoreResidents.toString) ) @@ -97,7 +101,7 @@ class ResidentListController @Inject() ( ) } - private def foldResidentDetailAt( + private def withResidentDetailAt( index: Int )(func: ResidentDetail => Future[Result])(using request: SessionRequest[AnyContent]): Future[Result] = LettingHistory diff --git a/app/controllers/lettingHistory/WelshJourneySupport.scala b/app/controllers/lettingHistory/WelshJourneySupport.scala index 01bac1d20..3a2179022 100644 --- a/app/controllers/lettingHistory/WelshJourneySupport.scala +++ b/app/controllers/lettingHistory/WelshJourneySupport.scala @@ -17,14 +17,14 @@ package controllers.lettingHistory import actions.SessionRequest -import models.submissions.lettingHistory.RentPeriod +import models.submissions.lettingHistory.RentalPeriod import play.api.mvc.AnyContent import java.time.LocalDate import java.time.Month.{APRIL, MARCH} trait WelshJourneySupport: - private def lastFiscalYearEnd = { + def lastFiscalYearEnd = { val now = LocalDate.now() if now.getMonth.getValue > 3 then now.getYear @@ -32,9 +32,9 @@ trait WelshJourneySupport: } // The Welsh journey requires 3 years of data instead of 1 year - def previousRentPeriod(using request: SessionRequest[AnyContent]) = + def previousRentalPeriod(using request: SessionRequest[AnyContent]) = val numberOfYearsBack = if request.sessionData.isWelsh then 3 else 1 - RentPeriod( + RentalPeriod( fromDate = LocalDate.of(lastFiscalYearEnd - numberOfYearsBack, APRIL, 1), toDate = LocalDate.of(lastFiscalYearEnd, MARCH, 31) ) diff --git a/app/form/lettingHistory/OccupierDetailForm.scala b/app/form/lettingHistory/OccupierDetailForm.scala new file mode 100644 index 000000000..0cdc5f393 --- /dev/null +++ b/app/form/lettingHistory/OccupierDetailForm.scala @@ -0,0 +1,41 @@ +/* + * 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 form.lettingHistory + +import form.AddressLine2Mapping.validateAddressLineTwo as line2 +import form.BuildingNameNumberMapping.validateBuildingNameNumber as line1 +import form.CountyMapping.validateCounty as county +import form.PostcodeMapping.postcode +import form.TownMapping.validateTown as town +import form.lettingHistory.FieldMappings.nonEmptyText +import models.submissions.lettingHistory.{Address, OccupierDetail} +import play.api.data.Form +import play.api.data.Forms.{mapping, optional} + +object OccupierDetailForm: + val theForm = Form( + mapping( + "name" -> nonEmptyText(errorMessage = "lettingHistory.occupierDetail.name.required"), + "address" -> mapping( + "line1" -> line1, + "line2" -> optional(line2), + "town" -> town, + "county" -> optional(county), + "postcode" -> postcode(requiredError = "error.postcodeAlternativeContact.required") + )(Address.apply)(Address.unapply) + )(OccupierDetail.apply)(OccupierDetail.unapply) + ) diff --git a/app/models/submissions/lettingHistory/Address.scala b/app/models/submissions/lettingHistory/Address.scala new file mode 100644 index 000000000..690a0de8f --- /dev/null +++ b/app/models/submissions/lettingHistory/Address.scala @@ -0,0 +1,32 @@ +/* + * 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 models.submissions.lettingHistory + +import play.api.libs.json.{Format, Json} + +case class Address( + line1: String, + line2: Option[String], + town: String, + county: Option[String], + postcode: String +) + +object Address: + def unapply(obj: Address): Option[(String, Option[String], String, Option[String], String)] = + Some(obj.line1, obj.line2, obj.town, obj.county, obj.postcode) + given Format[Address] = Json.format diff --git a/app/models/submissions/lettingHistory/LettingHistory.scala b/app/models/submissions/lettingHistory/LettingHistory.scala index 547f7fa20..36c5bf158 100644 --- a/app/models/submissions/lettingHistory/LettingHistory.scala +++ b/app/models/submissions/lettingHistory/LettingHistory.scala @@ -24,7 +24,7 @@ case class LettingHistory( hasPermanentResidents: Option[AnswersYesNo] = None, permanentResidents: List[ResidentDetail] = Nil, hasCompletedLettings: Option[AnswersYesNo] = None, - completedLettings: List[ResidentDetail] = Nil + completedLettings: List[OccupierDetail] = Nil ) object LettingHistory: @@ -119,4 +119,44 @@ object LettingHistory: lettingHistory.copy(hasCompletedLettings = Some(AnswerNo), completedLettings = Nil) ) + def byAddingOccupierNameAndAddress(name: String, address: Address)(using session: Session): Session = + val occupierDetail = OccupierDetail(name, address) + val ifEmpty = + LettingHistory( + hasCompletedLettings = Some(AnswerYes), + completedLettings = List(occupierDetail) + ) + + val copyFunc: LettingHistory => LettingHistory = { lettingHistory => + lettingHistory.completedLettings.zipWithIndex + .find { (occupier, _) => + // find the eventually existing resident by name (and more importantly its index) + occupier.name == name + } + .map { (_, foundIndex) => + // patch the occupier detail if it exists at foundIndex + val patchedOccupier = lettingHistory + .completedLettings(foundIndex) + .copy(name = name, address = address) + + val patchedCompletedLettings = lettingHistory.completedLettings.patch(foundIndex, List(patchedOccupier), 1) + val copiedLettingHistory = lettingHistory.copy(completedLettings = patchedCompletedLettings) + copiedLettingHistory + } + .getOrElse { + // not found ... then append it to the list of completed lettings + val extendedCompletedLettings = lettingHistory.completedLettings.:+(occupierDetail) + val copiedLettingHistory = lettingHistory.copy(completedLettings = extendedCompletedLettings) + copiedLettingHistory + } + } + val updatedSession = session.lettingHistory.fold(ifEmpty)(copyFunc) + session.copy(lettingHistory = Some(updatedSession)) + + def completedLettings(session: Session): List[OccupierDetail] = + for + lettingHistory <- session.lettingHistory.toList + completedLettings <- lettingHistory.completedLettings + yield completedLettings + given Format[LettingHistory] = Json.format diff --git a/app/models/submissions/lettingHistory/OccupierDetail.scala b/app/models/submissions/lettingHistory/OccupierDetail.scala new file mode 100644 index 000000000..5b32a4b44 --- /dev/null +++ b/app/models/submissions/lettingHistory/OccupierDetail.scala @@ -0,0 +1,29 @@ +/* + * 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 models.submissions.lettingHistory + +import play.api.libs.json.{Format, Json} + +case class OccupierDetail( + name: String, + address: Address + // TODO rental: RentalPeriod +) + +object OccupierDetail: + def unapply(obj: OccupierDetail): Option[(String, Address)] = Some(obj.name, obj.address) + given Format[OccupierDetail] = Json.format diff --git a/app/models/submissions/lettingHistory/RentPeriod.scala b/app/models/submissions/lettingHistory/RentalPeriod.scala similarity index 73% rename from app/models/submissions/lettingHistory/RentPeriod.scala rename to app/models/submissions/lettingHistory/RentalPeriod.scala index 137365e18..59c30238d 100644 --- a/app/models/submissions/lettingHistory/RentPeriod.scala +++ b/app/models/submissions/lettingHistory/RentalPeriod.scala @@ -16,12 +16,15 @@ package models.submissions.lettingHistory +import play.api.libs.json.{Format, Json} + import java.time.LocalDate -case class RentPeriod( +case class RentalPeriod( fromDate: LocalDate, toDate: LocalDate ) -object RentPeriod: - def unapply(obj: RentPeriod): Option[(LocalDate, LocalDate)] = Some((obj.fromDate, obj.toDate)) +object RentalPeriod: + def unapply(obj: RentalPeriod): Option[(LocalDate, LocalDate)] = Some((obj.fromDate, obj.toDate)) + given Format[RentalPeriod] = Json.format diff --git a/app/models/submissions/lettingHistory/ResidentDetail.scala b/app/models/submissions/lettingHistory/ResidentDetail.scala index 4e169cfd1..7db76783a 100644 --- a/app/models/submissions/lettingHistory/ResidentDetail.scala +++ b/app/models/submissions/lettingHistory/ResidentDetail.scala @@ -18,7 +18,10 @@ package models.submissions.lettingHistory import play.api.libs.json.{Format, Json} -case class ResidentDetail(name: String, address: String) +case class ResidentDetail( + name: String, + address: String +) object ResidentDetail: def unapply(obj: ResidentDetail): Option[(String, String)] = Some(obj.name, obj.address) diff --git a/app/models/submissions/lettingHistory/SensitiveAddress.scala b/app/models/submissions/lettingHistory/SensitiveAddress.scala new file mode 100644 index 000000000..fc580ec4c --- /dev/null +++ b/app/models/submissions/lettingHistory/SensitiveAddress.scala @@ -0,0 +1,53 @@ +/* + * 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 models.submissions.lettingHistory + +import uk.gov.hmrc.crypto.Sensitive +import uk.gov.hmrc.crypto.Sensitive.SensitiveString + +case class SensitiveAddress( + line1: SensitiveString, + line2: Option[SensitiveString], + town: SensitiveString, + county: Option[SensitiveString], + postcode: SensitiveString +) extends Sensitive[Address]: + + override def decryptedValue: Address = Address( + line1.decryptedValue, + line2.map(_.decryptedValue), + town.decryptedValue, + county.map(_.decryptedValue), + postcode.decryptedValue + ) + +object SensitiveAddress: + import crypto.MongoCrypto + import crypto.SensitiveFormats.sensitiveStringFormat + import play.api.libs.json.{Format, Json} + + implicit def format(using crypto: MongoCrypto): Format[SensitiveAddress] = Json.format + + // encryption method + def apply(address: Address): SensitiveAddress = + SensitiveAddress( + SensitiveString(address.line1), + address.line2.map(SensitiveString(_)), + SensitiveString(address.town), + address.county.map(SensitiveString(_)), + SensitiveString(address.postcode) + ) diff --git a/app/models/submissions/lettingHistory/SensitiveLettingHistory.scala b/app/models/submissions/lettingHistory/SensitiveLettingHistory.scala index 36901871d..a2e9cd740 100644 --- a/app/models/submissions/lettingHistory/SensitiveLettingHistory.scala +++ b/app/models/submissions/lettingHistory/SensitiveLettingHistory.scala @@ -23,7 +23,7 @@ case class SensitiveLettingHistory( hasPermanentResidents: Option[AnswersYesNo], permanentResidents: List[SensitiveResidentDetail], hasCompletedLettings: Option[AnswersYesNo], - completedLettings: List[SensitiveResidentDetail] + completedLettings: List[SensitiveOccupierDetail] ) extends Sensitive[LettingHistory]: override def decryptedValue: LettingHistory = @@ -45,5 +45,5 @@ object SensitiveLettingHistory: lettingHistory.hasPermanentResidents, lettingHistory.permanentResidents.map(SensitiveResidentDetail(_)), lettingHistory.hasCompletedLettings, - lettingHistory.completedLettings.map(SensitiveResidentDetail(_)) + lettingHistory.completedLettings.map(SensitiveOccupierDetail(_)) ) diff --git a/app/models/submissions/lettingHistory/SensitiveOccupierDetail.scala b/app/models/submissions/lettingHistory/SensitiveOccupierDetail.scala new file mode 100644 index 000000000..709dbc8fa --- /dev/null +++ b/app/models/submissions/lettingHistory/SensitiveOccupierDetail.scala @@ -0,0 +1,45 @@ +/* + * 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 models.submissions.lettingHistory + +import uk.gov.hmrc.crypto.Sensitive +import uk.gov.hmrc.crypto.Sensitive.SensitiveString + +case class SensitiveOccupierDetail( + name: SensitiveString, + address: SensitiveAddress + // TODO rental: RentalPeriod +) extends Sensitive[OccupierDetail]: + + override def decryptedValue: OccupierDetail = + OccupierDetail( + name.decryptedValue, + address.decryptedValue + ) + +object SensitiveOccupierDetail: + import crypto.MongoCrypto + import crypto.SensitiveFormats.sensitiveStringFormat + import play.api.libs.json.{Format, Json} + implicit def format(using crypto: MongoCrypto): Format[SensitiveOccupierDetail] = Json.format + + // encryption method + def apply(occupierDetail: OccupierDetail): SensitiveOccupierDetail = + SensitiveOccupierDetail( + SensitiveString(occupierDetail.name), + SensitiveAddress(occupierDetail.address) + ) diff --git a/app/navigation/LettingHistoryNavigator.scala b/app/navigation/LettingHistoryNavigator.scala index 134b4560d..1c0ebe83c 100644 --- a/app/navigation/LettingHistoryNavigator.scala +++ b/app/navigation/LettingHistoryNavigator.scala @@ -23,7 +23,7 @@ import models.Session import models.submissions.common.AnswerYes import models.submissions.lettingHistory.LettingHistory import models.submissions.lettingHistory.LettingHistory.{MaxNumberOfPermanentResidents, hasCompletedLettings, hasPermanentResidents, permanentResidents} -import navigation.identifiers.{CompletedLettingsPageId, Identifier, PermanentResidentsPageId, ResidentDetailPageId, ResidentListPageId} +import navigation.identifiers.{CompletedLettingsPageId, Identifier, OccupierDetailPageId, PermanentResidentsPageId, ResidentDetailPageId, ResidentListPageId, ResidentRemovePageId} import play.api.mvc.Results.Redirect import play.api.mvc.{AnyContent, Call, Result} import uk.gov.hmrc.http.HeaderCarrier @@ -58,25 +58,28 @@ class LettingHistoryNavigator @Inject() (audit: Audit) extends Navigator(audit): if fromPage == PermanentResidentsPageId.toString then routes.PermanentResidentsController.show else routes.ResidentListController.show + }, + OccupierDetailPageId -> { (_, _) => + Some(routes.CompletedLettingsController.show) } ) def backLinkUrl(ofPage: Identifier, navigationData: Map[String, String] = Map.empty)(using request: SessionRequest[AnyContent] ): Option[String] = - val navigationDataAndFromPage = - request.session - .get("from") - .fold(ifEmpty = navigationData)(from => navigationData + ("from" -> from)) - val call = for sessionToMaybeCallFunc <- backwardNavigationMap.get(ofPage) - backwardCall <- sessionToMaybeCallFunc.apply(request.sessionData, navigationDataAndFromPage) + backwardCall <- sessionToMaybeCallFunc.apply(request.sessionData, enriched(navigationData)) yield backwardCall call.map(_.toString) + private def enriched(navigationData: Map[String, String] = Map.empty)(using + request: SessionRequest[AnyContent] + ) = + request.session.get("from").fold(ifEmpty = navigationData)(from => navigationData + ("from" -> from)) + // ---------------------------------------------------------------------------------------------------------------- /* @@ -101,47 +104,54 @@ class LettingHistoryNavigator @Inject() (audit: Audit) extends Navigator(audit): ResidentDetailPageId -> { (_, _) => Some(routes.ResidentListController.show) }, + ResidentRemovePageId -> { (_, _) => + Some(routes.ResidentListController.show) + }, ResidentListPageId -> { (updatedSession, navigationData) => - // Note that navigationData.isDefinedAt("hasMoreResidents") is certainly true! See ResidentListController.submit() - if navigationData("hasMoreResidents") == AnswerYes.toString - then - if permanentResidents(updatedSession).size < MaxNumberOfPermanentResidents - then Some(routes.ResidentDetailController.show(index = None)) + // assert(navigationData.isDefinedAt("hasMoreResidents")) + for answer <- Some(navigationData("hasMoreResidents")) + yield + if answer == AnswerYes.toString + then + if permanentResidents(updatedSession).size < MaxNumberOfPermanentResidents + then routes.ResidentDetailController.show(index = None) + else + // TODO Introduce the controllers.lettingHistory.MaxPermanentResidentsController + Call("GET", "/path/to/max-permanent-residents") else - // TODO Introduce the controllers.lettingHistory.MaxPermanentResidentsController - Some(Call("GET", "/path/to/max-permanent-residents")) - else - // AnswerNo - Some(routes.CompletedLettingsController.show) + // AnswerNo + routes.CompletedLettingsController.show }, CompletedLettingsPageId -> { (updatedSession, _) => for answer <- hasCompletedLettings(updatedSession) yield if answer == AnswerYes - then - // TODO Introduce the OccupierDetailController - Call("GET", "/path/to/occupier-detail") + then routes.OccupierDetailController.show(index = None) else // AnswerNo - // TODO Introduce the OccupierDetailController + // TODO Introduce the HowManyNightsController Call("GET", "/path/to/how-many-nights") + }, + OccupierDetailPageId -> { (_, _) => + // TODO Introduce the RentalPeriodController + Some(Call("GET", "/path/to/rental-period")) } ) @deprecated override val routeMap: Map[Identifier, Session => Call] = Map.empty - def redirect(fromPage: Identifier, updatedSession: Session, navigationData: Map[String, String] = Map.empty)(using + def redirect(currentPage: Identifier, updatedSession: Session, navigationData: Map[String, String] = Map.empty)(using hc: HeaderCarrier, request: SessionRequest[AnyContent] ): Result = { val nextCall = - for call <- forwardNavigationMap(fromPage)(updatedSession, navigationData) + for call <- forwardNavigationMap(currentPage)(updatedSession, enriched(navigationData)) yield auditNextUrl(updatedSession)(call) nextCall match case Some(call) => - Redirect(call).withSession(request.session + ("from" -> fromPage.toString)) + Redirect(call).withSession(request.session + ("from" -> currentPage.toString)) case _ => throw new Exception("NavigatorIllegalStage : couldn't determine redirect call") } diff --git a/app/navigation/identifiers/LettingHistoryIdentifiers.scala b/app/navigation/identifiers/LettingHistoryIdentifiers.scala index eba5cd9f0..41653eba0 100644 --- a/app/navigation/identifiers/LettingHistoryIdentifiers.scala +++ b/app/navigation/identifiers/LettingHistoryIdentifiers.scala @@ -22,16 +22,24 @@ case object PermanentResidentsPageId extends Identifier: case object ResidentDetailPageId extends Identifier: override def toString: String = "residentDetailPage" +case object ResidentRemovePageId extends Identifier: + override def toString: String = "residentRemovePage" + case object ResidentListPageId extends Identifier: override def toString: String = "residentListPage" case object CompletedLettingsPageId extends Identifier: override def toString: String = "commercialLettingsPage" +case object OccupierDetailPageId extends Identifier: + override def toString: String = "occupierDetailPage" + extension (string: String) def asPageIdentifier: Option[Identifier] = string match case "permanentResidentsPage" => Some(PermanentResidentsPageId) case "residentDetailPage" => Some(ResidentDetailPageId) + case "residentRemovePage" => Some(ResidentRemovePageId) case "residentListPage" => Some(ResidentListPageId) case "completedLettingsPage" => Some(CompletedLettingsPageId) + case "occupierDetailPage" => Some(OccupierDetailPageId) case _ => None diff --git a/app/views/includes/fieldset.scala.html b/app/views/includes/fieldset.scala.html new file mode 100644 index 000000000..6e7e3de45 --- /dev/null +++ b/app/views/includes/fieldset.scala.html @@ -0,0 +1,48 @@ +@* + * 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. + *@ + +@* +* Design your service using GOV.UK styles, components and patterns +* See https://design-system.service.gov.uk/components/fieldset/ +*@ + +@import uk.gov.hmrc.govukfrontend.views.html.components.GovukFieldset +@import uk.gov.hmrc.govukfrontend.views.Aliases.Fieldset +@import uk.gov.hmrc.govukfrontend.views.Aliases.Legend +@import uk.gov.hmrc.govukfrontend.views.Aliases.Text + +@( + govukFieldset: GovukFieldset, + legendContent: String = "", + legendKey: String = "", + classes: String = "", + isPageHeading: Boolean = false +)( + htmlContent: Html +)( + implicit messages: Messages +) + +@govukFieldset( + Fieldset( + legend = Some(Legend( + content = Text(value = if (legendKey != "") messages(legendKey) else legendContent), + classes = classes, + isPageHeading = isPageHeading + )), + html = htmlContent + ) +) diff --git a/app/views/includes/hint.scala.html b/app/views/includes/hint.scala.html new file mode 100644 index 000000000..c9b5f7830 --- /dev/null +++ b/app/views/includes/hint.scala.html @@ -0,0 +1,36 @@ +@* + * 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. + *@ + +@import uk.gov.hmrc.govukfrontend.views.html.components.GovukHint +@import uk.gov.hmrc.govukfrontend.views.Aliases.Hint +@import uk.gov.hmrc.govukfrontend.views.Aliases.Text + +@( + govukHint: GovukHint, + hintContent: String = "", + hintKey: String = "", + classes: String = "", +)( + implicit messages: Messages +) + +@govukHint( + Hint( + content = Text(value = if(hintKey != "") messages(hintKey) else hintContent), + classes = classes + ) +) + diff --git a/app/views/lettingHistory/completedLettings.scala.html b/app/views/lettingHistory/completedLettings.scala.html index ecafa4f7a..a77fccc16 100644 --- a/app/views/lettingHistory/completedLettings.scala.html +++ b/app/views/lettingHistory/completedLettings.scala.html @@ -17,13 +17,13 @@ @import actions.SessionRequest @import controllers.lettingHistory.routes @import models.submissions.common.AnswersYesNo -@import models.submissions.lettingHistory.RentPeriod +@import models.submissions.lettingHistory.RentalPeriod as FiscalPeriod @import uk.gov.hmrc.govukfrontend.views.html.components.{FormWithCSRF, GovukButton, GovukRadios} @import util.DateUtilLocalised @this(layout: Layout, formWithCSRF: FormWithCSRF, govukRadios: GovukRadios, govukButton: GovukButton, dateUtil: DateUtilLocalised) -@(theForm: Form[AnswersYesNo], period: RentPeriod, backLink: Option[String])(implicit request: SessionRequest[AnyContent], messages: Messages) +@(theForm: Form[AnswersYesNo], fiscalPeriod: FiscalPeriod, backLink: Option[String])(implicit request: SessionRequest[AnyContent], messages: Messages) @layout( @@ -53,8 +53,8 @@ isHeading = true, legend = Some(messages( "lettingHistory.hasCompletedLettings.legend", - dateUtil.formatDayMonthAbbrYear(period.fromDate), - dateUtil.formatDayMonthAbbrYear(period.toDate) + dateUtil.formatDayMonthAbbrYear(fiscalPeriod.fromDate), + dateUtil.formatDayMonthAbbrYear(fiscalPeriod.toDate) )), ) @includes.continueSaveAsDraftButtons(govukButton) diff --git a/app/views/lettingHistory/occupierDetail.scala.html b/app/views/lettingHistory/occupierDetail.scala.html new file mode 100644 index 000000000..8c366d719 --- /dev/null +++ b/app/views/lettingHistory/occupierDetail.scala.html @@ -0,0 +1,107 @@ +@* + * 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. + *@ + +@import actions.SessionRequest +@import models.pages.Summary +@import models.submissions.lettingHistory.OccupierDetail +@import models.submissions.lettingHistory.RentalPeriod as FiscalPeriod +@import controllers.lettingHistory.routes +@import uk.gov.hmrc.govukfrontend.views.html.components.{FormWithCSRF, GovukFieldset, GovukHint, GovukButton, GovukInput} +@import util.DateUtilLocalised + +@this(layout: Layout, formWithCSRF: FormWithCSRF, govukFieldset: GovukFieldset, govukHint: GovukHint, govukInput: GovukInput, govukButton: GovukButton, dateUtil: DateUtilLocalised) + +@(theForm: Form[OccupierDetail], fiscalPeriod: FiscalPeriod, backLink: Option[String])(implicit request: SessionRequest[?], messages: Messages) + +@layout( + theForm = theForm, + summary = Some(request.sessionData.toSummary), + pageHeading = messages("lettingHistory.occupierDetail.heading"), + backLinkUrl = backLink, + showSection = true, + sectionName = messages("label.section.lettingHistory"), +) { + +

+ @messages( + "lettingHistory.occupierDetail.subheading", + dateUtil.formatDayMonthAbbrYear(fiscalPeriod.fromDate), + dateUtil.formatDayMonthAbbrYear(fiscalPeriod.toDate) + ) +

+ + @formWithCSRF(action = routes.OccupierDetailController.submit) { + @includes.textInput( + govukInput, + theForm, + id = "name", + classes = "govuk-!-width-two-thirds", + labelContent = "lettingHistory.occupierDetail.name.label", + labelClasses = "govuk-!-font-weight-bold", + ) + + @includes.fieldset( + govukFieldset, + legendContent = messages("lettingHistory.occupierDetail.address.legend"), + classes = "govuk-fieldset__legend--s" + ) { + @includes.hint( + govukHint, + hintContent = messages("lettingHistory.occupierDetail.address.hint"), + classes = "govuk-!-width-two-thirds govuk-!-margin-bottom-4" + ) + @includes.textInput( + govukInput, + theForm, + id = "address.line1", + classes = "govuk-!-width-two-thirds", + labelContent = "buildingNameNumber", + ) + @includes.textInput( + govukInput, + theForm, + id = "address.line2", + classes = "govuk-!-width-two-thirds", + labelContent = "street1", + optional = true + ) + @includes.textInput( + govukInput, + theForm, + id = "address.town", + classes = "govuk-!-width-one-half", + labelContent = "town" + ) + @includes.textInput( + govukInput, + theForm, + id = "address.county", + classes = "govuk-!-width-one-half", + labelContent = "county", + optional = true + ) + @includes.textInput( + govukInput, + theForm, + id = "address.postcode", + classes = "govuk-!-width-one-quarter", + labelContent = "postcode" + ) + } + + @includes.continueSaveAsDraftButtons(govukButton) + } +} \ No newline at end of file diff --git a/app/views/lettingHistory/residentList.scala.html b/app/views/lettingHistory/residentList.scala.html index 7e963efbb..68aa2bf93 100644 --- a/app/views/lettingHistory/residentList.scala.html +++ b/app/views/lettingHistory/residentList.scala.html @@ -24,28 +24,29 @@ @this(layout: Layout, formWithCSRF: FormWithCSRF, govukSummaryList: GovukSummaryList, govukRadios: GovukRadios, govukButton: GovukButton) -@(theForm: Form[AnswersYesNo], backLink: Option[String], residentsDetails: List[ResidentDetail])(implicit request: SessionRequest[?], messages: Messages) +@(theForm: Form[AnswersYesNo], permanentResidents: List[ResidentDetail], backLink: Option[String])(implicit request: SessionRequest[?], messages: Messages) -@residentsCount = @{ - residentsDetails.length -} +@fromPage = @{ request.session.get("from") } + +@residentsCount = @{ permanentResidents.length } @pageHeadingMessageKey = @{ - if(residentsCount == 1) "lettingHistory.residentList.heading.singular" + if (residentsCount == 0 && fromPage == Some("residentRemovePage")) "lettingHistory.residentList.heading.removed" + else if (residentsCount == 1) "lettingHistory.residentList.heading.singular" else "lettingHistory.residentList.heading.plural" } @layout( - theForm = theForm, summary = Some(request.sessionData.toSummary), pageHeading = messages(pageHeadingMessageKey, residentsCount), backLinkUrl = backLink, showSection = true, sectionName = messages("label.section.lettingHistory"), + theForm = theForm, ) { @govukSummaryList(SummaryList( - rows = residentsDetails.zipWithIndex.map { (resident, index) => + rows = permanentResidents.zipWithIndex.map { (resident, index) => SummaryListRow( key = Key( content = Text(resident.name), @@ -71,7 +72,6 @@ ) } )) - @formWithCSRF(action = routes.ResidentListController.submit) { @includes.radioButtonsYesNo( govukRadios, diff --git a/conf/lettingHistory.routes b/conf/lettingHistory.routes index d02417bbe..d67294494 100644 --- a/conf/lettingHistory.routes +++ b/conf/lettingHistory.routes @@ -12,4 +12,8 @@ POST /letting-history/resident-remove controllers.lett POST /letting-history/resident-list controllers.lettingHistory.ResidentListController.submit GET /letting-history/commercial-lettings controllers.lettingHistory.CompletedLettingsController.show -POST /letting-history/commercial-lettings controllers.lettingHistory.CompletedLettingsController.submit \ No newline at end of file +POST /letting-history/commercial-lettings controllers.lettingHistory.CompletedLettingsController.submit + + +GET /letting-history/occupier-detail controllers.lettingHistory.OccupierDetailController.show(index: Option[Int] ?= None) +POST /letting-history/occupier-detail controllers.lettingHistory.OccupierDetailController.submit \ No newline at end of file diff --git a/conf/messages b/conf/messages index 473236b38..82a7780eb 100644 --- a/conf/messages +++ b/conf/messages @@ -1616,6 +1616,7 @@ lettingHistory.residentDetail.address.label = Resident''s address lettingHistory.residentDetail.address.required = Enter the address of the permanent resident lettingHistory.residentDetail.address.hint = Enter any separately named or numbered address, or describe the part of the property they occupy. +lettingHistory.residentList.heading.removed = You have removed all permanent residents of the property lettingHistory.residentList.heading.singular = You have added {0} resident lettingHistory.residentList.heading.plural = You have added {0} residents lettingHistory.residentList.hasMoreResidents.label = Are any other parts of the property occupied by tenants or employees as their main residence? @@ -1630,6 +1631,13 @@ lettingHistory.completedLettings.paragraph.3.point.2 = let out to staff or perma lettingHistory.hasCompletedLettings.legend = Was any part of your property let in periods of 29 nights and longer between {0} and {1}. lettingHistory.hasCompletedLettings.required = Select yes if the property had any completed lettings in the period. +lettingHistory.occupierDetail.heading = Temporary occupier''s details +lettingHistory.occupierDetail.subheading = We need the contact details of each occupier who rented part of your property for 29 nights or more between {0} and {1}. Do not include any holiday lettings from this period. You will have the opportunity to add additional occupiers if required. +label.lettingHistory.occupierDetail.name.label = Occupier''s name +lettingHistory.occupierDetail.name.required = Enter the name of the temporary occupier +lettingHistory.occupierDetail.address.legend = Occupier''s address +lettingHistory.occupierDetail.address.hint = This should be the address they provided when they rented the property. + # 6010 TYPES ############################## diff --git a/conf/messages.cy b/conf/messages.cy index 99a24678a..f1fcef554 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -1584,6 +1584,22 @@ lettingHistory.residentList.heading.plural = You have added {0} residents lettingHistory.residentList.hasMoreResidents.label = Are any other parts of the property occupied by tenants or employees as their main residence? lettingHistory.residentList.hasMoreResidents.required = Select yes if the property has more occupiers +lettingHistory.completedLettings.heading = Completed commercial lettings - 29 nights and longer +lettingHistory.completedLettings.paragraph.1 = Declare if any parts of your property were let commercially - in periods of 29 nights and longer - within the stated timeframe. +lettingHistory.completedLettings.paragraph.2 = Include student lets, long-term bookings, and any consecutive tenancies totalling 29 nights or more. +lettingHistory.completedLettings.paragraph.3 = Do not include any parts of the property: +lettingHistory.completedLettings.paragraph.3.point.1 = used by family and friends +lettingHistory.completedLettings.paragraph.3.point.2 = let out to staff or permanent residents. +lettingHistory.hasCompletedLettings.legend = Was any part of your property let in periods of 29 nights and longer between {0} and {1}. +lettingHistory.hasCompletedLettings.required = Select yes if the property had any completed lettings in the period. + +lettingHistory.occupierDetail.heading = Temporary occupier''s details +lettingHistory.occupierDetail.subheading = We need the contact details of each occupier who rented part of your property for 29 nights or more between {0} and {1}. Do not include any holiday lettings from this period. You will have the opportunity to add additional occupiers if required. +label.lettingHistory.occupierDetail.name.label = Occupier''s name +lettingHistory.occupierDetail.name.required = Enter the name of the temporary occupier +lettingHistory.occupierDetail.address.legend = Occupier''s address +lettingHistory.occupierDetail.address.hint = This should be the address they provided when they rented the property. + # 6010 TYPES ############################## diff --git a/test/controllers/lettingHistory/CompletedLettingsControllerSpec.scala b/test/controllers/lettingHistory/CompletedLettingsControllerSpec.scala index a4c6e6f53..cf79aa0c3 100644 --- a/test/controllers/lettingHistory/CompletedLettingsControllerSpec.scala +++ b/test/controllers/lettingHistory/CompletedLettingsControllerSpec.scala @@ -58,7 +58,7 @@ class CompletedLettingsControllerSpec extends LettingHistoryControllerSpec: ) ) status(result) shouldBe SEE_OTHER - redirectLocation(result).value shouldBe "/path/to/occupier-detail" + redirectLocation(result).value shouldBe routes.OccupierDetailController.show(index = None).url verify(repository, once).saveOrUpdate(data.capture())(any[Writes[Session]], any[HeaderCarrier]) hasCompletedLettings(data).value should beAnswerYes } @@ -81,7 +81,7 @@ class CompletedLettingsControllerSpec extends LettingHistoryControllerSpec: ) { val result = controller.submit(fakePostRequest.withFormUrlEncodedBody("hasCompletedLettings" -> "yes")) status(result) shouldBe SEE_OTHER - redirectLocation(result).value shouldBe "/path/to/occupier-detail" + redirectLocation(result).value shouldBe routes.OccupierDetailController.show(index = None).url verify(repository, once).saveOrUpdate(data.capture())(any[Writes[Session]], any[HeaderCarrier]) hasCompletedLettings(data).value should beAnswerYes } diff --git a/test/controllers/lettingHistory/LettingHistoryControllerSpec.scala b/test/controllers/lettingHistory/LettingHistoryControllerSpec.scala index 48ec151c2..f43596100 100644 --- a/test/controllers/lettingHistory/LettingHistoryControllerSpec.scala +++ b/test/controllers/lettingHistory/LettingHistoryControllerSpec.scala @@ -17,7 +17,7 @@ package controllers.lettingHistory import models.Session -import models.submissions.lettingHistory.{LettingHistory, ResidentDetail} +import models.submissions.lettingHistory.{Address, LettingHistory, OccupierDetail, ResidentDetail} import org.mockito.ArgumentCaptor import play.api.libs.json.Writes import repositories.SessionRepo @@ -42,6 +42,35 @@ class LettingHistoryControllerSpec extends TestBaseSpec: ResidentDetail(name = "Mrs. Five", address = "Address Five") ) + val oneOccupier = List( + OccupierDetail( + name = "Mr. One", + address = Address("Address One", None, "Neverland", None, "BN124AX") + ) + ) + + val twoOccupiers = oneOccupier ++ List( + OccupierDetail( + name = "Mr. Two", + address = Address("Address Two", None, "Neverland", None, "BN124AX") + ) + ) + + val fiveOccupiers = twoOccupiers ++ List( + OccupierDetail( + name = "Miss. Three", + address = Address("Address Three", None, "Neverland", None, "BN124AX") + ), + OccupierDetail( + name = "Mr. Four", + address = Address("Address Four", None, "Neverland", None, "BN124AX") + ), + OccupierDetail( + name = "Mrs. Five", + address = Address("Address Five", None, "Neverland", None, "BN124AX") + ) + ) + trait MockRepositoryFixture: val repository = mock[SessionRepo] val data = captor[Session] @@ -59,3 +88,9 @@ class LettingHistoryControllerSpec extends TestBaseSpec: def hasCompletedLettings(session: ArgumentCaptor[Session]) = LettingHistory.hasCompletedLettings(session.getValue) + + def completedLettingAt(session: ArgumentCaptor[Session], index: Integer): Option[OccupierDetail] = + LettingHistory.completedLettings(session.getValue).lift(index) + + def completedLettings(session: ArgumentCaptor[Session]): List[OccupierDetail] = + LettingHistory.completedLettings(session.getValue) diff --git a/test/controllers/lettingHistory/OccupierDetailControllerSpec.scala b/test/controllers/lettingHistory/OccupierDetailControllerSpec.scala new file mode 100644 index 000000000..e0f31e606 --- /dev/null +++ b/test/controllers/lettingHistory/OccupierDetailControllerSpec.scala @@ -0,0 +1,193 @@ +/* + * 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 controllers.lettingHistory + +import models.Session +import models.submissions.common.{AnswerNo, AnswerYes} +import models.submissions.lettingHistory.{Address, LettingHistory, OccupierDetail} +import navigation.LettingHistoryNavigator +import play.api.http.MimeTypes.HTML +import play.api.http.Status.{BAD_REQUEST, OK, SEE_OTHER} +import play.api.libs.json.Writes +import play.api.mvc.Codec.utf_8 as UTF_8 +import play.api.test.Helpers.{charset, contentAsString, contentType, redirectLocation, status, stubMessagesControllerComponents} +import uk.gov.hmrc.http.HeaderCarrier +import views.html.lettingHistory.occupierDetail as OccupierDetailView + +class OccupierDetailControllerSpec extends LettingHistoryControllerSpec: + + "the OccupierDetail controller" when { + "the user session is fresh" should { + "be handling GET /detail by replying 200 with the form showing name and address fields" in new FreshSessionFixture { + val result = controller.show(maybeIndex = None)(fakeGetRequest) + val content = contentAsString(result) + status(result) shouldBe OK + contentType(result).value shouldBe HTML + charset(result).value shouldBe UTF_8.charset + content should include("""name="name"""") + content should include("""name="address.line1"""") + content should include("""name="address.town"""") + content should include("""name="address.county"""") + content should include("""name="address.postcode"""") + } + "be handling good POST /detail by replying 303 redirect to 'Rental Period' page" in new FreshSessionFixture { + val request = fakePostRequest.withFormUrlEncodedBody( + "name" -> "Mr. Unknown", + "address.line1" -> "11, Fantasy Street", + "address.line2" -> "", + "address.town" -> "Neverland", + "address.county" -> "", + "address.postcode" -> "BN124AX" + ) + val result = controller.submit(request) + status(result) shouldBe SEE_OTHER + redirectLocation(result).value shouldBe "/path/to/rental-period" + verify(repository, once).saveOrUpdate(data.capture())(any[Writes[Session]], any[HeaderCarrier]) + completedLettingAt(data, index = 0).value shouldBe OccupierDetail( + name = "Mr. Unknown", + address = Address( + line1 = "11, Fantasy Street", + line2 = None, + town = "Neverland", + county = None, + postcode = "BN12 4AX" + ) + ) + } + } + "the user session is stale" when { + "regardless of the given number residents" should { + "be handling GET /detail?index=0 by replying 200 with the form pre-filled with name and address values" in new StaleSessionFixture( + oneOccupier + ) { + val result = controller.show(maybeIndex = Some(0))(fakeGetRequest) + val content = contentAsString(result) + status(result) shouldBe OK + contentType(result).value shouldBe HTML + charset(result).value shouldBe UTF_8.charset + content should include("Mr. One") + content should include("Address One") + } + "be handling POST /detail?unknown by replying 303 redirect to 'Rental Period' page" in new StaleSessionFixture( + oneOccupier + ) { + // Post an unknown resident detail and expect it to become the third resident + val request = fakePostRequest.withFormUrlEncodedBody( + "name" -> "Mr. Unknown", + "address.line1" -> "11, Fantasy Street", + "address.line2" -> "", + "address.town" -> "Neverland", + "address.county" -> "", + "address.postcode" -> "BN124AX" + ) + val result = controller.submit(request) + status(result) shouldBe SEE_OTHER + redirectLocation(result).value shouldBe "/path/to/rental-period" + verify(repository, once).saveOrUpdate(data.capture())(any[Writes[Session]], any[HeaderCarrier]) + completedLettings(data) should have size 2 // instead of 1 + completedLettingAt(data, index = 0).value shouldBe oneOccupier.head + completedLettingAt(data, index = 1).value.name shouldBe "Mr. Unknown" + completedLettingAt(data, index = 1).value.address shouldBe Address( + line1 = "11, Fantasy Street", + line2 = None, + town = "Neverland", + county = None, + postcode = "BN12 4AX" + ) + } + "be handling POST /detail?overwrite by replying 303 redirect to 'Rental Period' page" in new StaleSessionFixture( + twoOccupiers + ) { + // Post the second resident detail again and expect it to be changed + val request = fakePostRequest.withFormUrlEncodedBody( + "name" -> twoOccupiers.last.name, + "address.line1" -> "22, Different Street", + "address.line2" -> "", + "address.postcode" -> "BN124AX", + "address.town" -> "Neverland", + "address.county" -> "Nowhere", + "address.postcode" -> "BN124AX" + ) + val result = controller.submit(request) + status(result) shouldBe SEE_OTHER + redirectLocation(result).value shouldBe "/path/to/rental-period" + verify(repository, once).saveOrUpdate(data.capture())(any[Writes[Session]], any[HeaderCarrier]) + completedLettings(data) should have size 2 // the same as it was before sending the post request + completedLettingAt(data, index = 0).value shouldBe oneOccupier.head + completedLettingAt(data, index = 1).value.name shouldBe "Mr. Two" + completedLettingAt(data, index = 1).value.address shouldBe Address( + line1 = "22, Different Street", // instead of "Address Two" + line2 = None, + town = "Neverland", + county = Some("Nowhere"), + postcode = "BN12 4AX" + ) + } + } + } + "the user session is either fresh or stale" should { + "be handling invalid POST /detail by replying 400 with error messages" in new FreshSessionFixture { + val result = controller.submit( + fakePostRequest.withFormUrlEncodedBody( + "name" -> "", + "address.line1" -> "", + "address.line2" -> "", + "address.town" -> "", + "address.county" -> "", + "address.postcode" -> "" + ) + ) + val content = contentAsString(result) + status(result) shouldBe BAD_REQUEST + content should include("lettingHistory.occupierDetail.name.required") + content should include("error.buildingNameNumber.required") + content should include("error.townCity.required") + content should include("error.postcodeAlternativeContact.required") + } + } + } + + // It provides the scenario of fresh session (there's no letting history yet in session) + trait FreshSessionFixture extends MockRepositoryFixture with SessionCapturingFixture: + val controller = new OccupierDetailController( + mcc = stubMessagesControllerComponents(), + navigator = inject[LettingHistoryNavigator], + theView = inject[OccupierDetailView], + sessionRefiner = preEnrichedActionRefiner( + lettingHistory = None + ), + repository + ) + + // It represents the scenario of ongoing session (with some letting history already created) + trait StaleSessionFixture(completedLettings: List[OccupierDetail]) + extends MockRepositoryFixture + with SessionCapturingFixture: + val controller = new OccupierDetailController( + mcc = stubMessagesControllerComponents(), + navigator = inject[LettingHistoryNavigator], + theView = inject[OccupierDetailView], + sessionRefiner = preEnrichedActionRefiner( + lettingHistory = Some( + LettingHistory( + hasCompletedLettings = Some(if completedLettings.isEmpty then AnswerNo else AnswerYes), + completedLettings = completedLettings + ) + ) + ), + repository + ) diff --git a/test/controllers/lettingHistory/ResidentDetailControllerSpec.scala b/test/controllers/lettingHistory/ResidentDetailControllerSpec.scala index d4efc0ab2..caf88b45d 100644 --- a/test/controllers/lettingHistory/ResidentDetailControllerSpec.scala +++ b/test/controllers/lettingHistory/ResidentDetailControllerSpec.scala @@ -33,7 +33,7 @@ class ResidentDetailControllerSpec extends LettingHistoryControllerSpec: "the ResidentDetail controller" when { "the user session is fresh" should { "be handling GET /detail by replying 200 with the form showing name and address fields" in new FreshSessionFixture { - val result = controller.show()(fakeGetRequest) + val result = controller.show(maybeIndex = None)(fakeGetRequest) val content = contentAsString(result) status(result) shouldBe OK contentType(result).value shouldBe HTML @@ -41,7 +41,7 @@ class ResidentDetailControllerSpec extends LettingHistoryControllerSpec: content should include("""name="name"""") content should include("""name="address"""") } - "be handling good POST /detail by replying 303 redirect to to 'Residents List' page" in new FreshSessionFixture { + "be handling good POST /detail by replying 303 redirect to the 'Residents List' page" in new FreshSessionFixture { val request = fakePostRequest.withFormUrlEncodedBody( "name" -> "Mr. Unknown", "address" -> "Neverland" @@ -61,7 +61,7 @@ class ResidentDetailControllerSpec extends LettingHistoryControllerSpec: "be handling GET /detail?index=0 by replying 200 with the form pre-filled with name and address values" in new StaleSessionFixture( oneResident ) { - val result = controller.show(index = Some(0))(fakeGetRequest) + val result = controller.show(maybeIndex = Some(0))(fakeGetRequest) val content = contentAsString(result) status(result) shouldBe OK contentType(result).value shouldBe HTML diff --git a/test/controllers/lettingHistory/ResidentListControllerSpec.scala b/test/controllers/lettingHistory/ResidentListControllerSpec.scala index 52624016f..0283d1dda 100644 --- a/test/controllers/lettingHistory/ResidentListControllerSpec.scala +++ b/test/controllers/lettingHistory/ResidentListControllerSpec.scala @@ -60,7 +60,7 @@ class ResidentListControllerSpec extends LettingHistoryControllerSpec: } "the user session is stale" when { "regardless of the given number residents" should { - "be handling GET /list and reply 200 with the form showing the list of known residents" in new StaleSessionFixture( + "be handling GET /list and reply 200 by showing the list of known residents" in new StaleSessionFixture( oneResident ) { val result = controller.show(fakeGetRequest) @@ -68,8 +68,7 @@ class ResidentListControllerSpec extends LettingHistoryControllerSpec: status(result) shouldBe OK contentType(result).value shouldBe HTML charset(result).value shouldBe UTF_8.charset - content should include("Mr. One") - content should include("Address One") + content should include(oneResident.head.name) } "be handling GET /remove?index=0 by replying 200 with the 'Confirm remove' page" in new StaleSessionFixture( oneResident @@ -119,7 +118,7 @@ class ResidentListControllerSpec extends LettingHistoryControllerSpec: ) { val result = controller.submit( fakePostRequest.withFormUrlEncodedBody( - "hasMoreResidents" -> "" + "hasMoreResidents" -> "" // yes or no is missing! ) ) status(result) shouldBe BAD_REQUEST @@ -138,7 +137,7 @@ class ResidentListControllerSpec extends LettingHistoryControllerSpec: "be handling invalid POST /list by replying 400 with error messages" in new FreshSessionFixture { val result = controller.submit( fakePostRequest.withFormUrlEncodedBody( - "hasMoreResidents" -> "" + "hasMoreResidents" -> "" // yes or no is missing ) ) status(result) shouldBe BAD_REQUEST diff --git a/test/form/lettingHistory/OccupierDetailFormSpec.scala b/test/form/lettingHistory/OccupierDetailFormSpec.scala new file mode 100644 index 000000000..ee4b4ef75 --- /dev/null +++ b/test/form/lettingHistory/OccupierDetailFormSpec.scala @@ -0,0 +1,78 @@ +/* + * 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 form.lettingHistory + +import form.lettingHistory.OccupierDetailForm.theForm +import models.submissions.lettingHistory.{Address, OccupierDetail} + +class OccupierDetailFormSpec extends FormSpec: + + it should "bind good data as expected" in { + val data = Map( + "name" -> "name", + "address.line1" -> "89, Fantasy Street", + // "address.line2" -> "Basement", + "address.town" -> "Birds Island", + // "address.county" -> "Neverland", + "address.postcode" -> "BN124AX" + ) + val bound = theForm.bind(data) + bound.hasErrors mustBe false + bound.data mustBe data + } + + it should "unbind good data as expected" in { + val occupierDetail = OccupierDetail( + name = "name", + address = Address( + line1 = "89, Fantasy Street", + line2 = None, + town = "Birds Island", + county = Some("Neverland"), + postcode = "BN124AX" + ) + ) + val filled = theForm.fill(occupierDetail) + filled.hasErrors mustBe false + filled.data mustBe Map( + "name" -> "name", + "address.line1" -> "89, Fantasy Street", + "address.town" -> "Birds Island", + "address.county" -> "Neverland", + "address.postcode" -> "BN124AX" + ) + } + + it should "detect errors" in { + // When the form gets submitted even though "untouched" + val bound = theForm.bind( + Map( + "name" -> "", + "address.line1" -> "", + "address.line2" -> "", + "address.town" -> "", + "address.county" -> "", + "address.postcode" -> "" + ) + ) + bound.hasErrors mustBe true + bound.errors must have size 4 + bound.error("name").value.message mustBe "lettingHistory.occupierDetail.name.required" + bound.error("address.line1").value.message mustBe "error.buildingNameNumber.required" + bound.error("address.town").value.message mustBe "error.townCity.required" + bound.error("address.postcode").value.message mustBe "error.postcodeAlternativeContact.required" + } diff --git a/test/form/lettingHistory/ResidentListFormSpec.scala b/test/form/lettingHistory/ResidentListFormSpec.scala index e77e40ec7..96854afa7 100644 --- a/test/form/lettingHistory/ResidentListFormSpec.scala +++ b/test/form/lettingHistory/ResidentListFormSpec.scala @@ -16,8 +16,8 @@ package form.lettingHistory -import models.submissions.common.AnswerYes import form.lettingHistory.ResidentListForm.theForm +import models.submissions.common.AnswerYes class ResidentListFormSpec extends FormSpec: diff --git a/test/models/submissions/lettingHistory/LettingHistorySpec.scala b/test/models/submissions/lettingHistory/LettingHistorySpec.scala deleted file mode 100644 index e5f13ffd7..000000000 --- a/test/models/submissions/lettingHistory/LettingHistorySpec.scala +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 models.submissions.lettingHistory - -import models.submissions.common.{Address, AnswerNo, AnswerYes} -import models.{ForType, Session} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatest.OptionValues -import utils.CustomMatchers - -class LettingHistorySpec extends AnyWordSpec with CustomMatchers with Matchers with OptionValues: - - "the LettingHistory model" when { - "updating a fresh session" should { - "set the isPermanent boolean flag" in new FreshSessionFixture { - val updatedSession = LettingHistory.withPermanentResidents(hasPermanentResidents = AnswerNo) - LettingHistory.hasPermanentResidents(updatedSession).value must beAnswerNo - } - "add the first resident detail" in new FreshSessionFixture { - val updateSession = LettingHistory.byAddingPermanentResident( - ResidentDetail( - name = "Mr. Peter Pan", - address = "20, Fantasy Street, Birds' Island, BIR067" - ) - ) - val permanentResidents = LettingHistory.permanentResidents(updateSession) - permanentResidents must have size 1 - permanentResidents(0).name mustBe "Mr. Peter Pan" - permanentResidents(0).address mustBe "20, Fantasy Street, Birds' Island, BIR067" - } - } - "updating a stale session" should { - "set the isPermanent boolean flag" in new StaleSessionFixture( - LettingHistory(hasPermanentResidents = Some(AnswerYes)) - ) { - val updatedSession = LettingHistory.withPermanentResidents(AnswerNo) - LettingHistory.hasPermanentResidents(updatedSession).value must beAnswerNo - } - "add a new unknown resident detail" in new StaleSessionFixture(havingKnownResident) { - val misterNewGuest = ResidentDetail( - name = "Mr. New Guest", - address = "Sleeping on the sofa" - ) - val updateSession = LettingHistory.byAddingPermanentResident(misterNewGuest) - val permanentResidents = LettingHistory.permanentResidents(updateSession) - permanentResidents must have size 2 - permanentResidents(0) mustBe misterKnownTenant - permanentResidents(1) mustBe misterNewGuest - } - "add an already known resident by just overwriting the same" in new StaleSessionFixture(havingKnownResident) { - // Update the session by adding the very same resident name - val misterKnownAtDifferentAddress = misterKnownTenant.copy(address = "different address") - val updateSession = LettingHistory.byAddingPermanentResident(misterKnownAtDifferentAddress) - // Assert that nothing changed (the size of the detail list is still 1) - val permanentResidents = LettingHistory.permanentResidents(updateSession) - permanentResidents must have size 1 - permanentResidents(0).name mustBe misterKnownTenant.name - permanentResidents(0).address mustBe misterKnownAtDifferentAddress.address - } - } - } - - trait FreshSessionFixture { - given Session = Session( - referenceNumber = "referenceNumber", - forType = ForType.FOR6048, - address = Address( - buildingNameNumber = "buildingNameNumber", - street1 = Some("street1"), - street2 = "street2", - county = Some("county"), - postcode = "postcode" - ), - token = "token", - isWelsh = false, - lettingHistory = None - ) - } - - trait StaleSessionFixture(stale: LettingHistory) { - given Session = Session( - referenceNumber = "referenceNumber", - forType = ForType.FOR6048, - address = Address( - buildingNameNumber = "buildingNameNumber", - street1 = Some("street1"), - street2 = "street2", - county = Some("county"), - postcode = "postcode" - ), - token = "token", - isWelsh = false, - lettingHistory = Some(stale) - ) - } - - val misterKnownTenant = ResidentDetail( - name = "Mr. Known Tenant", - address = "21, Already Added Street" - ) - - val havingKnownResident = LettingHistory( - hasPermanentResidents = Some(AnswerYes), - permanentResidents = List(misterKnownTenant) - ) diff --git a/test/models/submissions/lettingHistory/SensitiveLettingHistorySpec.scala b/test/models/submissions/lettingHistory/SensitiveLettingHistorySpec.scala index e88d20faa..fee13855c 100644 --- a/test/models/submissions/lettingHistory/SensitiveLettingHistorySpec.scala +++ b/test/models/submissions/lettingHistory/SensitiveLettingHistorySpec.scala @@ -44,6 +44,13 @@ class SensitiveLettingHistorySpec extends AnyFlatSpec with Matchers with OptionV encryptedResidentDetail.address must not be clearLettingHistory.permanentResidents.head.address (jsonValue \ "hasCompletedLettings").as[String] mustBe "yes" + val encryptedOccupierDetails = (jsonValue \ "completedLettings").head.as[SensitiveOccupierDetail] + encryptedOccupierDetails.name must not be clearLettingHistory.completedLettings.head.name + encryptedOccupierDetails.address.line1 must not be clearLettingHistory.completedLettings.head.address.line1 + encryptedOccupierDetails.address.line2.value must not be clearLettingHistory.completedLettings.head.address.line2.value + encryptedOccupierDetails.address.town must not be clearLettingHistory.completedLettings.head.address.town + encryptedOccupierDetails.address.county.value must not be clearLettingHistory.completedLettings.head.address.county.value + encryptedOccupierDetails.address.postcode must not be clearLettingHistory.completedLettings.head.address.postcode } it should "deserialize from encrypted JSON" in { @@ -60,5 +67,17 @@ class SensitiveLettingHistorySpec extends AnyFlatSpec with Matchers with OptionV address = "20, Fantasy Street, Birds' Island, BIR067" ) ), - hasCompletedLettings = Some(AnswerYes) + hasCompletedLettings = Some(AnswerYes), + completedLettings = List( + OccupierDetail( + name = "Miss Nobody", + address = Address( + line1 = "21, Somewhere Place", + line2 = Some("Basement"), + town = "NeverTown", + county = Some("Birds' Island"), + postcode = "BN124AX" + ) + ) + ) )