From a0060439180fb9035582bbf957432e27dfc60889 Mon Sep 17 00:00:00 2001 From: Warren Smithson <36073378+warrenskiwork1979@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:35:14 +0000 Subject: [PATCH] VEIOSS-350 | Allow Trader to Select Countries for Return (#4) * VEIOSS-350 | Add dropdown select for country. Create NoPeriodsAvailable page. Add missing captions to initial pages * VEIOSS-250 | Create AddToListPage for SoldToCountry. Refactor Action to exclude peroid param and refactor all associated controllers. WIP * VEIOSS-350 | Use Button group in view. --- .../controllers/$className$Controller.scala | 4 +- .../controllers/$className$Controller.scala | 4 +- .../controllers/$className$Controller.scala | 4 +- .../controllers/$className$Controller.scala | 4 +- .../controllers/$className$Controller.scala | 4 +- app/controllers/GetCountry.scala | 37 +++ app/controllers/IndexController.scala | 3 +- .../NoOtherPeriodsAvailableController.scala | 40 +++ .../SalesToCountryController.scala | 34 +-- app/controllers/SoldGoodsController.scala | 33 +-- app/controllers/SoldToCountryController.scala | 31 ++- .../SoldToCountryListController.scala | 87 +++++++ app/controllers/StartReturnController.scala | 29 +-- app/controllers/VatOnSalesController.scala | 34 +-- .../VatRatesFromCountryController.scala | 60 +++-- app/controllers/YourAccountController.scala | 8 +- .../AuthenticatedControllerComponents.scala | 9 +- .../actions/DataRetrievalAction.scala | 11 +- .../actions/IdentifierAction.scala | 11 +- app/controllers/auth/AuthController.scala | 3 +- app/forms/SoldToCountryListFormProvider.scala | 30 +++ app/models/Country.scala | 14 ++ app/models/UserAnswers.scala | 9 +- app/pages/NoOtherPeriodsAvailablePage.scala | 25 ++ app/pages/SalesToCountryPage.scala | 10 +- app/pages/SoldGoodsPage.scala | 8 +- app/pages/SoldToCountryListPage.scala | 71 ++++++ app/pages/SoldToCountryPage.scala | 12 +- app/pages/StartReturnPage.scala | 7 +- app/pages/VatOnSalesPage.scala | 10 +- app/pages/VatRatesFromCountryPage.scala | 10 +- app/pages/Waypoint.scala | 4 +- app/pages/YourAccountPage.scala | 25 ++ app/pages/package.scala | 24 ++ app/queries/DeriveNumberOfSales.scala | 27 ++ app/repositories/SessionRepository.scala | 18 +- app/utils/ItemsHelper.scala | 59 +++++ app/viewmodels/ListItemWrapper.scala | 21 ++ .../checkAnswers/SalesToCountrySummary.scala | 8 +- .../checkAnswers/SoldGoodsSummary.scala | 8 +- .../SoldToCountryListSummary.scala | 38 +++ .../checkAnswers/SoldToCountrySummary.scala | 8 +- .../checkAnswers/VatOnSalesSummary.scala | 8 +- .../VatRatesFromCountrySummary.scala | 8 +- app/viewmodels/govuk/CheckboxFluency.scala | 3 + app/viewmodels/govuk/FieldsetFluency.scala | 5 +- app/viewmodels/govuk/SelectFluency.scala | 81 ++++++ app/viewmodels/govuk/package.scala | 1 + .../NoOtherPeriodsAvailableView.scala.html | 33 +++ app/views/SalesToCountryView.scala.html | 2 +- app/views/SoldGoodsView.scala.html | 35 +-- app/views/SoldToCountryListView.scala.html | 75 ++++++ app/views/SoldToCountryView.scala.html | 29 ++- app/views/VatOnSalesView.scala.html | 2 +- app/views/VatRatesFromCountryView.scala.html | 11 +- app/views/components/addToList.scala.html | 46 ++++ conf/app.routes | 56 +++-- conf/messages.en | 22 +- test/controllers/IndexControllerSpec.scala | 2 +- ...oOtherPeriodsAvailableControllerSpec.scala | 44 ++++ .../SalesToCountryControllerSpec.scala | 9 +- .../controllers/SoldGoodsControllerSpec.scala | 9 +- .../SoldToCountryControllerSpec.scala | 6 +- .../SoldToCountryListControllerSpec.scala | 236 ++++++++++++++++++ .../StartReturnControllerSpec.scala | 60 +---- .../VatOnSalesControllerSpec.scala | 11 +- .../VatRatesFromCountryControllerSpec.scala | 35 ++- .../YourAccountControllerSpec.scala | 2 +- .../actions/DataRetrievalActionSpec.scala | 14 +- .../actions/FakeDataRetrievalAction.scala | 5 +- .../actions/GetRegistrationActionSpec.scala | 2 +- .../SoldToCountryListFormProviderSpec.scala | 45 ++++ 72 files changed, 1450 insertions(+), 343 deletions(-) create mode 100644 app/controllers/GetCountry.scala create mode 100644 app/controllers/NoOtherPeriodsAvailableController.scala create mode 100644 app/controllers/SoldToCountryListController.scala create mode 100644 app/forms/SoldToCountryListFormProvider.scala create mode 100644 app/pages/NoOtherPeriodsAvailablePage.scala create mode 100644 app/pages/SoldToCountryListPage.scala create mode 100644 app/pages/YourAccountPage.scala create mode 100644 app/pages/package.scala create mode 100644 app/queries/DeriveNumberOfSales.scala create mode 100644 app/utils/ItemsHelper.scala create mode 100644 app/viewmodels/ListItemWrapper.scala create mode 100644 app/viewmodels/checkAnswers/SoldToCountryListSummary.scala create mode 100644 app/viewmodels/govuk/SelectFluency.scala create mode 100644 app/views/NoOtherPeriodsAvailableView.scala.html create mode 100644 app/views/SoldToCountryListView.scala.html create mode 100644 app/views/components/addToList.scala.html create mode 100644 test/controllers/NoOtherPeriodsAvailableControllerSpec.scala create mode 100644 test/controllers/SoldToCountryListControllerSpec.scala create mode 100644 test/forms/SoldToCountryListFormProviderSpec.scala diff --git a/.g8/checkboxPage/app/controllers/$className$Controller.scala b/.g8/checkboxPage/app/controllers/$className$Controller.scala index d2d6f524..9a0763c7 100644 --- a/.g8/checkboxPage/app/controllers/$className$Controller.scala +++ b/.g8/checkboxPage/app/controllers/$className$Controller.scala @@ -25,7 +25,7 @@ class $className$Controller @Inject()( val form = formProvider() - def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData() { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +36,7 @@ class $className$Controller @Inject()( Ok(view(preparedForm, mode, period)) } - def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData().async { implicit request => form.bindFromRequest().fold( diff --git a/.g8/intPage/app/controllers/$className$Controller.scala b/.g8/intPage/app/controllers/$className$Controller.scala index d2d6f524..9a0763c7 100644 --- a/.g8/intPage/app/controllers/$className$Controller.scala +++ b/.g8/intPage/app/controllers/$className$Controller.scala @@ -25,7 +25,7 @@ class $className$Controller @Inject()( val form = formProvider() - def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData() { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +36,7 @@ class $className$Controller @Inject()( Ok(view(preparedForm, mode, period)) } - def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData().async { implicit request => form.bindFromRequest().fold( diff --git a/.g8/radioButtonPage/app/controllers/$className$Controller.scala b/.g8/radioButtonPage/app/controllers/$className$Controller.scala index dadd0d31..fcb2c1c5 100644 --- a/.g8/radioButtonPage/app/controllers/$className$Controller.scala +++ b/.g8/radioButtonPage/app/controllers/$className$Controller.scala @@ -25,7 +25,7 @@ class $className$Controller @Inject()( val form = formProvider() - def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData() { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +36,7 @@ class $className$Controller @Inject()( Ok(view(preparedForm, mode, period)) } - def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData().async { implicit request => form.bindFromRequest().fold( diff --git a/.g8/stringPage/app/controllers/$className$Controller.scala b/.g8/stringPage/app/controllers/$className$Controller.scala index 1ab61500..e724ee49 100644 --- a/.g8/stringPage/app/controllers/$className$Controller.scala +++ b/.g8/stringPage/app/controllers/$className$Controller.scala @@ -25,7 +25,7 @@ class $className$Controller @Inject()( val form = formProvider() - def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData() { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +36,7 @@ class $className$Controller @Inject()( Ok(view(preparedForm, mode, period)) } - def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(mode: Mode): Action[AnyContent] = cc.authAndRequireData().async { implicit request => form.bindFromRequest().fold( diff --git a/.g8/yesNoPage/app/controllers/$className$Controller.scala b/.g8/yesNoPage/app/controllers/$className$Controller.scala index 8092ede3..6b6bcc66 100644 --- a/.g8/yesNoPage/app/controllers/$className$Controller.scala +++ b/.g8/yesNoPage/app/controllers/$className$Controller.scala @@ -25,7 +25,7 @@ class $className;format="cap"$Controller @Inject()( val form = formProvider() - def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData() { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +36,7 @@ class $className;format="cap"$Controller @Inject()( Ok(view(preparedForm, mode, period)) } - def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(mode: Mode, period: Period): Action[AnyContent] = cc.authAndRequireData().async { implicit request => form.bindFromRequest().fold( diff --git a/app/controllers/GetCountry.scala b/app/controllers/GetCountry.scala new file mode 100644 index 00000000..7ce805bd --- /dev/null +++ b/app/controllers/GetCountry.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2023 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 + +import models.requests.DataRequest +import models.{Country, Index} +import pages.{JourneyRecoveryPage, SoldToCountryPage, Waypoints} +import play.api.mvc.Results.Redirect +import play.api.mvc.{AnyContent, Result} +import utils.FutureSyntax.FutureOps + +import scala.concurrent.Future + +trait GetCountry { + + protected def getCountry(waypoints: Waypoints, index: Index) + (block: Country => Future[Result]) + (implicit request: DataRequest[AnyContent]): Future[Result] = + request.userAnswers + .get(SoldToCountryPage(index)) + .map(block(_)) + .getOrElse(Redirect(JourneyRecoveryPage.route(waypoints)).toFuture) +} diff --git a/app/controllers/IndexController.scala b/app/controllers/IndexController.scala index 519d962d..74967587 100644 --- a/app/controllers/IndexController.scala +++ b/app/controllers/IndexController.scala @@ -17,6 +17,7 @@ package controllers import controllers.actions.AuthenticatedControllerComponents +import pages.EmptyWaypoints import play.api.i18n.I18nSupport import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController @@ -30,6 +31,6 @@ class IndexController @Inject()( protected val controllerComponents: MessagesControllerComponents = cc def onPageLoad: Action[AnyContent] = cc.authAndGetRegistration { _ => - Redirect(routes.YourAccountController.onPageLoad) + Redirect(routes.YourAccountController.onPageLoad(waypoints = EmptyWaypoints)) } } diff --git a/app/controllers/NoOtherPeriodsAvailableController.scala b/app/controllers/NoOtherPeriodsAvailableController.scala new file mode 100644 index 00000000..1db9cab2 --- /dev/null +++ b/app/controllers/NoOtherPeriodsAvailableController.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 + +import controllers.actions._ +import pages.Waypoints +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.NoOtherPeriodsAvailableView + +import javax.inject.Inject + +class NoOtherPeriodsAvailableController @Inject()( + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + view: NoOtherPeriodsAvailableView + ) extends FrontendBaseController with I18nSupport { + + protected val controllerComponents: MessagesControllerComponents = cc + + def onPageLoad(waypoints: Waypoints): Action[AnyContent] = (cc.actionBuilder andThen cc.identify) { + implicit request => + Ok(view(waypoints)) + } +} diff --git a/app/controllers/SalesToCountryController.scala b/app/controllers/SalesToCountryController.scala index 3578b1b6..a3a3ab10 100644 --- a/app/controllers/SalesToCountryController.scala +++ b/app/controllers/SalesToCountryController.scala @@ -18,31 +18,35 @@ package controllers import controllers.actions._ import forms.SalesToCountryFormProvider -import models.{Index, Period} +import models.Index import pages.{SalesToCountryPage, Waypoints} +import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import views.html.SalesToCountryView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} class SalesToCountryController @Inject()( - override val messagesApi: MessagesApi, - cc: AuthenticatedControllerComponents, - formProvider: SalesToCountryFormProvider, - view: SalesToCountryView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: SalesToCountryFormProvider, + view: SalesToCountryView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc - val form = formProvider() + val form: Form[Int] = formProvider() - def onPageLoad(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData() { implicit request => - val preparedForm = request.userAnswers.get(SalesToCountryPage(period, index)) match { + val period = request.userAnswers.period + + val preparedForm = request.userAnswers.get(SalesToCountryPage(index)) match { case None => form case Some(value) => form.fill(value) } @@ -50,18 +54,20 @@ class SalesToCountryController @Inject()( Ok(view(preparedForm, waypoints, period, index)) } - def onSubmit(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData().async { implicit request => + val period = request.userAnswers.period + form.bindFromRequest().fold( formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, period, index))), + BadRequest(view(formWithErrors, waypoints, period, index)).toFuture, value => for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(SalesToCountryPage(period, index), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(SalesToCountryPage(period, index).navigate(waypoints, request.userAnswers, updatedAnswers).route) + updatedAnswers <- Future.fromTry(request.userAnswers.set(SalesToCountryPage(index), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(SalesToCountryPage(index).navigate(waypoints, request.userAnswers, updatedAnswers).route) ) } } diff --git a/app/controllers/SoldGoodsController.scala b/app/controllers/SoldGoodsController.scala index 921ddb22..81d3c126 100644 --- a/app/controllers/SoldGoodsController.scala +++ b/app/controllers/SoldGoodsController.scala @@ -18,31 +18,34 @@ package controllers import controllers.actions._ import forms.SoldGoodsFormProvider -import models.Period import pages.{SoldGoodsPage, Waypoints} +import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import views.html.SoldGoodsView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} class SoldGoodsController @Inject()( - override val messagesApi: MessagesApi, - cc: AuthenticatedControllerComponents, - formProvider: SoldGoodsFormProvider, - view: SoldGoodsView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: SoldGoodsFormProvider, + view: SoldGoodsView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc - val form = formProvider() + val form: Form[Boolean] = formProvider() - def onPageLoad(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData() { implicit request => - val preparedForm = request.userAnswers.get(SoldGoodsPage(period)) match { + val period = request.userAnswers.period + + val preparedForm = request.userAnswers.get(SoldGoodsPage) match { case None => form case Some(value) => form.fill(value) } @@ -50,18 +53,20 @@ class SoldGoodsController @Inject()( Ok(view(preparedForm, waypoints, period)) } - def onSubmit(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData().async { implicit request => + val period = request.userAnswers.period + form.bindFromRequest().fold( formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, period))), + BadRequest(view(formWithErrors, waypoints, period)).toFuture, value => for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(SoldGoodsPage(period), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(SoldGoodsPage(period).navigate(waypoints, request.userAnswers, updatedAnswers).route) + updatedAnswers <- Future.fromTry(request.userAnswers.set(SoldGoodsPage, value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(SoldGoodsPage.navigate(waypoints, request.userAnswers, updatedAnswers).route) ) } } diff --git a/app/controllers/SoldToCountryController.scala b/app/controllers/SoldToCountryController.scala index d49bb8df..c697b381 100644 --- a/app/controllers/SoldToCountryController.scala +++ b/app/controllers/SoldToCountryController.scala @@ -18,29 +18,32 @@ package controllers import controllers.actions._ import forms.SoldToCountryFormProvider -import models.{Index, Period} +import models.Index import pages.{SoldToCountryPage, Waypoints} import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import queries.AllSalesQuery import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import views.html.SoldToCountryView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} class SoldToCountryController @Inject()( - override val messagesApi: MessagesApi, - cc: AuthenticatedControllerComponents, - formProvider: SoldToCountryFormProvider, - view: SoldToCountryView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: SoldToCountryFormProvider, + view: SoldToCountryView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc - def onPageLoad(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData() { implicit request => + val period = request.userAnswers.period + val form = formProvider( index, request.userAnswers.get(AllSalesQuery) @@ -48,7 +51,7 @@ class SoldToCountryController @Inject()( .map(_.country) ) - val preparedForm = request.userAnswers.get(SoldToCountryPage(period, index)) match { + val preparedForm = request.userAnswers.get(SoldToCountryPage(index)) match { case None => form case Some(value) => form.fill(value) } @@ -56,9 +59,11 @@ class SoldToCountryController @Inject()( Ok(view(preparedForm, waypoints, period, index)) } - def onSubmit(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData().async { implicit request => + val period = request.userAnswers.period + val form = formProvider( index, request.userAnswers.get(AllSalesQuery) @@ -68,13 +73,13 @@ class SoldToCountryController @Inject()( form.bindFromRequest().fold( formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, period, index))), + BadRequest(view(formWithErrors, waypoints, period, index)).toFuture, value => for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(SoldToCountryPage(period, index), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(SoldToCountryPage(period, index).navigate(waypoints, request.userAnswers, updatedAnswers).route) + updatedAnswers <- Future.fromTry(request.userAnswers.set(SoldToCountryPage(index), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(SoldToCountryPage(index).navigate(waypoints, request.userAnswers, updatedAnswers).route) ) } } diff --git a/app/controllers/SoldToCountryListController.scala b/app/controllers/SoldToCountryListController.scala new file mode 100644 index 00000000..43e7bc78 --- /dev/null +++ b/app/controllers/SoldToCountryListController.scala @@ -0,0 +1,87 @@ +/* + * Copyright 2023 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 + +import controllers.actions._ +import forms.SoldToCountryListFormProvider +import models.Country +import pages.{SoldToCountryListPage, Waypoints} +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import queries.DeriveNumberOfSales +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps +import utils.ItemsHelper.getDerivedItems +import viewmodels.checkAnswers.SoldToCountryListSummary +import views.html.SoldToCountryListView + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class SoldToCountryListController @Inject()( + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: SoldToCountryListFormProvider, + view: SoldToCountryListView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + protected val controllerComponents: MessagesControllerComponents = cc + + val form: Form[Boolean] = formProvider() + + def onPageLoad(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData().async { + implicit request => + + val period = request.userAnswers.period + + getDerivedItems(waypoints, DeriveNumberOfSales) { + number => + + val canAddSales = number < Country.euCountriesWithNI.size + val salesSummary = SoldToCountryListSummary + .addToListRows(request.userAnswers, waypoints, SoldToCountryListPage()) + + Ok(view(form, waypoints, period, salesSummary, canAddSales)).toFuture + } + } + + def onSubmit(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData().async { + implicit request => + + val period = request.userAnswers.period + + getDerivedItems(waypoints, DeriveNumberOfSales) { + number => + + val canAddSales = number < Country.euCountriesWithNI.size + val salesSummary = SoldToCountryListSummary + .addToListRows(request.userAnswers, waypoints, SoldToCountryListPage()) + + form.bindFromRequest().fold( + formWithErrors => + BadRequest(view(formWithErrors, waypoints, period, salesSummary, canAddSales)).toFuture, + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(SoldToCountryListPage(), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(SoldToCountryListPage().navigate(waypoints, request.userAnswers, updatedAnswers).route) + ) + } + } +} diff --git a/app/controllers/StartReturnController.scala b/app/controllers/StartReturnController.scala index df0b89fe..3cc36eec 100644 --- a/app/controllers/StartReturnController.scala +++ b/app/controllers/StartReturnController.scala @@ -20,6 +20,7 @@ import controllers.actions._ import forms.StartReturnFormProvider import models.{Period, UserAnswers} import pages.{StartReturnPage, Waypoints} +import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController @@ -40,15 +41,17 @@ class StartReturnController @Inject()( protected val controllerComponents: MessagesControllerComponents = cc - val form = formProvider() + val form: Form[Boolean] = formProvider() - def onPageLoad(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndGetOptionalData(period) { + def onPageLoad(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndGetOptionalData() { implicit request => + // TODO check for starting correct period + Ok(view(form, waypoints, period)) } - def onSubmit(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndGetOptionalData(period).async { + def onSubmit(waypoints: Waypoints, period: Period): Action[AnyContent] = cc.authAndGetOptionalData().async { implicit request => form.bindFromRequest().fold( @@ -57,23 +60,13 @@ class StartReturnController @Inject()( value => { - val answers: UserAnswers = request.userAnswers.getOrElse(UserAnswers( - request.userId, - period = period, - lastUpdated = Instant.now(clock) - )) - - val dbCall = if (!value) { - cc.sessionRepository.clear(request.userId) - } else { - Future.fromTry(answers.set(StartReturnPage(period), value)).flatMap { updatedAnswers => - cc.sessionRepository.set(updatedAnswers) - } - } + val initialisedAnswers = UserAnswers(id = request.userId, period = period, lastUpdated = Instant.now(clock)) for { - _ <- dbCall - } yield Redirect(StartReturnPage(period).navigate(waypoints, answers, answers).route) + answers <- request.userAnswers.getOrElse(initialisedAnswers).toFuture + updatedAnswers <- Future.fromTry(answers.set(StartReturnPage(period), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(StartReturnPage(period).navigate(waypoints, answers, updatedAnswers).route) } ) } diff --git a/app/controllers/VatOnSalesController.scala b/app/controllers/VatOnSalesController.scala index daceaae6..8d6e2298 100644 --- a/app/controllers/VatOnSalesController.scala +++ b/app/controllers/VatOnSalesController.scala @@ -18,31 +18,35 @@ package controllers import controllers.actions._ import forms.VatOnSalesFormProvider -import models.{Index, Period} +import models.{Index, VatOnSales} import pages.{VatOnSalesPage, Waypoints} +import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import views.html.VatOnSalesView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} class VatOnSalesController @Inject()( - override val messagesApi: MessagesApi, - cc: AuthenticatedControllerComponents, - formProvider: VatOnSalesFormProvider, - view: VatOnSalesView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: VatOnSalesFormProvider, + view: VatOnSalesView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc - val form = formProvider() + val form: Form[VatOnSales] = formProvider() - def onPageLoad(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData() { implicit request => - val preparedForm = request.userAnswers.get(VatOnSalesPage(period, index)) match { + val period = request.userAnswers.period + + val preparedForm = request.userAnswers.get(VatOnSalesPage(index)) match { case None => form case Some(value) => form.fill(value) } @@ -50,18 +54,20 @@ class VatOnSalesController @Inject()( Ok(view(preparedForm, waypoints, period, index)) } - def onSubmit(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData().async { implicit request => + val period = request.userAnswers.period + form.bindFromRequest().fold( formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, period, index))), + BadRequest(view(formWithErrors, waypoints, period, index)).toFuture, value => for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(VatOnSalesPage(period, index), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(VatOnSalesPage(period, index).navigate(waypoints, request.userAnswers, updatedAnswers).route) + updatedAnswers <- Future.fromTry(request.userAnswers.set(VatOnSalesPage(index), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(VatOnSalesPage(index).navigate(waypoints, request.userAnswers, updatedAnswers).route) ) } } diff --git a/app/controllers/VatRatesFromCountryController.scala b/app/controllers/VatRatesFromCountryController.scala index 3f340a84..a07ec44e 100644 --- a/app/controllers/VatRatesFromCountryController.scala +++ b/app/controllers/VatRatesFromCountryController.scala @@ -18,50 +18,64 @@ package controllers import controllers.actions._ import forms.VatRatesFromCountryFormProvider -import models.{Index, Period} +import models.{Index, VatRatesFromCountry} import pages.{VatRatesFromCountryPage, Waypoints} +import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import views.html.VatRatesFromCountryView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} class VatRatesFromCountryController @Inject()( - override val messagesApi: MessagesApi, - cc: AuthenticatedControllerComponents, - formProvider: VatRatesFromCountryFormProvider, - view: VatRatesFromCountryView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + override val messagesApi: MessagesApi, + cc: AuthenticatedControllerComponents, + formProvider: VatRatesFromCountryFormProvider, + view: VatRatesFromCountryView + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with GetCountry { protected val controllerComponents: MessagesControllerComponents = cc - val form = formProvider() + val form: Form[Set[VatRatesFromCountry]] = formProvider() - def onPageLoad(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period) { + def onPageLoad(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData().async { implicit request => - val preparedForm = request.userAnswers.get(VatRatesFromCountryPage(period, index)) match { - case None => form - case Some(value) => form.fill(value) - } + val period = request.userAnswers.period + + getCountry(waypoints, index) { + country => - Ok(view(preparedForm, waypoints, period, index)) + val preparedForm = request.userAnswers.get(VatRatesFromCountryPage(index)) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, waypoints, period, index, country)).toFuture + } } - def onSubmit(waypoints: Waypoints, period: Period, index: Index): Action[AnyContent] = cc.authAndRequireData(period).async { + def onSubmit(waypoints: Waypoints, index: Index): Action[AnyContent] = cc.authAndRequireData().async { implicit request => - form.bindFromRequest().fold( - formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, period, index))), + val period = request.userAnswers.period - value => - for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(VatRatesFromCountryPage(period, index), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(VatRatesFromCountryPage(period, index).navigate(waypoints, request.userAnswers, updatedAnswers).route) - ) + getCountry(waypoints, index) { + country => + + form.bindFromRequest().fold( + formWithErrors => + BadRequest(view(formWithErrors, waypoints, period, index, country)).toFuture, + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(VatRatesFromCountryPage(index), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(VatRatesFromCountryPage(index).navigate(waypoints, request.userAnswers, updatedAnswers).route) + ) + } } } diff --git a/app/controllers/YourAccountController.scala b/app/controllers/YourAccountController.scala index f753ee41..9bc89310 100644 --- a/app/controllers/YourAccountController.scala +++ b/app/controllers/YourAccountController.scala @@ -19,29 +19,27 @@ package controllers import config.FrontendAppConfig import controllers.actions.AuthenticatedControllerComponents import logging.Logging +import pages.Waypoints import play.api.i18n.I18nSupport import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import views.html.YourAccountView import javax.inject.Inject -import scala.concurrent.ExecutionContext class YourAccountController @Inject()( cc: AuthenticatedControllerComponents, view: YourAccountView, appConfig: FrontendAppConfig - )(implicit ec: ExecutionContext) + ) extends FrontendBaseController with I18nSupport with Logging { protected val controllerComponents: MessagesControllerComponents = cc - def onPageLoad: Action[AnyContent] = cc.authAndGetRegistration { + def onPageLoad(waypoints: Waypoints): Action[AnyContent] = cc.authAndGetRegistration { implicit request => Ok(view(request.registrationWrapper.vatInfo.getName, request.iossNumber, appConfig.amendRegistrationUrl)) } - - } diff --git a/app/controllers/actions/AuthenticatedControllerComponents.scala b/app/controllers/actions/AuthenticatedControllerComponents.scala index ed79745d..0d69b516 100644 --- a/app/controllers/actions/AuthenticatedControllerComponents.scala +++ b/app/controllers/actions/AuthenticatedControllerComponents.scala @@ -17,7 +17,6 @@ package controllers.actions import models.requests.{DataRequest, IdentifierRequest, OptionalDataRequest, RegistrationRequest} -import models.Period import play.api.http.FileMimeTypes import play.api.i18n.{Langs, MessagesApi} import play.api.mvc._ @@ -48,13 +47,13 @@ trait AuthenticatedControllerComponents extends MessagesControllerComponents { getRegistration } - def authAndGetOptionalData(period: Period): ActionBuilder[OptionalDataRequest, AnyContent] = { + def authAndGetOptionalData(): ActionBuilder[OptionalDataRequest, AnyContent] = { authAndGetRegistration andThen - getData(period) + getData() } - def authAndRequireData(period: Period): ActionBuilder[DataRequest, AnyContent] = { - authAndGetOptionalData(period) andThen + def authAndRequireData(): ActionBuilder[DataRequest, AnyContent] = { + authAndGetOptionalData() andThen requireData } diff --git a/app/controllers/actions/DataRetrievalAction.scala b/app/controllers/actions/DataRetrievalAction.scala index 04084bab..39b73ca0 100644 --- a/app/controllers/actions/DataRetrievalAction.scala +++ b/app/controllers/actions/DataRetrievalAction.scala @@ -17,26 +17,25 @@ package controllers.actions import models.requests.{OptionalDataRequest, RegistrationRequest} -import models.Period import play.api.mvc.ActionTransformer import repositories.SessionRepository import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} -class DataRetrievalAction(period: Period, repository: SessionRepository) +class DataRetrievalAction(sessionRepository: SessionRepository) (implicit val executionContext: ExecutionContext) extends ActionTransformer[RegistrationRequest, OptionalDataRequest] { override protected def transform[A](request: RegistrationRequest[A]): Future[OptionalDataRequest[A]] = - repository.get(request.userId, period).map { x => - OptionalDataRequest(request.request, request.credentials, request.vrn, request.iossNumber, request.registrationWrapper, x) + sessionRepository.get(request.userId).map { maybeUserAnswers => + OptionalDataRequest(request.request, request.credentials, request.vrn, request.iossNumber, request.registrationWrapper, maybeUserAnswers) } } class DataRetrievalActionProvider @Inject()(repository: SessionRepository) (implicit ec: ExecutionContext) { - def apply(period: Period): DataRetrievalAction = - new DataRetrievalAction(period, repository) + def apply(): DataRetrievalAction = + new DataRetrievalAction(repository) } \ No newline at end of file diff --git a/app/controllers/actions/IdentifierAction.scala b/app/controllers/actions/IdentifierAction.scala index b5578d9a..313ce3eb 100644 --- a/app/controllers/actions/IdentifierAction.scala +++ b/app/controllers/actions/IdentifierAction.scala @@ -88,8 +88,13 @@ class IdentifierAction @Inject()( } } - private def getSuccessfulResponse[A](request: Request[A], credentials: Credentials, vrn: Vrn, iossNumber: String, groupId: String) - (implicit hc: HeaderCarrier): Future[Either[Result, IdentifierRequest[A]]] = { + private def getSuccessfulResponse[A]( + request: Request[A], + credentials: Credentials, + vrn: Vrn, + iossNumber: String, + groupId: String + ): Future[Either[Result, IdentifierRequest[A]]] = { val identifierRequest = IdentifierRequest(request, credentials, vrn, iossNumber) Right(identifierRequest).toFuture } @@ -101,7 +106,7 @@ class IdentifierAction @Inject()( iossNumber: String, groupId: String, confidence: ConfidenceLevel - )(implicit hc: HeaderCarrier): Future[Either[Result, IdentifierRequest[A]]] = { + ): Future[Either[Result, IdentifierRequest[A]]] = { if (confidence >= ConfidenceLevel.L200) { getSuccessfulResponse(request, credentials, vrn, iossNumber, groupId) } else { diff --git a/app/controllers/auth/AuthController.scala b/app/controllers/auth/AuthController.scala index 2f118c71..ef0eee6e 100644 --- a/app/controllers/auth/AuthController.scala +++ b/app/controllers/auth/AuthController.scala @@ -23,13 +23,12 @@ import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import javax.inject.Inject -import scala.concurrent.ExecutionContext class AuthController @Inject()( cc: AuthenticatedControllerComponents, config: FrontendAppConfig, - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + ) extends FrontendBaseController with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc diff --git a/app/forms/SoldToCountryListFormProvider.scala b/app/forms/SoldToCountryListFormProvider.scala new file mode 100644 index 00000000..09e129af --- /dev/null +++ b/app/forms/SoldToCountryListFormProvider.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2023 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 + +import javax.inject.Inject + +import forms.mappings.Mappings +import play.api.data.Form + +class SoldToCountryListFormProvider @Inject() extends Mappings { + + def apply(): Form[Boolean] = + Form( + "value" -> boolean("soldToCountryList.error.required") + ) +} diff --git a/app/models/Country.scala b/app/models/Country.scala index d6fa858a..158cbd7b 100644 --- a/app/models/Country.scala +++ b/app/models/Country.scala @@ -17,6 +17,8 @@ package models import play.api.libs.json.{Json, OFormat} +import uk.gov.hmrc.govukfrontend.views.viewmodels.select.SelectItem +import viewmodels.govuk.select._ case class Country(code: String, name: String) @@ -260,5 +262,17 @@ object Country { euCountries.take(positionOfNI) ++ Seq(northernIreland) ++ euCountries.drop(positionOfNI) } + val euCountrySelectItems: Seq[SelectItem] = + selectItems(euCountries) + + def selectItems(countries: Seq[Country]): Seq[SelectItem] = + SelectItem(value = None, text = "Select a country") +: + countries.map { + country => + SelectItemViewModel( + value = country.code, + text = country.name + ) + } } diff --git a/app/models/UserAnswers.scala b/app/models/UserAnswers.scala index e2041581..7f0ced92 100644 --- a/app/models/UserAnswers.scala +++ b/app/models/UserAnswers.scala @@ -17,7 +17,7 @@ package models import play.api.libs.json._ -import queries.{Gettable, Settable} +import queries.{Derivable, Gettable, Settable} import uk.gov.hmrc.mongo.play.json.formats.MongoJavatimeFormats import java.time.Instant @@ -33,6 +33,13 @@ final case class UserAnswers( def get[A](page: Gettable[A])(implicit rds: Reads[A]): Option[A] = Reads.optionNoError(Reads.at(page.path)).reads(data).getOrElse(None) + def get[A, B](derivable: Derivable[A, B])(implicit rds: Reads[A]): Option[B] = { + Reads.optionNoError(Reads.at(derivable.path)) + .reads(data) + .getOrElse(None) + .map(derivable.derive) + } + def set[A](page: Settable[A], value: A)(implicit writes: Writes[A]): Try[UserAnswers] = { val updatedData = data.setObject(page.path, Json.toJson(value)) match { diff --git a/app/pages/NoOtherPeriodsAvailablePage.scala b/app/pages/NoOtherPeriodsAvailablePage.scala new file mode 100644 index 00000000..79aa7605 --- /dev/null +++ b/app/pages/NoOtherPeriodsAvailablePage.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2023 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 + +import controllers.routes +import play.api.mvc.Call + +case object NoOtherPeriodsAvailablePage extends Page { + override def route(waypoints: Waypoints): Call = + routes.NoOtherPeriodsAvailableController.onPageLoad(waypoints) +} diff --git a/app/pages/SalesToCountryPage.scala b/app/pages/SalesToCountryPage.scala index 882b1e52..763ee07b 100644 --- a/app/pages/SalesToCountryPage.scala +++ b/app/pages/SalesToCountryPage.scala @@ -17,18 +17,18 @@ package pages import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import play.api.libs.json.JsPath import play.api.mvc.Call -case class SalesToCountryPage(period: Period, index: Index) extends QuestionPage[Int] { +case class SalesToCountryPage(index: Index) extends QuestionPage[Int] { - override def path: JsPath = JsPath \ toString + override def path: JsPath = JsPath \ PageConstants.sales \ index.position \ toString override def toString: String = "salesToCountry" - override def route(waypoints: Waypoints): Call = routes.SalesToCountryController.onPageLoad(waypoints, period, index) + override def route(waypoints: Waypoints): Call = routes.SalesToCountryController.onPageLoad(waypoints, index) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - VatOnSalesPage(period, index) + VatOnSalesPage(index) } diff --git a/app/pages/SoldGoodsPage.scala b/app/pages/SoldGoodsPage.scala index f5320589..2bb78dd2 100644 --- a/app/pages/SoldGoodsPage.scala +++ b/app/pages/SoldGoodsPage.scala @@ -17,18 +17,18 @@ package pages import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import play.api.libs.json.JsPath import play.api.mvc.Call -case class SoldGoodsPage(period: Period) extends QuestionPage[Boolean] { +case object SoldGoodsPage extends QuestionPage[Boolean] { override def path: JsPath = JsPath \ toString override def toString: String = "soldGoods" - override def route(waypoints: Waypoints): Call = routes.SoldGoodsController.onPageLoad(waypoints, period) + override def route(waypoints: Waypoints): Call = routes.SoldGoodsController.onPageLoad(waypoints) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - SoldToCountryPage(period, Index(0)) // TODO should it always be Index(0)? + SoldToCountryPage(Index(0)) // TODO should it always be Index(0)? } diff --git a/app/pages/SoldToCountryListPage.scala b/app/pages/SoldToCountryListPage.scala new file mode 100644 index 00000000..fb49e7e9 --- /dev/null +++ b/app/pages/SoldToCountryListPage.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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 + +import controllers.routes +import models.{Country, Index, UserAnswers} +import play.api.libs.json.{JsObject, JsPath} +import play.api.mvc.Call +import queries.{Derivable, DeriveNumberOfSales} + +object SoldToCountryListPage { + + val normalModeUrlFragment: String = "add-sales-country-list" + val checkModeUrlFragment: String = "change-add-sales-country-list" +} + +final case class SoldToCountryListPage(override val index: Option[Index] = None) extends AddItemPage(index) with QuestionPage[Boolean] { + + override def isTheSamePage(other: Page): Boolean = other match { + case _: SoldToCountryListPage => true + case _ => false + } + + override val normalModeUrlFragment: String = SoldToCountryListPage.normalModeUrlFragment + override val checkModeUrlFragment: String = SoldToCountryListPage.checkModeUrlFragment + + override def path: JsPath = JsPath \ toString + + override def toString: String = "soldToCountryList" + + override def route(waypoints: Waypoints): Call = + routes.SoldToCountryListController.onPageLoad(waypoints) + + override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = + answers.get(this).map { + case true => + index + .map { i => + if (i.position +1 < Country.euCountriesWithNI.size) { + SoldToCountryPage(Index(i.position + 1)) + } else { + CheckYourAnswersPage + } + } + .getOrElse { + answers + .get(deriveNumberOfItems) + .map(n => SoldToCountryPage(Index(n))) + .orRecover + } + case false => + CheckYourAnswersPage + }.orRecover + + + override def deriveNumberOfItems: Derivable[Seq[JsObject], Int] = DeriveNumberOfSales +} diff --git a/app/pages/SoldToCountryPage.scala b/app/pages/SoldToCountryPage.scala index 8ca29924..a9129942 100644 --- a/app/pages/SoldToCountryPage.scala +++ b/app/pages/SoldToCountryPage.scala @@ -17,18 +17,18 @@ package pages import controllers.routes -import models.{Country, Index, Period, UserAnswers} +import models.{Country, Index, UserAnswers} import play.api.libs.json.JsPath import play.api.mvc.Call -case class SoldToCountryPage(period: Period, index: Index) extends QuestionPage[Country] { +case class SoldToCountryPage(index: Index) extends QuestionPage[Country] { - override def path: JsPath = JsPath \ toString + override def path: JsPath = JsPath \ PageConstants.sales \ index.position \ toString - override def toString: String = "soldToCountry" + override def toString: String = "country" - override def route(waypoints: Waypoints): Call = routes.SoldToCountryController.onPageLoad(waypoints, period, index) + override def route(waypoints: Waypoints): Call = routes.SoldToCountryController.onPageLoad(waypoints, index) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - VatRatesFromCountryPage(period, index) + VatRatesFromCountryPage(index) } diff --git a/app/pages/StartReturnPage.scala b/app/pages/StartReturnPage.scala index b3a71b6d..715bed49 100644 --- a/app/pages/StartReturnPage.scala +++ b/app/pages/StartReturnPage.scala @@ -30,5 +30,10 @@ case class StartReturnPage(period: Period) extends QuestionPage[Boolean] { override def route(waypoints: Waypoints): Call = routes.StartReturnController.onPageLoad(waypoints, period) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - SoldGoodsPage(period) + answers.get(this).map { + case true => + SoldGoodsPage + case false => + NoOtherPeriodsAvailablePage + }.orRecover } diff --git a/app/pages/VatOnSalesPage.scala b/app/pages/VatOnSalesPage.scala index af04c1ab..d2bba9dc 100644 --- a/app/pages/VatOnSalesPage.scala +++ b/app/pages/VatOnSalesPage.scala @@ -17,18 +17,18 @@ package pages import controllers.routes -import models.{Index, Period, UserAnswers, VatOnSales} +import models.{Index, UserAnswers, VatOnSales} import play.api.libs.json.JsPath import play.api.mvc.Call -case class VatOnSalesPage(period: Period, index: Index) extends QuestionPage[VatOnSales] { +case class VatOnSalesPage(index: Index) extends QuestionPage[VatOnSales] { - override def path: JsPath = JsPath \ toString + override def path: JsPath = JsPath \ PageConstants.sales \ index.position \ toString override def toString: String = "vatOnSales" - override def route(waypoints: Waypoints): Call = routes.VatOnSalesController.onPageLoad(waypoints, period, index) + override def route(waypoints: Waypoints): Call = routes.VatOnSalesController.onPageLoad(waypoints, index) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - CheckYourAnswersPage + SoldToCountryListPage(Some(index)) } diff --git a/app/pages/VatRatesFromCountryPage.scala b/app/pages/VatRatesFromCountryPage.scala index f40d4717..3b2561d0 100644 --- a/app/pages/VatRatesFromCountryPage.scala +++ b/app/pages/VatRatesFromCountryPage.scala @@ -17,18 +17,18 @@ package pages import controllers.routes -import models.{Index, Period, UserAnswers, VatRatesFromCountry} +import models.{Index, UserAnswers, VatRatesFromCountry} import play.api.libs.json.JsPath import play.api.mvc.Call -case class VatRatesFromCountryPage(period: Period, index: Index) extends QuestionPage[Set[VatRatesFromCountry]] { +case class VatRatesFromCountryPage(index: Index) extends QuestionPage[Set[VatRatesFromCountry]] { - override def path: JsPath = JsPath \ toString + override def path: JsPath = JsPath \ PageConstants.sales \ index.position \ toString override def toString: String = "vatRatesFromCountry" - override def route(waypoints: Waypoints): Call = routes.VatRatesFromCountryController.onPageLoad(waypoints, period, index) + override def route(waypoints: Waypoints): Call = routes.VatRatesFromCountryController.onPageLoad(waypoints, index) override protected def nextPageNormalMode(waypoints: Waypoints, answers: UserAnswers): Page = - SalesToCountryPage(period, index) + SalesToCountryPage(index) } diff --git a/app/pages/Waypoint.scala b/app/pages/Waypoint.scala index 203ef966..a1112025 100644 --- a/app/pages/Waypoint.scala +++ b/app/pages/Waypoint.scala @@ -16,7 +16,7 @@ package pages -import models.Mode +import models.{CheckMode, Mode, NormalMode} case class Waypoint( page: WaypointPage, @@ -28,6 +28,8 @@ object Waypoint { private val fragments: Map[String, Waypoint] = Map( + SoldToCountryListPage.normalModeUrlFragment -> SoldToCountryListPage().waypoint(NormalMode), + SoldToCountryListPage.checkModeUrlFragment -> SoldToCountryListPage().waypoint(CheckMode), CheckYourAnswersPage.urlFragment -> CheckYourAnswersPage.waypoint ) diff --git a/app/pages/YourAccountPage.scala b/app/pages/YourAccountPage.scala new file mode 100644 index 00000000..50838b8b --- /dev/null +++ b/app/pages/YourAccountPage.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2023 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 + +import controllers.routes +import play.api.mvc.Call + +object YourAccountPage extends Page { + override def route(waypoints: Waypoints): Call = + routes.YourAccountController.onPageLoad(waypoints) +} diff --git a/app/pages/package.scala b/app/pages/package.scala new file mode 100644 index 00000000..fcccd40b --- /dev/null +++ b/app/pages/package.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2023 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 object pages { + + implicit class RecoveryOps(val a: Option[Page]) { + + def orRecover: Page = + a.getOrElse(JourneyRecoveryPage) + } +} diff --git a/app/queries/DeriveNumberOfSales.scala b/app/queries/DeriveNumberOfSales.scala new file mode 100644 index 00000000..40c41cc1 --- /dev/null +++ b/app/queries/DeriveNumberOfSales.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2023 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 queries + +import pages.PageConstants +import play.api.libs.json.{JsObject, JsPath} + +case object DeriveNumberOfSales extends Derivable[Seq[JsObject], Int] { + + override val derive: Seq[JsObject] => Int = _.size + + override def path: JsPath = JsPath \ PageConstants.sales +} diff --git a/app/repositories/SessionRepository.scala b/app/repositories/SessionRepository.scala index aca5904d..e8b2a8e6 100644 --- a/app/repositories/SessionRepository.scala +++ b/app/repositories/SessionRepository.scala @@ -17,12 +17,11 @@ package repositories import config.FrontendAppConfig -import models.{Period, UserAnswers} +import models.UserAnswers import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.model._ import play.api.libs.json.Format import uk.gov.hmrc.mongo.MongoComponent -import uk.gov.hmrc.mongo.play.json.Codecs.JsonOps import uk.gov.hmrc.mongo.play.json.PlayMongoRepository import uk.gov.hmrc.mongo.play.json.formats.MongoJavatimeFormats @@ -55,13 +54,6 @@ class SessionRepository @Inject()( private def byId(id: String): Bson = Filters.equal("_id", id) - private def byUserIdAndPeriod(userId: String, period: Period): Bson = { - Filters.and( - Filters.equal("_id", userId), - Filters.equal("period", period.toBson(legacyNumbers = false)) - ) - } - def keepAlive(id: String): Future[Boolean] = collection .updateOne( @@ -79,14 +71,6 @@ class SessionRepository @Inject()( .headOption() } - def get(userId: String, period: Period): Future[Option[UserAnswers]] = - keepAlive(userId).flatMap { - _ => - collection - .find(byUserIdAndPeriod(userId, period)) - .headOption() - } - def set(answers: UserAnswers): Future[Boolean] = { val updatedAnswers = answers copy (lastUpdated = Instant.now(clock)) diff --git a/app/utils/ItemsHelper.scala b/app/utils/ItemsHelper.scala new file mode 100644 index 00000000..3e19ce4d --- /dev/null +++ b/app/utils/ItemsHelper.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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 utils + +import controllers.actions.AuthenticatedControllerComponents +import models.requests.DataRequest +import pages.{JourneyRecoveryPage, QuestionPage, Waypoints} +import play.api.libs.json.JsObject +import play.api.mvc.{AnyContent, Result} +import play.api.mvc.Results.Redirect +import queries.{Derivable, Settable} +import utils.FutureSyntax.FutureOps + +import scala.concurrent.{ExecutionContext, Future} + +object ItemsHelper { + + def getDerivedItems(waypoints: Waypoints, derivable: Derivable[Seq[JsObject], Int])(block: Int => Future[Result]) + (implicit request: DataRequest[AnyContent]): Future[Result] = { + request.userAnswers.get(derivable).map { + number => + block(number) + }.getOrElse(Redirect(JourneyRecoveryPage.route(waypoints).url).toFuture) + } + + def determineRemoveAllItemsAndRedirect[A]( + waypoints: Waypoints, + doRemoveItems: Boolean, + cc: AuthenticatedControllerComponents, + query: Settable[A], + hasItems: QuestionPage[Boolean], + deleteAllItemsPage: QuestionPage[Boolean] + )(implicit ec: ExecutionContext, request: DataRequest[AnyContent]): Future[Result] = { + val removeItems = if (doRemoveItems) { + request.userAnswers.remove(query) + } else { + request.userAnswers.set(hasItems, true) + } + for { + updatedAnswers <- Future.fromTry(removeItems) + calculatedAnswers <- Future.fromTry(updatedAnswers.set(deleteAllItemsPage, doRemoveItems)) + _ <- cc.sessionRepository.set(calculatedAnswers) + } yield Redirect(deleteAllItemsPage.navigate(waypoints, request.userAnswers, calculatedAnswers).route) + } +} diff --git a/app/viewmodels/ListItemWrapper.scala b/app/viewmodels/ListItemWrapper.scala new file mode 100644 index 00000000..2ab0c36a --- /dev/null +++ b/app/viewmodels/ListItemWrapper.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2023 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 + +import uk.gov.hmrc.hmrcfrontend.views.viewmodels.addtoalist.ListItem + +case class ListItemWrapper(listItem: ListItem, removeButtonEnabled: Boolean) diff --git a/app/viewmodels/checkAnswers/SalesToCountrySummary.scala b/app/viewmodels/checkAnswers/SalesToCountrySummary.scala index c6bdf395..10ac2cfd 100644 --- a/app/viewmodels/checkAnswers/SalesToCountrySummary.scala +++ b/app/viewmodels/checkAnswers/SalesToCountrySummary.scala @@ -17,7 +17,7 @@ package viewmodels.checkAnswers import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import pages.{SalesToCountryPage, Waypoints} import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow @@ -26,15 +26,15 @@ import viewmodels.implicits._ object SalesToCountrySummary { - def row(answers: UserAnswers, waypoints: Waypoints, period: Period, index: Index)(implicit messages: Messages): Option[SummaryListRow] = - answers.get(SalesToCountryPage(period, index)).map { + def row(answers: UserAnswers, waypoints: Waypoints, index: Index)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(SalesToCountryPage(index)).map { answer => SummaryListRowViewModel( key = "salesToCountry.checkYourAnswersLabel", value = ValueViewModel(answer.toString), actions = Seq( - ActionItemViewModel("site.change", routes.SalesToCountryController.onPageLoad(waypoints, period, index).url) + ActionItemViewModel("site.change", routes.SalesToCountryController.onPageLoad(waypoints, index).url) .withVisuallyHiddenText(messages("salesToCountry.change.hidden")) ) ) diff --git a/app/viewmodels/checkAnswers/SoldGoodsSummary.scala b/app/viewmodels/checkAnswers/SoldGoodsSummary.scala index 65e9872a..70a45116 100644 --- a/app/viewmodels/checkAnswers/SoldGoodsSummary.scala +++ b/app/viewmodels/checkAnswers/SoldGoodsSummary.scala @@ -17,7 +17,7 @@ package viewmodels.checkAnswers import controllers.routes -import models.{Period, UserAnswers} +import models.UserAnswers import pages.{SoldGoodsPage, Waypoints} import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow @@ -26,8 +26,8 @@ import viewmodels.implicits._ object SoldGoodsSummary { - def row(answers: UserAnswers, waypoints: Waypoints, period: Period)(implicit messages: Messages): Option[SummaryListRow] = - answers.get(SoldGoodsPage(period)).map { + def row(answers: UserAnswers, waypoints: Waypoints)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(SoldGoodsPage).map { answer => val value = if (answer) "site.yes" else "site.no" @@ -36,7 +36,7 @@ object SoldGoodsSummary { key = "soldGoods.checkYourAnswersLabel", value = ValueViewModel(value), actions = Seq( - ActionItemViewModel("site.change", routes.SoldGoodsController.onPageLoad(waypoints, period).url) + ActionItemViewModel("site.change", routes.SoldGoodsController.onPageLoad(waypoints).url) .withVisuallyHiddenText(messages("soldGoods.change.hidden")) ) ) diff --git a/app/viewmodels/checkAnswers/SoldToCountryListSummary.scala b/app/viewmodels/checkAnswers/SoldToCountryListSummary.scala new file mode 100644 index 00000000..5e540dcc --- /dev/null +++ b/app/viewmodels/checkAnswers/SoldToCountryListSummary.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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 + +import models.UserAnswers +import pages.{AddItemPage, JourneyRecoveryPage, Waypoints} +import play.twirl.api.HtmlFormat +import queries.AllSalesQuery +import uk.gov.hmrc.hmrcfrontend.views.viewmodels.addtoalist.ListItem + +object SoldToCountryListSummary { + + def addToListRows(answers: UserAnswers, waypoints: Waypoints, sourcePage: AddItemPage): Seq[ListItem] = { + answers.get(AllSalesQuery).getOrElse(List.empty).zipWithIndex.map { + case (salesToCountryWithOptionalVat, countryIndex) => + + ListItem( + name = HtmlFormat.escape(salesToCountryWithOptionalVat.country.name).toString, + changeUrl = JourneyRecoveryPage.changeLink(waypoints, sourcePage).url, // TODO -> to Mini CYA when created + removeUrl = JourneyRecoveryPage.changeLink(waypoints, sourcePage).url // TODO -> to DeleteSoldToCountrySalesPage when created + ) + } + } +} diff --git a/app/viewmodels/checkAnswers/SoldToCountrySummary.scala b/app/viewmodels/checkAnswers/SoldToCountrySummary.scala index 3586c0a5..566756ce 100644 --- a/app/viewmodels/checkAnswers/SoldToCountrySummary.scala +++ b/app/viewmodels/checkAnswers/SoldToCountrySummary.scala @@ -17,7 +17,7 @@ package viewmodels.checkAnswers import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import pages.{SoldToCountryPage, Waypoints} import play.api.i18n.Messages import play.twirl.api.HtmlFormat @@ -27,15 +27,15 @@ import viewmodels.implicits._ object SoldToCountrySummary { - def row(answers: UserAnswers, waypoints: Waypoints, period: Period, index: Index)(implicit messages: Messages): Option[SummaryListRow] = - answers.get(SoldToCountryPage(period, index)).map { + def row(answers: UserAnswers, waypoints: Waypoints, index: Index)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(SoldToCountryPage(index)).map { answer => SummaryListRowViewModel( key = "soldToCountry.checkYourAnswersLabel", value = ValueViewModel(HtmlFormat.escape(answer.name).toString), actions = Seq( - ActionItemViewModel("site.change", routes.SoldToCountryController.onPageLoad(waypoints, period, index).url) + ActionItemViewModel("site.change", routes.SoldToCountryController.onPageLoad(waypoints, index).url) .withVisuallyHiddenText(messages("soldToCountry.change.hidden")) ) ) diff --git a/app/viewmodels/checkAnswers/VatOnSalesSummary.scala b/app/viewmodels/checkAnswers/VatOnSalesSummary.scala index ce96884b..1378e8a6 100644 --- a/app/viewmodels/checkAnswers/VatOnSalesSummary.scala +++ b/app/viewmodels/checkAnswers/VatOnSalesSummary.scala @@ -17,7 +17,7 @@ package viewmodels.checkAnswers import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import pages.{VatOnSalesPage, Waypoints} import play.api.i18n.Messages import play.twirl.api.HtmlFormat @@ -28,8 +28,8 @@ import viewmodels.implicits._ object VatOnSalesSummary { - def row(answers: UserAnswers, waypoints: Waypoints, period: Period, index: Index)(implicit messages: Messages): Option[SummaryListRow] = - answers.get(VatOnSalesPage(period, index)).map { + def row(answers: UserAnswers, waypoints: Waypoints, index: Index)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(VatOnSalesPage(index)).map { answer => val value = ValueViewModel( @@ -42,7 +42,7 @@ object VatOnSalesSummary { key = "vatOnSales.checkYourAnswersLabel", value = value, actions = Seq( - ActionItemViewModel("site.change", routes.VatOnSalesController.onPageLoad(waypoints, period, index).url) + ActionItemViewModel("site.change", routes.VatOnSalesController.onPageLoad(waypoints, index).url) .withVisuallyHiddenText(messages("vatOnSales.change.hidden")) ) ) diff --git a/app/viewmodels/checkAnswers/VatRatesFromCountrySummary.scala b/app/viewmodels/checkAnswers/VatRatesFromCountrySummary.scala index 29f195f6..11da626d 100644 --- a/app/viewmodels/checkAnswers/VatRatesFromCountrySummary.scala +++ b/app/viewmodels/checkAnswers/VatRatesFromCountrySummary.scala @@ -17,7 +17,7 @@ package viewmodels.checkAnswers import controllers.routes -import models.{Index, Period, UserAnswers} +import models.{Index, UserAnswers} import pages.{VatRatesFromCountryPage, Waypoints} import play.api.i18n.Messages import play.twirl.api.HtmlFormat @@ -28,8 +28,8 @@ import viewmodels.implicits._ object VatRatesFromCountrySummary { - def row(answers: UserAnswers, waypoints: Waypoints, period: Period, index: Index)(implicit messages: Messages): Option[SummaryListRow] = - answers.get(VatRatesFromCountryPage(period, index)).map { + def row(answers: UserAnswers, waypoints: Waypoints, index: Index)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(VatRatesFromCountryPage(index)).map { answers => val value = ValueViewModel( @@ -45,7 +45,7 @@ object VatRatesFromCountrySummary { key = "vatRatesFromCountry.checkYourAnswersLabel", value = value, actions = Seq( - ActionItemViewModel("site.change", routes.VatRatesFromCountryController.onPageLoad(waypoints, period, index).url) + ActionItemViewModel("site.change", routes.VatRatesFromCountryController.onPageLoad(waypoints, index).url) .withVisuallyHiddenText(messages("vatRatesFromCountry.change.hidden")) ) ) diff --git a/app/viewmodels/govuk/CheckboxFluency.scala b/app/viewmodels/govuk/CheckboxFluency.scala index ce8d5b4d..be1ffa8d 100644 --- a/app/viewmodels/govuk/CheckboxFluency.scala +++ b/app/viewmodels/govuk/CheckboxFluency.scala @@ -66,6 +66,9 @@ trait CheckboxFluency { def describedBy(value: String): Checkboxes = checkboxes.copy(describedBy = Some(value)) + + def withHint(hint: Hint): Checkboxes = + checkboxes copy (hint = Some(hint)) } object CheckboxItemViewModel { diff --git a/app/viewmodels/govuk/FieldsetFluency.scala b/app/viewmodels/govuk/FieldsetFluency.scala index d04273af..73205553 100644 --- a/app/viewmodels/govuk/FieldsetFluency.scala +++ b/app/viewmodels/govuk/FieldsetFluency.scala @@ -57,12 +57,15 @@ trait FieldsetFluency { implicit class FluentLegend(legend: Legend) { - def asPageHeading(size: LegendSize = LegendSize.ExtraLarge): Legend = + def asPageHeading(size: LegendSize = LegendSize.Large): Legend = legend .copy(isPageHeading = true) .withCssClass(size.toString) def withCssClass(newClass: String): Legend = legend.copy(classes = s"${legend.classes} $newClass") + + def withSize(size: LegendSize): Legend = + legend.withCssClass(size.toString) } } diff --git a/app/viewmodels/govuk/SelectFluency.scala b/app/viewmodels/govuk/SelectFluency.scala new file mode 100644 index 00000000..43650171 --- /dev/null +++ b/app/viewmodels/govuk/SelectFluency.scala @@ -0,0 +1,81 @@ +/* + * Copyright 2023 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.govuk + +import play.api.data.Field +import play.api.i18n.Messages +import uk.gov.hmrc.govukfrontend.views.viewmodels.hint.Hint +import uk.gov.hmrc.govukfrontend.views.viewmodels.label.Label +import uk.gov.hmrc.govukfrontend.views.viewmodels.select.{Select, SelectItem} +import viewmodels.ErrorMessageAwareness + +object select extends SelectFluency + +trait SelectFluency { + + object SelectViewModel extends ErrorMessageAwareness { + + def apply( + field: Field, + items: Seq[SelectItem], + label: Label + )(implicit messages: Messages): Select = + Select( + id = field.id, + name = field.name, + items = items map (item => item copy (selected = field.value.isDefined && field.value == item.value)), + label = label, + errorMessage = errorMessage(field) + ) + } + + implicit class FluentSelect(select: Select) { + + def withHint(hint: Hint): Select = + select copy (hint = Some(hint)) + + def describedBy(value: String): Select = + select copy (describedBy = Some(value)) + + def withFormGroupClasses(classes: String): Select = + select copy (formGroupClasses = classes) + + def withCssClass(newClass: String): Select = + select copy (classes = s"${select.classes} $newClass") + + def withAttribute(attribute: (String, String)): Select = + select copy (attributes = select.attributes + attribute) + } + + object SelectItemViewModel { + + def apply( + value: String, + text: String + ): SelectItem = + SelectItem(value = Some(value), text = text) + } + + implicit class FluentSelectItem(item: SelectItem) { + + def disabled: SelectItem = + item copy (disabled = true) + + def withAttribute(attribute: (String, String)): SelectItem = + item copy (attributes = item.attributes + attribute) + } +} diff --git a/app/viewmodels/govuk/package.scala b/app/viewmodels/govuk/package.scala index 6e87720b..afc238fa 100644 --- a/app/viewmodels/govuk/package.scala +++ b/app/viewmodels/govuk/package.scala @@ -30,6 +30,7 @@ package object govuk { with InputFluency with LabelFluency with RadiosFluency + with SelectFluency with SummaryListFluency with TagFluency } diff --git a/app/views/NoOtherPeriodsAvailableView.scala.html b/app/views/NoOtherPeriodsAvailableView.scala.html new file mode 100644 index 00000000..71ef8f38 --- /dev/null +++ b/app/views/NoOtherPeriodsAvailableView.scala.html @@ -0,0 +1,33 @@ +@* + * Copyright 2023 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 pages.YourAccountPage + +@this( + layout: templates.Layout, + govukButton: GovukButton +) + +@(waypoints: Waypoints)(implicit request: Request[_], messages: Messages) + + @layout(pageTitle = titleNoForm(messages("noOtherPeriodsAvailable.title"))) { + +
@messages("noOtherPeriodsAvailable.p1")
+ +@Html(messages("noOtherPeriodsAvailable.backToYourAccount", YourAccountPage.route(waypoints).url))
+ } diff --git a/app/views/SalesToCountryView.scala.html b/app/views/SalesToCountryView.scala.html index eef96673..808b9059 100644 --- a/app/views/SalesToCountryView.scala.html +++ b/app/views/SalesToCountryView.scala.html @@ -28,7 +28,7 @@ @layout(pageTitle = title(form, messages("salesToCountry.title"))) { - @formHelper(action = routes.SalesToCountryController.onSubmit(waypoints, period, index), Symbol("autoComplete") -> "off") { + @formHelper(action = routes.SalesToCountryController.onSubmit(waypoints, index), Symbol("autoComplete") -> "off") { @if(form.errors.nonEmpty) { @govukErrorSummary(ErrorSummaryViewModel(form)) diff --git a/app/views/SoldGoodsView.scala.html b/app/views/SoldGoodsView.scala.html index 763b27de..343c96db 100644 --- a/app/views/SoldGoodsView.scala.html +++ b/app/views/SoldGoodsView.scala.html @@ -14,30 +14,35 @@ * limitations under the License. *@ +@import viewmodels.LegendSize + @this( - layout: templates.Layout, - formHelper: FormWithCSRF, - govukErrorSummary: GovukErrorSummary, - govukRadios: GovukRadios, - button: ButtonGroup + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukRadios: GovukRadios, + button: ButtonGroup ) @(form: Form[_], waypoints: Waypoints, period: Period)(implicit request: Request[_], messages: Messages) -@layout(pageTitle = title(form, messages("soldGoods.title", period.displayText))) { + @layout(pageTitle = title(form, messages("soldGoods.title", period.displayText))) { - @formHelper(action = routes.SoldGoodsController.onSubmit(waypoints, period), Symbol("autoComplete") -> "off") { + @formHelper(action = routes.SoldGoodsController.onSubmit(waypoints), Symbol("autoComplete") -> "off") { - @if(form.errors.nonEmpty) { - @govukErrorSummary(ErrorSummaryViewModel(form)) - } + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } - @govukRadios( - RadiosViewModel.yesNo( - field = form("value"), - legend = LegendViewModel(messages("soldGoods.heading", period.displayText)).asPageHeading() + @govukRadios( + RadiosViewModel.yesNo( + field = form("value"), + legend = HmrcPageHeadingLegend( + content = messages("soldGoods.heading", period.displayText), + caption = period.displayText + ).asPageHeading(size = LegendSize.Large) + ) ) - ) @button( messages("site.continue") diff --git a/app/views/SoldToCountryListView.scala.html b/app/views/SoldToCountryListView.scala.html new file mode 100644 index 00000000..4052240d --- /dev/null +++ b/app/views/SoldToCountryListView.scala.html @@ -0,0 +1,75 @@ +@* + * Copyright 2023 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.hmrcfrontend.views.viewmodels.addtoalist.{ListItem, Long} +@import viewmodels.LegendSize + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukRadios: GovukRadios, + addToList: components.addToList, + button: ButtonGroup +) + +@(form: Form[_], waypoints: Waypoints, period: Period, salesList: Seq[ListItem], canAddSales: Boolean)(implicit request: Request[_], messages: Messages) + +@titleText = @{ + salesList.size match { + case 1 => messages("soldToCountryList.title.singular") + case i => messages("soldToCountryList.title.plural", i) + } +} + +@headingText = @{ + salesList.size match { + case 1 => messages("soldToCountryList.heading.singular") + case i => messages("soldToCountryList.heading.plural", i) + } +} + +@layout(pageTitle = title(form, titleText)) { + + @formHelper(action = routes.SoldToCountryListController.onSubmit(waypoints), Symbol("autoComplete") -> "off") { + + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + +@messages("soldToCountryList.maximumReached")
+ + } + + @button( + messages("site.continue") + ) + } +} diff --git a/app/views/SoldToCountryView.scala.html b/app/views/SoldToCountryView.scala.html index b45b103b..d843870e 100644 --- a/app/views/SoldToCountryView.scala.html +++ b/app/views/SoldToCountryView.scala.html @@ -14,32 +14,39 @@ * limitations under the License. *@ -@import viewmodels.InputWidth._ +@import viewmodels.InputWidth.Fixed20 +@import viewmodels.LabelSize +@import views.html.components.ButtonGroup @this( - layout: templates.Layout, - formHelper: FormWithCSRF, - govukErrorSummary: GovukErrorSummary, - govukInput: GovukInput, - button: ButtonGroup + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukSelect: GovukSelect, + button: ButtonGroup ) @(form: Form[_], waypoints: Waypoints, period: Period, index: Index)(implicit request: Request[_], messages: Messages) @layout(pageTitle = title(form, messages("soldToCountry.title"))) { - @formHelper(action = routes.SoldToCountryController.onSubmit(waypoints, period, index)) { + @formHelper(action = routes.SoldToCountryController.onSubmit(waypoints, index)) { @if(form.errors.nonEmpty) { @govukErrorSummary(ErrorSummaryViewModel(form)) } - @govukInput( - InputViewModel( + @govukSelect( + SelectViewModel( field = form("value"), - label = LabelViewModel(messages("soldToCountry.heading")).asPageHeading() + items = Country.euCountrySelectItems, + label = HmrcPageHeadingLabel( + content = messages("soldToCountry.heading"), + caption = period.displayText + ).asPageHeading(size = LabelSize.Large) ) - .withWidth(Full) + .withCssClass(Fixed20.toString) + .withCssClass("autocomplete") ) @button( diff --git a/app/views/VatOnSalesView.scala.html b/app/views/VatOnSalesView.scala.html index d8477d0b..b646f352 100644 --- a/app/views/VatOnSalesView.scala.html +++ b/app/views/VatOnSalesView.scala.html @@ -26,7 +26,7 @@ @layout(pageTitle = title(form, messages("vatOnSales.title"))) { - @formHelper(action = routes.VatOnSalesController.onSubmit(waypoints, period, index), Symbol("autoComplete") -> "off") { + @formHelper(action = routes.VatOnSalesController.onSubmit(waypoints, index), Symbol("autoComplete") -> "off") { @if(form.errors.nonEmpty) { @govukErrorSummary(ErrorSummaryViewModel(form, errorLinkOverrides = Map("value" -> "value_0"))) diff --git a/app/views/VatRatesFromCountryView.scala.html b/app/views/VatRatesFromCountryView.scala.html index 60a67da1..43b7acee 100644 --- a/app/views/VatRatesFromCountryView.scala.html +++ b/app/views/VatRatesFromCountryView.scala.html @@ -22,11 +22,11 @@ button: ButtonGroup ) -@(form: Form[_], waypoints: Waypoints, period: Period, index: Index)(implicit request: Request[_], messages: Messages) +@(form: Form[_], waypoints: Waypoints, period: Period, index: Index, country: Country)(implicit request: Request[_], messages: Messages) @layout(pageTitle = title(form, messages("vatRatesFromCountry.title"))) { - @formHelper(action = routes.VatRatesFromCountryController.onSubmit(waypoints, period, index), Symbol("autoComplete") -> "off") { + @formHelper(action = routes.VatRatesFromCountryController.onSubmit(waypoints, index), Symbol("autoComplete") -> "off") { @if(form.errors.nonEmpty) { @govukErrorSummary(ErrorSummaryViewModel(form, errorLinkOverrides = Map("value" -> "value_0"))) @@ -36,9 +36,12 @@ CheckboxesViewModel( form = form, name = "value", - legend = LegendViewModel(messages("vatRatesFromCountry.heading")).asPageHeading(), + legend = HmrcPageHeadingLegend( + content = messages("vatRatesFromCountry.heading"), + caption = messages("vatRatesFromCountry.caption", period.displayText, country.name) + ), items = VatRatesFromCountry.checkboxItems - ) + ).withHint(HintViewModel(messages("vatRatesFromCountry.hint"))) ) @button( diff --git a/app/views/components/addToList.scala.html b/app/views/components/addToList.scala.html new file mode 100644 index 00000000..bd54da1f --- /dev/null +++ b/app/views/components/addToList.scala.html @@ -0,0 +1,46 @@ +@* + * Copyright 2023 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.hmrcfrontend.views.viewmodels.addtoalist +@import uk.gov.hmrc.hmrcfrontend.views.viewmodels.addtoalist.{ItemSize, ListItem} + +@this() + +@(itemList: Seq[ListItem], itemSize: ItemSize = addtoalist.Short, changePrefix: String, removePrefix: String)(implicit messages: Messages) + +