From 1963c49091987eec62de769fb07db9696a758aaf Mon Sep 17 00:00:00 2001 From: stephen-oppong-beduh <56881842+stephen-oppong-beduh@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:37:26 +0000 Subject: [PATCH] SASS-9705 - Added Foreign expenses HYF page --- ...ignExpensesSectionCompleteController.scala | 86 ++++++++ ...nExpensesSectionCompleteFormProvider.scala | 30 +++ app/models/JourneyPath.scala | 2 + app/navigation/ForeignPropertyNavigator.scala | 6 + .../foreign/ForeignPropertySummaryPage.scala | 16 +- .../ForeignExpensesSectionCompletePage.scala | 29 +++ ...eignExpensesSectionCompleteView.scala.html | 49 +++++ conf/foreign.routes | 5 +- ...xpensesSectionCompleteControllerSpec.scala | 197 ++++++++++++++++++ ...ensesSectionCompleteFormProviderSpec.scala | 45 ++++ 10 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 app/controllers/foreign/expenses/ForeignExpensesSectionCompleteController.scala create mode 100644 app/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProvider.scala create mode 100644 app/pages/foreign/expenses/ForeignExpensesSectionCompletePage.scala create mode 100644 app/views/foreign/expenses/ForeignExpensesSectionCompleteView.scala.html create mode 100644 test/controllers/foreign/expenses/ForeignExpensesSectionCompleteControllerSpec.scala create mode 100644 test/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProviderSpec.scala diff --git a/app/controllers/foreign/expenses/ForeignExpensesSectionCompleteController.scala b/app/controllers/foreign/expenses/ForeignExpensesSectionCompleteController.scala new file mode 100644 index 00000000..2f72956d --- /dev/null +++ b/app/controllers/foreign/expenses/ForeignExpensesSectionCompleteController.scala @@ -0,0 +1,86 @@ +/* + * 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.foreign.expenses + +import controllers.ControllerUtils.statusForPage +import controllers.actions._ +import forms.foreign.expenses.ForeignExpensesSectionCompleteFormProvider +import models.JourneyPath.ForeignPropertyExpenses +import models.{JourneyContext, NormalMode} +import navigation.ForeignPropertyNavigator +import pages.foreign.expenses.ForeignExpensesSectionCompletePage +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import service.JourneyAnswersService +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.foreign.expenses.ForeignExpensesSectionCompleteView + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class ForeignExpensesSectionCompleteController @Inject()( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: ForeignPropertyNavigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: ForeignExpensesSectionCompleteFormProvider, + val controllerComponents: MessagesControllerComponents, + view: ForeignExpensesSectionCompleteView, + journeyAnswersService: JourneyAnswersService + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + val form = formProvider() + + def onPageLoad(taxYear: Int, countryCode: String): Action[AnyContent] = (identify andThen getData andThen requireData) { + implicit request => + + val preparedForm = request.userAnswers.get(ForeignExpensesSectionCompletePage(countryCode)) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, taxYear, countryCode)) + } + + def onSubmit(taxYear: Int, countryCode: String): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, taxYear, countryCode))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(ForeignExpensesSectionCompletePage(countryCode), value)) + _ <- sessionRepository.set(updatedAnswers) + status <- journeyAnswersService.setStatus( + JourneyContext( + taxYear = taxYear, + mtditid = request.user.mtditid, + nino = request.user.nino, + journeyPath = ForeignPropertyExpenses + ), + status = statusForPage(value), + request.user + ) + } yield Redirect(navigator.nextPage(ForeignExpensesSectionCompletePage(countryCode), taxYear, NormalMode, request.userAnswers, updatedAnswers)) + ) + } +} diff --git a/app/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProvider.scala b/app/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProvider.scala new file mode 100644 index 00000000..32d31f2e --- /dev/null +++ b/app/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProvider.scala @@ -0,0 +1,30 @@ +/* + * 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 forms.foreign.expenses + +import forms.mappings.Mappings +import play.api.data.Form + +import javax.inject.Inject + +class ForeignExpensesSectionCompleteFormProvider @Inject() extends Mappings { + + def apply(): Form[Boolean] = + Form( + "foreignExpensesSectionComplete" -> boolean("haveYouFinishedThisSection.error.required") + ) +} diff --git a/app/models/JourneyPath.scala b/app/models/JourneyPath.scala index b2cf13f7..70294454 100644 --- a/app/models/JourneyPath.scala +++ b/app/models/JourneyPath.scala @@ -92,4 +92,6 @@ object JourneyPath { case object ForeignPropertyTax extends WithName("foreign-property-tax") with JourneyPath + case object ForeignPropertyExpenses extends WithName("foreign-property-expenses") with JourneyPath + } diff --git a/app/navigation/ForeignPropertyNavigator.scala b/app/navigation/ForeignPropertyNavigator.scala index eacd8301..b4db18d0 100644 --- a/app/navigation/ForeignPropertyNavigator.scala +++ b/app/navigation/ForeignPropertyNavigator.scala @@ -25,6 +25,7 @@ import models.ForeignTotalIncome.{LessThanOneThousand, OneThousandAndMore} import models._ import pages.Page import pages.foreign._ +import pages.foreign.expenses.ForeignExpensesSectionCompletePage import pages.foreign.income._ import play.api.mvc.Call @@ -71,6 +72,11 @@ class ForeignPropertyNavigator { ForeignReceivedGrantLeaseAmountController .onPageLoad(taxYear, countryCode, NormalMode) } + case ForeignExpensesSectionCompletePage(countryCode) => + taxYear => + _ => + _ => + SummaryController.show(taxYear) case _ => _ => _ => _ => controllers.routes.IndexController.onPageLoad } diff --git a/app/pages/foreign/ForeignPropertySummaryPage.scala b/app/pages/foreign/ForeignPropertySummaryPage.scala index b283b2f0..a76bb0b2 100644 --- a/app/pages/foreign/ForeignPropertySummaryPage.scala +++ b/app/pages/foreign/ForeignPropertySummaryPage.scala @@ -17,11 +17,8 @@ package pages.foreign import models.{NormalMode, UserAnswers} +import pages.foreign.expenses.ForeignExpensesSectionCompletePage import pages.foreign.income.ForeignIncomeSectionCompletePage -import controllers.propertyrentals.expenses.routes.ExpensesCheckYourAnswersController -import models.{NormalMode, Rentals, UserAnswers} -import pages.enhancedstructuresbuildingallowance.EsbaSectionFinishedPage -import play.api.mvc.Call import viewmodels.summary.{TaskListItem, TaskListTag} case class ForeignPropertySummaryPage( @@ -73,6 +70,15 @@ object ForeignPropertySummaryPage { } .getOrElse(TaskListTag.NotStarted) + val taskListTagForExpenses = + userAnswers + .flatMap { answers => + answers.get(ForeignExpensesSectionCompletePage(countryCode)).map { finishedYesOrNo => + if (finishedYesOrNo) TaskListTag.Completed else TaskListTag.InProgress + } + } + .getOrElse(TaskListTag.NotStarted) + Seq( TaskListItem( "foreign.tax", @@ -89,7 +95,7 @@ object ForeignPropertySummaryPage { TaskListItem( "foreign.expenses", controllers.foreign.expenses.routes.ForeignPropertyExpensesStartController.onPageLoad(taxYear, countryCode), - TaskListTag.NotStarted, + taskListTagForExpenses, s"foreign_property_expenses_$countryCode" ) ) diff --git a/app/pages/foreign/expenses/ForeignExpensesSectionCompletePage.scala b/app/pages/foreign/expenses/ForeignExpensesSectionCompletePage.scala new file mode 100644 index 00000000..964454de --- /dev/null +++ b/app/pages/foreign/expenses/ForeignExpensesSectionCompletePage.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 pages.foreign.expenses + +import models.ForeignProperty +import pages.PageConstants.expensesPath +import pages.QuestionPage +import play.api.libs.json.JsPath + +case class ForeignExpensesSectionCompletePage(countryCode: String) extends QuestionPage[Boolean] { + + override def path: JsPath = JsPath \ expensesPath(ForeignProperty) \ countryCode.toUpperCase \ toString + + override def toString: String = "foreignExpensesSectionComplete" +} diff --git a/app/views/foreign/expenses/ForeignExpensesSectionCompleteView.scala.html b/app/views/foreign/expenses/ForeignExpensesSectionCompleteView.scala.html new file mode 100644 index 00000000..06a11ce1 --- /dev/null +++ b/app/views/foreign/expenses/ForeignExpensesSectionCompleteView.scala.html @@ -0,0 +1,49 @@ +@* + * 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 controllers.foreign.expenses.routes +@import viewmodels.LegendSize + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukRadios: GovukRadios, + govukButton: GovukButton +) + +@(form: Form[_], taxYear: Int, countryCode: String)(implicit request: Request[_], messages: Messages) + +@layout(pageTitle = title(form, messages("haveYouFinishedThisSection.title"))) { + + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + + @formHelper(action = routes.ForeignExpensesSectionCompleteController.onSubmit(taxYear, countryCode), Symbol("autoComplete") -> "off") { + + @govukRadios( + RadiosViewModel.yesNo( + field = form("foreignExpensesSectionComplete"), + legend = LegendViewModel(messages("haveYouFinishedThisSection.heading")).asPageHeading(LegendSize.Large) + ).withHint(HintViewModel(messages("haveYouFinishedThisSection.hint"))) + ) + + @govukButton( + ButtonViewModel(messages("site.continue")).withId("continue") + ) + } +} diff --git a/conf/foreign.routes b/conf/foreign.routes index 1b047274..8b4a2893 100644 --- a/conf/foreign.routes +++ b/conf/foreign.routes @@ -129,4 +129,7 @@ POST /:taxYear/foreign-property/expenses/:countryCode/change-foreign-non-r GET /:taxYear/foreign-property/expenses/:countryCode/foreign-professional-fees controllers.foreign.expenses.ForeignProfessionalFeesController.onPageLoad(taxYear: Int, countryCode: String, mode: Mode = NormalMode) POST /:taxYear/foreign-property/expenses/:countryCode/foreign-professional-fees controllers.foreign.expenses.ForeignProfessionalFeesController.onSubmit(taxYear: Int, countryCode: String, mode: Mode = NormalMode) GET /:taxYear/foreign-property/expenses/:countryCode/change-foreign-professional-fees controllers.foreign.expenses.ForeignProfessionalFeesController.onPageLoad(taxYear: Int, countryCode: String, mode: Mode = CheckMode) -POST /:taxYear/foreign-property/expenses/:countryCode/change-foreign-professional-fees controllers.foreign.expenses.ForeignProfessionalFeesController.onSubmit(taxYear: Int, countryCode: String, mode: Mode = CheckMode) \ No newline at end of file +POST /:taxYear/foreign-property/expenses/:countryCode/change-foreign-professional-fees controllers.foreign.expenses.ForeignProfessionalFeesController.onSubmit(taxYear: Int, countryCode: String, mode: Mode = CheckMode) + +GET /:taxYear/foreign-property/expenses/:countryCode/complete-yes-no controllers.foreign.expenses.ForeignExpensesSectionCompleteController.onPageLoad(taxYear: Int, countryCode: String) +POST /:taxYear/foreign-property/expenses/:countryCode/complete-yes-no controllers.foreign.expenses.ForeignExpensesSectionCompleteController.onSubmit(taxYear: Int, countryCode: String) diff --git a/test/controllers/foreign/expenses/ForeignExpensesSectionCompleteControllerSpec.scala b/test/controllers/foreign/expenses/ForeignExpensesSectionCompleteControllerSpec.scala new file mode 100644 index 00000000..c61f9b18 --- /dev/null +++ b/test/controllers/foreign/expenses/ForeignExpensesSectionCompleteControllerSpec.scala @@ -0,0 +1,197 @@ +/* + * 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.foreign.expenses + +import base.SpecBase +import forms.foreign.expenses.ForeignExpensesSectionCompleteFormProvider +import models.JourneyPath.ForeignPropertyExpenses +import models.{JourneyContext, User, UserAnswers} +import navigation.{FakeForeignPropertyNavigator, ForeignPropertyNavigator} +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{doReturn, when} +import org.scalatest.prop.TableFor1 +import org.scalatest.prop.Tables.Table +import org.scalatestplus.mockito.MockitoSugar +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks.forAll +import pages.foreign.expenses.ForeignExpensesSectionCompletePage +import play.api.data.Form +import play.api.inject.bind +import play.api.mvc.Call +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import repositories.SessionRepository +import service.JourneyAnswersService +import uk.gov.hmrc.http.HeaderCarrier +import views.html.foreign.expenses.ForeignExpensesSectionCompleteView + +import java.time.LocalDate +import scala.concurrent.Future + +class ForeignExpensesSectionCompleteControllerSpec extends SpecBase with MockitoSugar { + + def onwardRoute: Call = Call("GET", "/foo") + + val scenarios: TableFor1[String] = Table[String]("individualOrAgent", "individual", "agent") + + val formProvider = new ForeignExpensesSectionCompleteFormProvider() + val form: Form[Boolean] = formProvider() + val taxYear: Int = LocalDate.now().getYear + val countryCode: String = "USA" + implicit val hc: HeaderCarrier = new HeaderCarrier() + + lazy val foreignExpensesSectionCompleteRoute: String = routes.ForeignExpensesSectionCompleteController.onPageLoad(taxYear, countryCode).url + + forAll(scenarios) { (individualOrAgent: String) => + val isAgent: Boolean = individualOrAgent == "agent" + val user: User = User( + mtditid = "mtditid", + nino = "nino", + isAgent = isAgent, + affinityGroup = "affinityGroup", + agentRef = Some("agentReferenceNumber") + ) + + s"ForeignExpensesSectionComplete Controller for an $individualOrAgent" - { + + "must return OK and the correct view for a GET" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers), isAgent = isAgent).build() + + running(application) { + val request = FakeRequest(GET, foreignExpensesSectionCompleteRoute) + + val result = route(application, request).value + + val view = application.injector.instanceOf[ForeignExpensesSectionCompleteView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form, taxYear, countryCode)(request, messages(application)).toString + } + } + + "must populate the view correctly on a GET when the question has previously been answered" in { + + val userAnswers = UserAnswers(userAnswersId).set(ForeignExpensesSectionCompletePage(countryCode), true).success.value + + val application = applicationBuilder(userAnswers = Some(userAnswers), isAgent = isAgent).build() + + running(application) { + val request = FakeRequest(GET, foreignExpensesSectionCompleteRoute) + + val view = application.injector.instanceOf[ForeignExpensesSectionCompleteView] + + val result = route(application, request).value + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form.fill(true), taxYear, countryCode)(request, messages(application)).toString + } + } + + "must redirect to the next page when valid data is submitted" in { + + val mockSessionRepository = mock[SessionRepository] + val mockJourneyAnswersService = mock[JourneyAnswersService] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + doReturn(Future.successful(Right("completed"))) + .when(mockJourneyAnswersService).setStatus( + ArgumentMatchers.eq( + JourneyContext( + taxYear = taxYear, + mtditid = user.mtditid, + nino = user.nino, + journeyPath = ForeignPropertyExpenses + ) + ), + ArgumentMatchers.eq("completed"), + ArgumentMatchers.eq(user) + )(any()) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers), isAgent = isAgent) + .overrides( + bind[ForeignPropertyNavigator].toInstance(new FakeForeignPropertyNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository), + bind[JourneyAnswersService].toInstance(mockJourneyAnswersService) + ) + .build() + + running(application) { + val request = + FakeRequest(POST, foreignExpensesSectionCompleteRoute) + .withFormUrlEncodedBody(("foreignExpensesSectionComplete", "true")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url + } + } + + "must return a Bad Request and errors when invalid data is submitted" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers), isAgent = isAgent).build() + + running(application) { + val request = + FakeRequest(POST, foreignExpensesSectionCompleteRoute) + .withFormUrlEncodedBody(("foreignExpensesSectionComplete", "")) + + val boundForm = form.bind(Map("foreignExpensesSectionComplete" -> "")) + + val view = application.injector.instanceOf[ForeignExpensesSectionCompleteView] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + contentAsString(result) mustEqual view(boundForm, taxYear, countryCode)(request, messages(application)).toString + } + } + + "must redirect to Journey Recovery for a GET if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None, isAgent = isAgent).build() + + running(application) { + val request = FakeRequest(GET, foreignExpensesSectionCompleteRoute) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url + } + } + + "must redirect to Journey Recovery for a POST if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None, isAgent = isAgent).build() + + running(application) { + val request = + FakeRequest(POST, foreignExpensesSectionCompleteRoute) + .withFormUrlEncodedBody(("foreignExpensesSectionComplete", "true")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url + } + } + } + } +} diff --git a/test/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProviderSpec.scala b/test/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProviderSpec.scala new file mode 100644 index 00000000..e83fd631 --- /dev/null +++ b/test/forms/foreign/expenses/ForeignExpensesSectionCompleteFormProviderSpec.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 forms.foreign.expenses + +import forms.behaviours.BooleanFieldBehaviours +import play.api.data.FormError + +class ForeignExpensesSectionCompleteFormProviderSpec extends BooleanFieldBehaviours { + + val requiredKey = "haveYouFinishedThisSection.error.required" + val invalidKey = "error.boolean" + + val form = new ForeignExpensesSectionCompleteFormProvider()() + + ".foreignExpensesSectionComplete" - { + + val fieldName = "foreignExpensesSectionComplete" + + behave like booleanField( + form, + fieldName, + invalidError = FormError(fieldName, invalidKey) + ) + + behave like mandatoryField( + form, + fieldName, + requiredError = FormError(fieldName, requiredKey) + ) + } +}