From 5616155b946c66a342c1b763116ca8f009fd6b67 Mon Sep 17 00:00:00 2001 From: RGlossop <75027918+RGlossop@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:25:01 +0000 Subject: [PATCH] SASS-9703: Expenses section: Create 'Other allowable property expenses' page --- ...rAllowablePropertyExpensesController.scala | 72 ++++++++ ...llowablePropertyExpensesFormProvider.scala | 36 ++++ ...gnOtherAllowablePropertyExpensesPage.scala | 29 +++ ...sidentialPropertyFinanceCostsSummary.scala | 6 +- ...therAllowablePropertyExpensesSummary.scala | 42 +++++ ...PropertyRepairsAndMaintenanceSummary.scala | 6 +- ...erAllowablePropertyExpensesView.scala.html | 61 +++++++ conf/foreign.routes | 6 +- conf/messages.cy | 27 ++- conf/messages.en | 27 ++- ...owablePropertyExpensesControllerSpec.scala | 166 ++++++++++++++++++ ...ablePropertyExpensesFormProviderSpec.scala | 62 +++++++ 12 files changed, 531 insertions(+), 9 deletions(-) create mode 100644 app/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesController.scala create mode 100644 app/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProvider.scala create mode 100644 app/pages/foreign/expenses/ForeignOtherAllowablePropertyExpensesPage.scala create mode 100644 app/viewmodels/checkAnswers/foreign/expenses/ForeignOtherAllowablePropertyExpensesSummary.scala create mode 100644 app/views/foreign/expenses/ForeignOtherAllowablePropertyExpensesView.scala.html create mode 100644 test/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesControllerSpec.scala create mode 100644 test/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProviderSpec.scala diff --git a/app/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesController.scala b/app/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesController.scala new file mode 100644 index 00000000..2640580d --- /dev/null +++ b/app/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesController.scala @@ -0,0 +1,72 @@ +/* + * 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.actions._ +import forms.foreign.expenses.ForeignOtherAllowablePropertyExpensesFormProvider +import models.Mode +import navigation.ForeignPropertyNavigator +import pages.foreign.expenses.ForeignOtherAllowablePropertyExpensesPage +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.foreign.expenses.ForeignOtherAllowablePropertyExpensesView + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class ForeignOtherAllowablePropertyExpensesController @Inject()( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + foreignNavigator: ForeignPropertyNavigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: ForeignOtherAllowablePropertyExpensesFormProvider, + val controllerComponents: MessagesControllerComponents, + view: ForeignOtherAllowablePropertyExpensesView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + + + def onPageLoad(taxYear: Int, countryCode: String,mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { + implicit request => + val form = formProvider(request.user.isAgentMessageKey) + val preparedForm = request.userAnswers.get(ForeignOtherAllowablePropertyExpensesPage(countryCode)) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, taxYear, countryCode, request.user.isAgentMessageKey, mode)) + } + + def onSubmit(taxYear: Int, countryCode: String, mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + val form = formProvider(request.user.isAgentMessageKey) + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, taxYear, countryCode, request.user.isAgentMessageKey, mode))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(ForeignOtherAllowablePropertyExpensesPage(countryCode), value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(foreignNavigator.nextPage(ForeignOtherAllowablePropertyExpensesPage(countryCode), taxYear, mode, request.userAnswers, updatedAnswers)) + ) + } +} diff --git a/app/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProvider.scala b/app/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProvider.scala new file mode 100644 index 00000000..78aa2965 --- /dev/null +++ b/app/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProvider.scala @@ -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. + */ + +package forms.foreign.expenses + +import forms.mappings.Mappings +import play.api.data.Form + +import javax.inject.Inject + +class ForeignOtherAllowablePropertyExpensesFormProvider @Inject() extends Mappings { + + val minimum = 0 + val maximum = 100000000 + def apply(individualOrAgent: String): Form[BigDecimal] = + Form( + "otherAllowablePropertyExpensesAmount" -> currency( + s"foreignOtherAllowablePropertyExpenses.error.required.$individualOrAgent", + s"foreignOtherAllowablePropertyExpenses.error.nonNumeric.$individualOrAgent", + s"foreignOtherAllowablePropertyExpenses.error.nonNumeric.$individualOrAgent") + .verifying(inRange(BigDecimal(minimum), BigDecimal(maximum), "foreignOtherAllowablePropertyExpenses.error.outOfRange")) + ) +} diff --git a/app/pages/foreign/expenses/ForeignOtherAllowablePropertyExpensesPage.scala b/app/pages/foreign/expenses/ForeignOtherAllowablePropertyExpensesPage.scala new file mode 100644 index 00000000..a16ce9ea --- /dev/null +++ b/app/pages/foreign/expenses/ForeignOtherAllowablePropertyExpensesPage.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.foreignPropertyExpensesPath +import pages.QuestionPage +import play.api.libs.json.JsPath + +case class ForeignOtherAllowablePropertyExpensesPage(countryCode: String) extends QuestionPage[BigDecimal] { + + override def path: JsPath = JsPath \ foreignPropertyExpensesPath(ForeignProperty) \ countryCode.toUpperCase \ toString + + override def toString: String = "foreignOtherAllowablePropertyExpenses" +} diff --git a/app/viewmodels/checkAnswers/foreign/expenses/ForeignNonResidentialPropertyFinanceCostsSummary.scala b/app/viewmodels/checkAnswers/foreign/expenses/ForeignNonResidentialPropertyFinanceCostsSummary.scala index 1039dab3..9c19a0c4 100644 --- a/app/viewmodels/checkAnswers/foreign/expenses/ForeignNonResidentialPropertyFinanceCostsSummary.scala +++ b/app/viewmodels/checkAnswers/foreign/expenses/ForeignNonResidentialPropertyFinanceCostsSummary.scala @@ -16,11 +16,11 @@ package viewmodels.checkAnswers.foreign.expenses -import controllers.routes import models.{CheckMode, UserAnswers} import pages.foreign.expenses.ForeignNonResidentialPropertyFinanceCostsPage import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.checkAnswers.FormatUtils.{bigDecimalCurrency, keyCssClass, valueCssClass} import viewmodels.govuk.summarylist._ import viewmodels.implicits._ @@ -31,8 +31,8 @@ object ForeignNonResidentialPropertyFinanceCostsSummary { answer => SummaryListRowViewModel( - key = "foreignNonResidentialPropertyFinanceCosts.checkYourAnswersLabel", - value = ValueViewModel(answer.toString), + key = KeyViewModel("foreignNonResidentialPropertyFinanceCosts.checkYourAnswersLabel").withCssClass(keyCssClass), + value = ValueViewModel(bigDecimalCurrency(answer)).withCssClass(valueCssClass), actions = Seq( ActionItemViewModel("site.change", controllers.foreign.expenses.routes.ForeignNonResidentialPropertyFinanceCostsController.onPageLoad(taxYear, countryCode , CheckMode).url) .withVisuallyHiddenText(messages("foreignNonResidentialPropertyFinanceCosts.change.hidden")) diff --git a/app/viewmodels/checkAnswers/foreign/expenses/ForeignOtherAllowablePropertyExpensesSummary.scala b/app/viewmodels/checkAnswers/foreign/expenses/ForeignOtherAllowablePropertyExpensesSummary.scala new file mode 100644 index 00000000..27515cb7 --- /dev/null +++ b/app/viewmodels/checkAnswers/foreign/expenses/ForeignOtherAllowablePropertyExpensesSummary.scala @@ -0,0 +1,42 @@ +/* + * 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 viewmodels.checkAnswers.foreign.expenses + +import models.{CheckMode, UserAnswers} +import pages.foreign.expenses.ForeignOtherAllowablePropertyExpensesPage +import play.api.i18n.Messages +import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.checkAnswers.FormatUtils.{bigDecimalCurrency, valueCssClass} +import viewmodels.govuk.summarylist._ +import viewmodels.implicits._ + +object ForeignOtherAllowablePropertyExpensesSummary { + + def row(taxYear: Int, countryCode: String, answers: UserAnswers)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(ForeignOtherAllowablePropertyExpensesPage(countryCode)).map { + answer => + + SummaryListRowViewModel( + key = KeyViewModel("foreignOtherAllowablePropertyExpenses.checkYourAnswersLabel").withCssClass(valueCssClass), + value = ValueViewModel(bigDecimalCurrency(answer)).withCssClass(valueCssClass), + actions = Seq( + ActionItemViewModel("site.change", controllers.foreign.expenses.routes.ForeignOtherAllowablePropertyExpensesController.onPageLoad(taxYear, countryCode, CheckMode).url) + .withVisuallyHiddenText(messages("foreignOtherAllowablePropertyExpenses.change.hidden")) + ) + ) + } +} diff --git a/app/viewmodels/checkAnswers/foreign/expenses/ForeignPropertyRepairsAndMaintenanceSummary.scala b/app/viewmodels/checkAnswers/foreign/expenses/ForeignPropertyRepairsAndMaintenanceSummary.scala index a3af88b1..4972157a 100644 --- a/app/viewmodels/checkAnswers/foreign/expenses/ForeignPropertyRepairsAndMaintenanceSummary.scala +++ b/app/viewmodels/checkAnswers/foreign/expenses/ForeignPropertyRepairsAndMaintenanceSummary.scala @@ -16,11 +16,11 @@ package viewmodels.checkAnswers.foreign.expenses -import controllers.routes import models.{CheckMode, UserAnswers} import pages.foreign.expenses.ForeignPropertyRepairsAndMaintenancePage import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.checkAnswers.FormatUtils.{bigDecimalCurrency, keyCssClass, valueCssClass} import viewmodels.govuk.summarylist._ import viewmodels.implicits._ @@ -31,8 +31,8 @@ object ForeignPropertyRepairsAndMaintenanceSummary { answer => SummaryListRowViewModel( - key = "foreignPropertyRepairsAndMaintenance.checkYourAnswersLabel", - value = ValueViewModel(answer.toString), + key = KeyViewModel("foreignPropertyRepairsAndMaintenance.checkYourAnswersLabel").withCssClass(keyCssClass), + value = ValueViewModel(bigDecimalCurrency(answer)).withCssClass(valueCssClass), actions = Seq( ActionItemViewModel("site.change", controllers.foreign.expenses.routes.ForeignPropertyRepairsAndMaintenanceController.onPageLoad(taxYear, countryCode, CheckMode).url) .withVisuallyHiddenText(messages("foreignPropertyRepairsAndMaintenance.change.hidden")) diff --git a/app/views/foreign/expenses/ForeignOtherAllowablePropertyExpensesView.scala.html b/app/views/foreign/expenses/ForeignOtherAllowablePropertyExpensesView.scala.html new file mode 100644 index 00000000..a0086135 --- /dev/null +++ b/app/views/foreign/expenses/ForeignOtherAllowablePropertyExpensesView.scala.html @@ -0,0 +1,61 @@ +@* + * 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 viewmodels.InputWidth._ +@import viewmodels.LabelSize + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukInput: GovukInput, + govukButton: GovukButton +) + +@(form: Form[_], taxYear: Int, countryCode: String, individualOrAgent: String, mode: Mode)(implicit request: Request[_], messages: Messages) + +@layout(pageTitle = title(form, messages("foreignOtherAllowablePropertyExpenses.title"))) { + + @formHelper(action = controllers.foreign.expenses.routes.ForeignOtherAllowablePropertyExpensesController.onSubmit(taxYear, countryCode, mode), Symbol("autoComplete") -> "off") { + + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } +

@messages("foreignOtherAllowablePropertyExpenses.heading")

+ +

@messages("foreignOtherAllowablePropertyExpenses.hyperlink.text.1") @messages("foreignOtherAllowablePropertyExpenses.hyperlink.text.2")

+ @govukInput( + InputViewModel( + field = form("otherAllowablePropertyExpensesAmount"), + label = LabelViewModel(messages(s"foreignOtherAllowablePropertyExpenses.label.$individualOrAgent")).withCssClass("govuk-label govuk-label--m") + ).withHint(HintViewModel(messages("foreignOtherAllowablePropertyExpenses.hint"))) + .asNumeric() + .withWidth(Fixed10).withPrefix(PrefixOrSuffix(content = Text("£"))) + ) + + @govukButton( + ButtonViewModel(messages("site.continue")).withId("continue") + ) + } +} diff --git a/conf/foreign.routes b/conf/foreign.routes index 8b4a2893..8fff5832 100644 --- a/conf/foreign.routes +++ b/conf/foreign.routes @@ -103,7 +103,6 @@ POST /:taxYear/foreign-property/income/:countryCode/foreign-income-section GET /:taxYear/foreign-property/income/:countryCode/check-your-answers controllers.foreign.income.ForeignPropertyIncomeCheckYourAnswersController.onPageLoad(taxYear: Int, countryCode: String) POST /:taxYear/foreign-property/income/:countryCode/check-your-answers controllers.foreign.income.ForeignPropertyIncomeCheckYourAnswersController.onSubmit(taxYear: Int, countryCode: String) - GET /:taxYear/foreign-property/expenses/:countryCode/foreign-property-expenses-start controllers.foreign.expenses.ForeignPropertyExpensesStartController.onPageLoad(taxYear: Int, countryCode: String) GET /:taxYear/foreign-property/expenses/:countryCode/rents-rates-and-insurance controllers.foreign.expenses.ForeignRentsRatesAndInsuranceController.onPageLoad(taxYear: Int, countryCode: String, mode: Mode = NormalMode) @@ -133,3 +132,8 @@ POST /:taxYear/foreign-property/expenses/:countryCode/change-foreign-profe 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) + +GET /:taxYear/foreign-property/expenses/:countryCode/foreign-other-allowable-property-expenses controllers.foreign.expenses.ForeignOtherAllowablePropertyExpensesController.onPageLoad(taxYear: Int, countryCode: String, mode: Mode = NormalMode) +POST /:taxYear/foreign-property/expenses/:countryCode/foreign-other-allowable-property-expenses controllers.foreign.expenses.ForeignOtherAllowablePropertyExpensesController.onSubmit(taxYear: Int, countryCode: String, mode: Mode = NormalMode) +GET /:taxYear/foreign-property/expenses/:countryCode/change-foreign-other-allowable-property-expenses controllers.foreign.expenses.ForeignOtherAllowablePropertyExpensesController.onPageLoad(taxYear: Int, countryCode: String, mode: Mode = CheckMode) +POST /:taxYear/foreign-property/expenses/:countryCode/change-foreign-other-allowable-property-expenses controllers.foreign.expenses.ForeignOtherAllowablePropertyExpensesController.onSubmit(taxYear: Int, countryCode: String, mode: Mode = CheckMode) diff --git a/conf/messages.cy b/conf/messages.cy index 7197af50..8d7c9004 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -2130,4 +2130,29 @@ foreignProfessionalFees.error.nonNumeric.agent = *Missing Welsh* foreignProfessionalFees.error.required.individual = *Missing Welsh* foreignProfessionalFees.error.required.agent = *Missing Welsh* foreignProfessionalFees.error.outOfRange = *Missing Welsh* -foreignProfessionalFees.change.hidden = *Missing Welsh* \ No newline at end of file +foreignProfessionalFees.change.hidden = *Missing Welsh* + +foreignOtherAllowablePropertyExpenses.title = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.heading = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet1 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet2 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet3 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet4 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet5.individual = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet5.agent = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet6 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet7.individual = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.bullet7.agent = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.hyperlink.text.1 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.hyperlink.text.2 = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.hyperlink.href = https://www.gov.uk/expenses-if-youre-self-employed +foreignOtherAllowablePropertyExpenses.label.individual = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.label.agent = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.hint = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.checkYourAnswersLabel = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.error.nonNumeric.individual = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.error.nonNumeric.agent = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.error.required.individual = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.error.required.agent = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.error.outOfRange = *Missing Welsh* +foreignOtherAllowablePropertyExpenses.change.hidden = *Missing Welsh* \ No newline at end of file diff --git a/conf/messages.en b/conf/messages.en index 4d70e528..b0638171 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -2126,7 +2126,7 @@ foreignNonResidentialPropertyFinanceCosts.bullet.2 = any interest on the loan or foreignNonResidentialPropertyFinanceCosts.p2.individual = You cannot claim the cost of any capital repayments from your mortgage. foreignNonResidentialPropertyFinanceCosts.p2.agent = They cannot claim the cost of any capital repayments from their mortgage. foreignNonResidentialPropertyFinanceCosts.legend.individual = How much were your non-residential property finance costs? -foreignNonResidentialPropertyFinanceCosts.legend.agent = How much were your client's non-residential property finance costs? +foreignNonResidentialPropertyFinanceCosts.legend.agent = How much were your client''s non-residential property finance costs? foreignNonResidentialPropertyFinanceCosts.checkYourAnswersLabel = Non-residential property finance costs foreignNonResidentialPropertyFinanceCosts.error.nonNumeric.individual = The amount you spent on non-residential property finance costs can only include pounds and pence, for example £600 or £600.20 foreignNonResidentialPropertyFinanceCosts.error.nonNumeric.agent = The amount your client spent on non-residential property finance costs can only include pounds and pence, for example £600 or £600.20 @@ -2157,3 +2157,28 @@ foreignProfessionalFees.error.required.individual = Enter how much you spent on foreignProfessionalFees.error.required.agent = Enter how much your client spent on legal, management or other professional fees foreignProfessionalFees.error.outOfRange = Enter an amount between {0} and {1} foreignProfessionalFees.change.hidden = Legal, management or other professional fees + +foreignOtherAllowablePropertyExpenses.title = Other allowable property expenses +foreignOtherAllowablePropertyExpenses.heading = Other allowable property expenses +foreignOtherAllowablePropertyExpenses.bullet1 = office costs, for example stationery or phone bills +foreignOtherAllowablePropertyExpenses.bullet2 = clothing expenses, for example uniforms +foreignOtherAllowablePropertyExpenses.bullet3 = things you buy to sell on, for example stock or raw materials +foreignOtherAllowablePropertyExpenses.bullet4 = financial costs, for example insurance or bank charges +foreignOtherAllowablePropertyExpenses.bullet5.individual = costs of your business premises, for example heating, lighting, business rates +foreignOtherAllowablePropertyExpenses.bullet5.agent = costs of your client''s business premises, for example heating, lighting, business rates +foreignOtherAllowablePropertyExpenses.bullet6 = advertising or marketing, for example website costs +foreignOtherAllowablePropertyExpenses.bullet7.individual = training courses related to your business, for example refresher courses +foreignOtherAllowablePropertyExpenses.bullet7.agent = training courses related to their business, for example refresher courses +foreignOtherAllowablePropertyExpenses.hyperlink.text.1 = Read more about +foreignOtherAllowablePropertyExpenses.hyperlink.text.2 = allowable property expenses at GOV.UK (opens in new tab). +foreignOtherAllowablePropertyExpenses.hyperlink.href = https://www.gov.uk/expenses-if-youre-self-employed +foreignOtherAllowablePropertyExpenses.label.individual = How much were your other allowable property expenses? +foreignOtherAllowablePropertyExpenses.label.agent = How much were your client''s other allowable property expenses? +foreignOtherAllowablePropertyExpenses.hint = For example, £900 or £100.20 +foreignOtherAllowablePropertyExpenses.checkYourAnswersLabel = Other allowable property expenses +foreignOtherAllowablePropertyExpenses.error.nonNumeric.individual = The amount for your other allowable property expenses can only include pounds and pence, for example £600 or £600.20 +foreignOtherAllowablePropertyExpenses.error.nonNumeric.agent = The amount for your client''s other allowable property expenses can only include pounds and pence, for example £600 or £600.20 +foreignOtherAllowablePropertyExpenses.error.required.individual = Enter the amount for your other allowable property expenses +foreignOtherAllowablePropertyExpenses.error.required.agent = Enter the amount for your client''s other allowable property expenses +foreignOtherAllowablePropertyExpenses.error.outOfRange = Enter an amount between {0} and {1} +foreignOtherAllowablePropertyExpenses.change.hidden = Other allowable property expenses diff --git a/test/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesControllerSpec.scala b/test/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesControllerSpec.scala new file mode 100644 index 00000000..455a233f --- /dev/null +++ b/test/controllers/foreign/expenses/ForeignOtherAllowablePropertyExpensesControllerSpec.scala @@ -0,0 +1,166 @@ +/* + * 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 controllers.routes +import forms.foreign.expenses.ForeignOtherAllowablePropertyExpensesFormProvider +import models.{NormalMode, UserAnswers} +import navigation.{FakeForeignPropertyNavigator, FakeNavigator, ForeignPropertyNavigator, Navigator} +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import pages.foreign.expenses.ForeignOtherAllowablePropertyExpensesPage +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 views.html.foreign.expenses.ForeignOtherAllowablePropertyExpensesView + +import scala.concurrent.Future + +class ForeignOtherAllowablePropertyExpensesControllerSpec extends SpecBase with MockitoSugar { + + val formProvider = new ForeignOtherAllowablePropertyExpensesFormProvider() + private val isAgentMessageKey = "individual" + val form: Form[BigDecimal] = formProvider(isAgentMessageKey) + + def onwardRoute = Call("GET", "/foo") + + val validAnswer: BigDecimal = BigDecimal(0) + val taxYear = 2023 + val countryCode = "AUS" + + lazy val foreignOtherAllowablePropertyExpensesRoute = + controllers.foreign.expenses.routes.ForeignOtherAllowablePropertyExpensesController.onPageLoad(taxYear, countryCode, NormalMode).url + + "ForeignOtherAllowablePropertyExpenses Controller" - { + + "must return OK and the correct view for a GET" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers), isAgent = false).build() + + running(application) { + val request = FakeRequest(GET, foreignOtherAllowablePropertyExpensesRoute) + + val result = route(application, request).value + + val view = application.injector.instanceOf[ForeignOtherAllowablePropertyExpensesView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form, taxYear, countryCode, isAgentMessageKey, NormalMode)(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(ForeignOtherAllowablePropertyExpensesPage(countryCode), validAnswer).success.value + + val application = applicationBuilder(userAnswers = Some(userAnswers), isAgent = false).build() + + running(application) { + val request = FakeRequest(GET, foreignOtherAllowablePropertyExpensesRoute) + + val view = application.injector.instanceOf[ForeignOtherAllowablePropertyExpensesView] + + val result = route(application, request).value + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form.fill(validAnswer), taxYear, countryCode, isAgentMessageKey, NormalMode)(request, messages(application)).toString + } + } + + "must redirect to the next page when valid data is submitted" in { + + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers), isAgent = false) + .overrides( + bind[ForeignPropertyNavigator].toInstance(new FakeForeignPropertyNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + val request = + FakeRequest(POST, foreignOtherAllowablePropertyExpensesRoute) + .withFormUrlEncodedBody(("otherAllowablePropertyExpensesAmount", validAnswer.toString)) + + 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 = false).build() + + running(application) { + val request = + FakeRequest(POST, foreignOtherAllowablePropertyExpensesRoute) + .withFormUrlEncodedBody(("otherAllowablePropertyExpensesAmount", "invalid value")) + + val boundForm = form.bind(Map("otherAllowablePropertyExpensesAmount" -> "invalid value")) + + val view = application.injector.instanceOf[ForeignOtherAllowablePropertyExpensesView] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + contentAsString(result) mustEqual view(boundForm, taxYear, countryCode, isAgentMessageKey, NormalMode)(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 = false).build() + + running(application) { + val request = FakeRequest(GET, foreignOtherAllowablePropertyExpensesRoute) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual 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 = false).build() + + running(application) { + val request = + FakeRequest(POST, foreignOtherAllowablePropertyExpensesRoute) + .withFormUrlEncodedBody(("otherAllowablePropertyExpensesAmount", validAnswer.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + } + } + } +} diff --git a/test/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProviderSpec.scala b/test/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProviderSpec.scala new file mode 100644 index 00000000..800a9316 --- /dev/null +++ b/test/forms/foreign/expenses/ForeignOtherAllowablePropertyExpensesFormProviderSpec.scala @@ -0,0 +1,62 @@ +/* + * 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.CurrencyFieldBehaviours +import play.api.data.FormError + +class ForeignOtherAllowablePropertyExpensesFormProviderSpec extends CurrencyFieldBehaviours { + + val form = new ForeignOtherAllowablePropertyExpensesFormProvider()("individual") + + ".otherAllowablePropertyExpensesAmount" - { + + val fieldName = "otherAllowablePropertyExpensesAmount" + + val minimum = 0 + val maximum = 100000000 + + val validDataGenerator = intsInRangeWithCommas(minimum, maximum) + + behave like fieldThatBindsValidData( + form, + fieldName, + validDataGenerator + ) + + behave like currencyField( + form, + fieldName, + nonNumericError = FormError(fieldName, "foreignOtherAllowablePropertyExpenses.error.nonNumeric.individual"), + twoDecimalPlacesError = FormError(fieldName, "foreignOtherAllowablePropertyExpenses.error.nonNumeric.individual") + ) + + behave like currencyFieldWithRange( + form, + fieldName, + minimum = minimum, + maximum = maximum, + expectedError = FormError(fieldName, "foreignOtherAllowablePropertyExpenses.error.outOfRange", Seq(minimum, maximum)) + ) + + behave like mandatoryField( + form, + fieldName, + requiredError = FormError(fieldName, "foreignOtherAllowablePropertyExpenses.error.required.individual") + ) + } +}