diff --git a/app/controllers/CheckSalesController.scala b/app/controllers/CheckSalesController.scala index c318513c..093e17c9 100644 --- a/app/controllers/CheckSalesController.scala +++ b/app/controllers/CheckSalesController.scala @@ -19,13 +19,14 @@ package controllers import controllers.actions._ import forms.CheckSalesFormProvider import models.Index -import pages.{CheckSalesPage, Waypoints} +import pages.{CheckSalesPage, SalesToCountryPage, Waypoints} import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} -import queries.RemainingVatRatesFromCountryQuery +import queries.{RemainingVatRatesFromCountryQuery, VatRateWithOptionalSalesFromCountry} import services.VatRateService import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.CompletionChecks import utils.FutureSyntax.FutureOps import viewmodels.checkAnswers.CheckSalesSummary import views.html.CheckSalesView @@ -39,7 +40,8 @@ class CheckSalesController @Inject()( formProvider: CheckSalesFormProvider, vatRateService: VatRateService, view: CheckSalesView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with GetCountry with GetVatRates { + )(implicit ec: ExecutionContext) + extends FrontendBaseController with CompletionChecks with I18nSupport with GetCountry with GetVatRates { protected val controllerComponents: MessagesControllerComponents = cc @@ -61,35 +63,70 @@ class CheckSalesController @Inject()( case Some(value) => form.fill(value) } - Ok(view(preparedForm, waypoints, period, checkSalesSummary, countryIndex, country, canAddAnotherVatRate)).toFuture + withCompleteDataAsync[VatRateWithOptionalSalesFromCountry]( + countryIndex, + data = getIncompleteVatRateAndSales _, + onFailure = (incomplete: Seq[VatRateWithOptionalSalesFromCountry]) => { + Ok(view( + preparedForm, + waypoints, + period, + checkSalesSummary, + countryIndex, + country, + canAddAnotherVatRate, + incomplete)).toFuture + }) { + Ok(view(preparedForm, waypoints, period, checkSalesSummary, countryIndex, country, canAddAnotherVatRate)).toFuture + } } } } - def onSubmit(waypoints: Waypoints, countryIndex: Index): Action[AnyContent] = cc.authAndRequireData().async { + def onSubmit(waypoints: Waypoints, countryIndex: Index, incompletePromptShown: Boolean): Action[AnyContent] = cc.authAndRequireData().async { implicit request => getCountry(waypoints, countryIndex) { country => getAllVatRatesFromCountry(waypoints, countryIndex) { vatRates => val period = request.userAnswers.period + val vatRateIndex = Index(vatRates.vatRatesFromCountry.map(_.size).getOrElse(0) - 1) + val remainingVatRates = vatRateService.getRemainingVatRatesForCountry(period, country, vatRates) val canAddAnotherVatRate = remainingVatRates.nonEmpty val checkSalesSummary = CheckSalesSummary.rows(request.userAnswers, waypoints, countryIndex) - form.bindFromRequest().fold( - formWithErrors => - BadRequest(view(formWithErrors, waypoints, period, checkSalesSummary, countryIndex, country, canAddAnotherVatRate)).toFuture, - - value => - for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(CheckSalesPage(countryIndex), value)) - updatedAnswersWithRemainingVatRates <- Future.fromTry(updatedAnswers.set(RemainingVatRatesFromCountryQuery(countryIndex), remainingVatRates)) - _ <- cc.sessionRepository.set(updatedAnswersWithRemainingVatRates) - } yield Redirect(CheckSalesPage(countryIndex).navigate(waypoints, request.userAnswers, updatedAnswersWithRemainingVatRates).route) - ) + val salesToCountry = request.userAnswers.get(SalesToCountryPage(countryIndex, vatRateIndex)) + + withCompleteDataAsync[VatRateWithOptionalSalesFromCountry]( + countryIndex, + data = getIncompleteVatRateAndSales _, + onFailure = (_: Seq[VatRateWithOptionalSalesFromCountry]) => { + if(incompletePromptShown) { + salesToCountry match { + case Some(_) => + Redirect(routes.VatOnSalesController.onPageLoad(waypoints, countryIndex, vatRateIndex)).toFuture + case None => + Redirect(routes.SalesToCountryController.onPageLoad(waypoints, countryIndex, vatRateIndex)).toFuture + } + } else { + Redirect(routes.CheckSalesController.onPageLoad(waypoints, countryIndex)).toFuture + } + }) { + form.bindFromRequest().fold( + formWithErrors => + BadRequest(view(formWithErrors, waypoints, period, checkSalesSummary, countryIndex, country, canAddAnotherVatRate, Seq.empty)).toFuture, + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(CheckSalesPage(countryIndex), value)) + updatedAnswersWithRemainingVatRates <- Future.fromTry(updatedAnswers.set(RemainingVatRatesFromCountryQuery(countryIndex), remainingVatRates)) + _ <- cc.sessionRepository.set(updatedAnswersWithRemainingVatRates) + } yield Redirect(CheckSalesPage(countryIndex).navigate(waypoints, request.userAnswers, updatedAnswersWithRemainingVatRates).route) + ) + } } } } diff --git a/app/controllers/CheckYourAnswersController.scala b/app/controllers/CheckYourAnswersController.scala index 6ef4b3bb..b40dbe07 100644 --- a/app/controllers/CheckYourAnswersController.scala +++ b/app/controllers/CheckYourAnswersController.scala @@ -34,6 +34,7 @@ import uk.gov.hmrc.govukfrontend.views.Aliases.Card import uk.gov.hmrc.govukfrontend.views.viewmodels.content.HtmlContent import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.{CardTitle, SummaryList} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import utils.FutureSyntax.FutureOps import viewmodels.checkAnswers._ import viewmodels.checkAnswers.corrections.{CorrectPreviousReturnSummary, CorrectionNoPaymentDueSummary, CorrectionReturnPeriodSummary} import viewmodels.govuk.summarylist._ @@ -51,7 +52,8 @@ class CheckYourAnswersController @Inject()( auditService: AuditService, periodService: PeriodService, view: CheckYourAnswersView, - saveForLaterConnector: SaveForLaterConnector + saveForLaterConnector: SaveForLaterConnector, + redirectService: RedirectService )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with Logging { protected val controllerComponents: MessagesControllerComponents = cc @@ -59,7 +61,9 @@ class CheckYourAnswersController @Inject()( def onPageLoad(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData() { implicit request => - val errors: List[ValidationError] = Nil // TODO + val period = request.userAnswers.period + + val errors: List[ValidationError] = redirectService.validate(period) val businessSummaryList = getBusinessSummaryList(request, waypoints) @@ -75,8 +79,6 @@ class CheckYourAnswersController @Inject()( val maybeExclusion: Option[EtmpExclusion] = request.registrationWrapper.registration.exclusions.lastOption - val period = request.userAnswers.period - val nextPeriodString = periodService.getNextPeriod(period).displayYearMonth val nextPeriod: LocalDate = LocalDate.parse(nextPeriodString, DateTimeFormatter.ofPattern("yyyy-MM-dd")) @@ -102,22 +104,32 @@ class CheckYourAnswersController @Inject()( def onSubmit(waypoints: Waypoints, incompletePromptShown: Boolean): Action[AnyContent] = cc.authAndRequireData().async { implicit request => val userAnswers = request.userAnswers - coreVatReturnService.submitCoreVatReturn(userAnswers).flatMap { remainingTotalAmountVatDueGBP => - auditService.audit(ReturnsAuditModel.build(userAnswers, SubmissionResult.Success)) - userAnswers.set(TotalAmountVatDueGBPQuery, remainingTotalAmountVatDueGBP) match { - case Failure(exception) => - logger.error(s"Couldn't update users answers with remaining owed vat ${exception.getMessage}", exception) - Future.successful(Redirect(controllers.submissionResults.routes.ReturnSubmissionFailureController.onPageLoad().url)) - case Success(updatedAnswers) => - cc.sessionRepository.set(updatedAnswers).map(_ => - Redirect(controllers.submissionResults.routes.SuccessfullySubmittedController.onPageLoad().url) - ) - } - }.recoverWith { - case e: Exception => - logger.error(s"Error while submitting VAT return ${e.getMessage}", e) - auditService.audit(ReturnsAuditModel.build(userAnswers, SubmissionResult.Failure)) - saveUserAnswersOnCoreError(controllers.submissionResults.routes.ReturnSubmissionFailureController.onPageLoad) + + val preferredPeriod = userAnswers.period + + val redirectToFirstError = redirectService.getRedirect(waypoints, redirectService.validate(preferredPeriod)).headOption + + (redirectToFirstError, incompletePromptShown) match { + case (Some(redirect), true) => Redirect(redirect).toFuture + case (Some(_), false) => Redirect(routes.CheckYourAnswersController.onPageLoad(waypoints)).toFuture + case _ => + coreVatReturnService.submitCoreVatReturn(userAnswers).flatMap { remainingTotalAmountVatDueGBP => + auditService.audit(ReturnsAuditModel.build(userAnswers, SubmissionResult.Success)) + userAnswers.set(TotalAmountVatDueGBPQuery, remainingTotalAmountVatDueGBP) match { + case Failure(exception) => + logger.error(s"Couldn't update users answers with remaining owed vat ${exception.getMessage}", exception) + Future.successful(Redirect(controllers.submissionResults.routes.ReturnSubmissionFailureController.onPageLoad().url)) + case Success(updatedAnswers) => + cc.sessionRepository.set(updatedAnswers).map(_ => + Redirect(controllers.submissionResults.routes.SuccessfullySubmittedController.onPageLoad().url) + ) + } + }.recoverWith { + case e: Exception => + logger.error(s"Error while submitting VAT return ${e.getMessage}", e) + auditService.audit(ReturnsAuditModel.build(userAnswers, SubmissionResult.Failure)) + saveUserAnswersOnCoreError(controllers.submissionResults.routes.ReturnSubmissionFailureController.onPageLoad) + } } } diff --git a/app/controllers/SoldToCountryListController.scala b/app/controllers/SoldToCountryListController.scala index 0c818494..4cb20899 100644 --- a/app/controllers/SoldToCountryListController.scala +++ b/app/controllers/SoldToCountryListController.scala @@ -18,13 +18,14 @@ package controllers import controllers.actions._ import forms.SoldToCountryListFormProvider -import models.Country +import models.{Country, Index} 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.CompletionChecks import utils.FutureSyntax.FutureOps import utils.ItemsHelper.getDerivedItems import viewmodels.checkAnswers.SoldToCountryListSummary @@ -38,7 +39,7 @@ class SoldToCountryListController @Inject()( cc: AuthenticatedControllerComponents, formProvider: SoldToCountryListFormProvider, view: SoldToCountryListView - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + )(implicit ec: ExecutionContext) extends FrontendBaseController with CompletionChecks with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc @@ -56,16 +57,40 @@ class SoldToCountryListController @Inject()( val salesSummary = SoldToCountryListSummary .addToListRows(request.userAnswers, waypoints, SoldToCountryListPage()) - Ok(view(form, waypoints, period, salesSummary, canAddSales)).toFuture + withCompleteDataAsync[Country]( + data = getCountriesWithIncompleteSales _, + onFailure = (incomplete: Seq[Country]) => { + Ok(view(form, waypoints, period, salesSummary, canAddSales, incomplete)).toFuture + }) { + Ok(view(form, waypoints, period, salesSummary, canAddSales)).toFuture + } + } } - def onSubmit(waypoints: Waypoints): Action[AnyContent] = cc.authAndRequireData().async { + def onSubmit(waypoints: Waypoints, incompletePromptShown: Boolean): Action[AnyContent] = cc.authAndRequireData().async { implicit request => val period = request.userAnswers.period - getDerivedItems(waypoints, DeriveNumberOfSales) { + withCompleteDataAsync[Country]( + data = getCountriesWithIncompleteSales _, + onFailure = (incompleteCountries: Seq[Country]) => { + if (incompletePromptShown) { + firstIndexedIncompleteCountrySales(incompleteCountries) match { + case Some(incompleteCountry) => + if (incompleteCountry._1.vatRatesFromCountry.isEmpty) { + Redirect(routes.VatRatesFromCountryController.onPageLoad(waypoints, Index(incompleteCountry._2))).toFuture + } else { + Redirect(routes.CheckSalesController.onPageLoad(waypoints, Index(incompleteCountry._2))).toFuture + } + case None => + Redirect(routes.JourneyRecoveryController.onPageLoad()).toFuture + } + } else { + Redirect(routes.SoldToCountryListController.onPageLoad(waypoints)).toFuture + } + })(getDerivedItems(waypoints, DeriveNumberOfSales) { number => val canAddSales = number < Country.euCountriesWithNI.size @@ -82,6 +107,6 @@ class SoldToCountryListController @Inject()( _ <- 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 ea547508..9a324b98 100644 --- a/app/controllers/StartReturnController.scala +++ b/app/controllers/StartReturnController.scala @@ -24,7 +24,7 @@ import pages.{StartReturnPage, Waypoints} import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} -import services.{PartialReturnPeriodService, PeriodService} +import services.PartialReturnPeriodService import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import utils.FutureSyntax.FutureOps import views.html.StartReturnView @@ -37,7 +37,6 @@ class StartReturnController @Inject()( override val messagesApi: MessagesApi, cc: AuthenticatedControllerComponents, formProvider: StartReturnFormProvider, - periodService: PeriodService, partialReturnPeriodService: PartialReturnPeriodService, view: StartReturnView, clock: Clock diff --git a/app/controllers/corrections/CorrectionListCountriesController.scala b/app/controllers/corrections/CorrectionListCountriesController.scala index 9b76ddba..41c214e0 100644 --- a/app/controllers/corrections/CorrectionListCountriesController.scala +++ b/app/controllers/corrections/CorrectionListCountriesController.scala @@ -18,6 +18,7 @@ package controllers.corrections import controllers.actions._ import forms.corrections.CorrectionListCountriesFormProvider +import models.corrections.CorrectionToCountry import models.{Country, Index} import pages.Waypoints import pages.corrections.CorrectionListCountriesPage @@ -25,18 +26,20 @@ 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.CompletionChecks +import utils.FutureSyntax.FutureOps import viewmodels.checkAnswers.corrections.CorrectionListCountriesSummary import views.html.corrections.CorrectionListCountriesView import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} - class CorrectionListCountriesController @Inject()( override val messagesApi: MessagesApi, cc: AuthenticatedControllerComponents, formProvider: CorrectionListCountriesFormProvider, view: CorrectionListCountriesView - )(implicit ec: ExecutionContext) extends FrontendBaseController with CorrectionBaseController with I18nSupport { + )(implicit ec: ExecutionContext) + extends FrontendBaseController with CorrectionBaseController with CompletionChecks with I18nSupport { protected val controllerComponents: MessagesControllerComponents = cc @@ -53,34 +56,67 @@ class CorrectionListCountriesController @Inject()( val canAddCountries = number < Country.euCountriesWithNI.size val list = CorrectionListCountriesSummary .addToListRows(request.userAnswers, waypoints, periodIndex, CorrectionListCountriesPage(periodIndex)) - - Ok(view(form, waypoints, list, period, correctionPeriod, periodIndex, canAddCountries, Seq.empty)) + withCompleteData[CorrectionToCountry]( + periodIndex, + data = getIncompleteCorrections _, + onFailure = (incompleteCorrections: Seq[CorrectionToCountry]) => { + Ok(view( + form, + waypoints, + list, + period, + correctionPeriod, + periodIndex, + canAddCountries, + incompleteCorrections.map(_.correctionCountry.name) + )) + }) { + Ok(view(form, waypoints, list, period, correctionPeriod, periodIndex, canAddCountries, Seq.empty)) + } } } - def onSubmit(waypoints: Waypoints, periodIndex: Index): Action[AnyContent] = cc.authAndGetDataAndCorrectionEligible().async { + def onSubmit(waypoints: Waypoints, periodIndex: Index, incompletePromptShown: Boolean): Action[AnyContent] = cc.authAndGetDataAndCorrectionEligible().async { implicit request => val period = request.userAnswers.period - getNumberOfCorrectionsAsync(periodIndex) { - (number, correctionPeriod) => - val canAddCountries = number < Country.euCountriesWithNI.size - val list = CorrectionListCountriesSummary - .addToListRows(request.userAnswers, waypoints, periodIndex, CorrectionListCountriesPage(periodIndex)) - - form.bindFromRequest().fold( - formWithErrors => - Future.successful(BadRequest(view(formWithErrors, waypoints, list, period, correctionPeriod, periodIndex, canAddCountries, Seq.empty))), + withCompleteDataAsync[CorrectionToCountry]( + periodIndex, + data = getIncompleteCorrections _, + onFailure = (incompleteCorrections: Seq[CorrectionToCountry]) => { + if (incompletePromptShown) { + firstIndexedIncompleteCorrection(periodIndex, incompleteCorrections) match { + case Some(incompleteCorrections) => + Redirect(routes.VatAmountCorrectionCountryController.onPageLoad(waypoints, periodIndex, Index(incompleteCorrections._2))).toFuture + case None => + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()).toFuture + } + } else { + Redirect(routes.CorrectionListCountriesController.onPageLoad(waypoints, periodIndex)).toFuture + } + })( + onSuccess = { + getNumberOfCorrectionsAsync(periodIndex) { (number, correctionPeriod) => + val canAddCountries = number < Country.euCountriesWithNI.size + val list = CorrectionListCountriesSummary + .addToListRows(request.userAnswers, waypoints, periodIndex, CorrectionListCountriesPage(periodIndex)) - value => - for { - updatedAnswers <- Future.fromTry(request.userAnswers.set(CorrectionListCountriesPage(periodIndex), value)) - _ <- cc.sessionRepository.set(updatedAnswers) - } yield Redirect(CorrectionListCountriesPage(periodIndex).navigate(waypoints, request.userAnswers, updatedAnswers).route)) + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, waypoints, list, period, correctionPeriod, periodIndex, canAddCountries, Seq.empty))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set(CorrectionListCountriesPage(periodIndex), value)) + _ <- cc.sessionRepository.set(updatedAnswers) + } yield Redirect(CorrectionListCountriesPage(periodIndex).navigate(waypoints, request.userAnswers, updatedAnswers).route) + ) + } + } - } + ) } diff --git a/app/controllers/corrections/CorrectionReturnYearController.scala b/app/controllers/corrections/CorrectionReturnYearController.scala index 7826f087..26093f89 100644 --- a/app/controllers/corrections/CorrectionReturnYearController.scala +++ b/app/controllers/corrections/CorrectionReturnYearController.scala @@ -62,15 +62,20 @@ class CorrectionReturnYearController @Inject()( } filteredFulfilledObligations.map { obligations => - val periodKeys = obligations.map(obligation => ConvertPeriodKey.yearFromEtmpPeriodKey(obligation.periodKey)).distinct - val form: Form[Int] = formProvider(index, periodKeys) - val preparedForm = request.userAnswers.get(CorrectionReturnYearPage(index)) match { - case None => form - case Some(value) => form.fill(value) - } + if (obligations.size < 2) { + Redirect(controllers.corrections.routes.CorrectionReturnSinglePeriodController.onPageLoad(waypoints, index)) + } else { + val periodKeys = obligations.map(obligation => ConvertPeriodKey.yearFromEtmpPeriodKey(obligation.periodKey)).distinct - Ok(view(preparedForm, waypoints, period, utils.ItemsHelper.radioButtonItems(periodKeys), index)) + val form: Form[Int] = formProvider(index, periodKeys) + val preparedForm = request.userAnswers.get(CorrectionReturnYearPage(index)) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, waypoints, period, utils.ItemsHelper.radioButtonItems(periodKeys), index)) + } } } @@ -92,8 +97,11 @@ class CorrectionReturnYearController @Inject()( form.bindFromRequest().fold( formWithErrors => - - BadRequest(view(formWithErrors, waypoints, period, utils.ItemsHelper.radioButtonItems(periodKeys), index)).toFuture, + if (obligations.size < 2) { + Redirect(controllers.corrections.routes.CorrectionReturnSinglePeriodController.onPageLoad(waypoints, index)).toFuture + } else { + BadRequest(view(formWithErrors, waypoints, period, utils.ItemsHelper.radioButtonItems(periodKeys), index)).toFuture + }, value => for { diff --git a/app/controllers/corrections/VatAmountCorrectionCountryController.scala b/app/controllers/corrections/VatAmountCorrectionCountryController.scala index d55d8ec5..2e4356f3 100644 --- a/app/controllers/corrections/VatAmountCorrectionCountryController.scala +++ b/app/controllers/corrections/VatAmountCorrectionCountryController.scala @@ -61,7 +61,17 @@ class VatAmountCorrectionCountryController @Inject()( case None => form case Some(value) => form.fill(value) } - Ok(view(preparedForm, waypoints, period, periodIndex, correctionReturnPeriod, countryIndex, country, isCountryPreviouslyDeclared, previouslyDeclaredAmount)).toFuture + Ok(view( + preparedForm, + waypoints, + period, + periodIndex, + correctionReturnPeriod, + countryIndex, + country, + isCountryPreviouslyDeclared, + previouslyDeclaredAmount + )).toFuture } } } @@ -84,7 +94,17 @@ class VatAmountCorrectionCountryController @Inject()( form.bindFromRequest().fold( formWithErrors => - BadRequest(view(formWithErrors, waypoints, period, periodIndex, correctionReturnPeriod, countryIndex, country, isCountryPreviouslyDeclared, previouslyDeclaredAmount)).toFuture, + BadRequest(view( + formWithErrors, + waypoints, + period, + periodIndex, + correctionReturnPeriod, + countryIndex, + country, + isCountryPreviouslyDeclared, + previouslyDeclaredAmount + )).toFuture, value => for { diff --git a/app/models/domain/VatRate.scala b/app/models/domain/VatRate.scala new file mode 100644 index 00000000..f108be19 --- /dev/null +++ b/app/models/domain/VatRate.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.domain + +import models.{Enumerable, WithName} +import play.api.libs.json.{Json, OFormat} + +case class VatRate(rate: BigDecimal, rateType: VatRateType) { + lazy val rateForDisplay: String = if(rate.isWhole) { + rate.toString.split('.').headOption.getOrElse(rate.toString) + "%" + } else { + rate.toString + "%" + } +} + +object VatRate { + + implicit val format: OFormat[VatRate] = Json.format[VatRate] +} + +sealed trait VatRateType + +object VatRateType extends Enumerable.Implicits { + + case object Standard extends WithName("STANDARD") with VatRateType + case object Reduced extends WithName("REDUCED") with VatRateType + + val values: Seq[VatRateType] = Seq(Standard, Reduced) + + implicit val enumerable: Enumerable[VatRateType] = + Enumerable(values.map(v => v.toString -> v): _*) +} diff --git a/app/models/package.scala b/app/models/package.scala index 2fd43842..e8fba079 100644 --- a/app/models/package.scala +++ b/app/models/package.scala @@ -14,10 +14,13 @@ * limitations under the License. */ +import cats.data.ValidatedNec import play.api.libs.json._ package object models { + type ValidationResult[A] = ValidatedNec[ValidationError, A] + implicit class RichJsObject(jsObject: JsObject) { def setObject(path: JsPath, value: JsValue): JsResult[JsObject] = diff --git a/app/models/requests/VatReturnRequest.scala b/app/models/requests/VatReturnRequest.scala new file mode 100644 index 00000000..de91911d --- /dev/null +++ b/app/models/requests/VatReturnRequest.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.requests + +import models.Period +import play.api.libs.json.{Json, OFormat} +import queries.SalesToCountry +import uk.gov.hmrc.domain.Vrn + +import java.time.LocalDate + +case class VatReturnRequest( + vrn: Vrn, + period: Period, + startDate: Option[LocalDate], + endDate: Option[LocalDate], + sales: List[SalesToCountry] + ) + + +object VatReturnRequest { + + implicit val format: OFormat[VatReturnRequest] = Json.format[VatReturnRequest] +} diff --git a/app/models/requests/corrections/CorrectionRequest.scala b/app/models/requests/corrections/CorrectionRequest.scala new file mode 100644 index 00000000..9b5cc04f --- /dev/null +++ b/app/models/requests/corrections/CorrectionRequest.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.requests.corrections + +import models.StandardPeriod +import models.corrections.PeriodWithCorrections +import play.api.libs.json.{Json, OFormat} +import uk.gov.hmrc.domain.Vrn + +case class CorrectionRequest( + vrn: Vrn, + period: StandardPeriod, + corrections: List[PeriodWithCorrections] + ) + +object CorrectionRequest { + + implicit val format: OFormat[CorrectionRequest] = Json.format[CorrectionRequest] + +} diff --git a/app/pages/PageConstants.scala b/app/pages/PageConstants.scala index e92304f2..096c61e3 100644 --- a/app/pages/PageConstants.scala +++ b/app/pages/PageConstants.scala @@ -23,6 +23,7 @@ object PageConstants { val vatRates: String = "vatRatesFromCountry" val salesAtVatRate: String = "salesAtVatRate" val netValueOfSales: String = "netValueOfSales" + val vatOnSales: String = "vatOnSales" val corrections: String = "corrections" val correctionsToCountry: String = "correctionsToCountry" val previouslyDeclaredCorrectionsForCountry: String = "previouslyDeclaredCorrectionAmountForCountry" diff --git a/app/queries/AllSalesByCountryQuery.scala b/app/queries/AllSalesByCountryQuery.scala index 5434ce9c..da00fb66 100644 --- a/app/queries/AllSalesByCountryQuery.scala +++ b/app/queries/AllSalesByCountryQuery.scala @@ -16,6 +16,7 @@ package queries +import models.domain.VatRate import models.{Country, Index, VatOnSales, VatRateFromCountry, VatRateType} import pages.PageConstants import play.api.libs.functional.syntax._ @@ -39,7 +40,14 @@ case class VatRateWithOptionalSalesFromCountry( validFrom: LocalDate, validUntil: Option[LocalDate] = None, salesAtVatRate: Option[OptionalSalesAtVatRate] - ) + ) { + + lazy val rateForDisplay: String = if(rate.isWhole) { + rate.toString.split('.').headOption.getOrElse(rate.toString) + "%" + } else { + rate.toString + "%" + } +} object VatRateWithOptionalSalesFromCountry { @@ -111,3 +119,23 @@ case class AllSalesByCountryQuery(countryIndex: Index) extends Gettable[SalesToC override def path: JsPath = JsPath \ PageConstants.sales \ countryIndex.position } + +case class SalesToCountry( + country: Country, + amounts: List[SalesDetails] + ) + +object SalesToCountry { + + implicit val format: OFormat[SalesToCountry] = Json.format[SalesToCountry] +} + +case class SalesDetails( + vatRate: VatRate, + netValueOfSales: Option[BigDecimal], + vatOnSales: VatOnSales + ) + +object SalesDetails { + implicit val format: OFormat[SalesDetails] = Json.format[SalesDetails] +} \ No newline at end of file diff --git a/app/queries/AllSalesWithOptionalVatQuery.scala b/app/queries/AllSalesWithOptionalVatQuery.scala new file mode 100644 index 00000000..b4819064 --- /dev/null +++ b/app/queries/AllSalesWithOptionalVatQuery.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 queries + +import models.Index +import pages.PageConstants +import play.api.libs.json.JsPath + +case class AllSalesWithOptionalVatQuery(countryIndex: Index) + extends Gettable[Seq[VatRateWithOptionalSalesFromCountry]] with Settable[Seq[VatRateWithOptionalSalesFromCountry]] { + + override def path: JsPath = + JsPath \ PageConstants.sales \ countryIndex.position \ PageConstants.vatRates + +} \ No newline at end of file diff --git a/app/queries/SalesAtVatRateQuery.scala b/app/queries/SalesAtVatRateQuery.scala new file mode 100644 index 00000000..b0da54a5 --- /dev/null +++ b/app/queries/SalesAtVatRateQuery.scala @@ -0,0 +1,26 @@ +/* + * 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 queries + +import models.Index +import pages.PageConstants.{salesAtVatRate, sales, vatRates} +import play.api.libs.json.JsPath + +case class SalesAtVatRateQuery(countryIndex: Index, vatRateIndex: Index) extends Gettable[OptionalSalesAtVatRate] with Settable[OptionalSalesAtVatRate] { + + override def path: JsPath = JsPath \ sales \ countryIndex.position \ vatRates \ vatRateIndex.position \ salesAtVatRate +} diff --git a/app/queries/VatOnSalesFromQuery.scala b/app/queries/VatOnSalesFromQuery.scala new file mode 100644 index 00000000..1d0e0cce --- /dev/null +++ b/app/queries/VatOnSalesFromQuery.scala @@ -0,0 +1,26 @@ +/* + * 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 queries + +import models.{Index, VatOnSales} +import pages.PageConstants.{sales, vatRates, vatOnSales} +import play.api.libs.json.JsPath + +case class VatOnSalesFromQuery(countryIndex: Index, vatRateIndex: Index) extends Gettable[VatOnSales] { + + override def path: JsPath = JsPath \ sales \ countryIndex.position \ vatRates \ vatRateIndex.position \ vatOnSales +} diff --git a/app/services/CorrectionService.scala b/app/services/CorrectionService.scala index 4ae02070..bb741367 100644 --- a/app/services/CorrectionService.scala +++ b/app/services/CorrectionService.scala @@ -16,10 +16,16 @@ package services +import cats.implicits.{catsSyntaxValidatedIdBinCompat0, toTraverseOps} import connectors.VatReturnConnector import logging.Logging +import models.corrections.{CorrectionToCountry, PeriodWithCorrections} import models.etmp.EtmpVatReturn -import models.{Country, Period} +import models.requests.corrections.CorrectionRequest +import models.{Country, DataMissingError, Index, Period, StandardPeriod, UserAnswers, ValidationResult} +import pages.corrections.CorrectPreviousReturnPage +import queries.{AllCorrectionCountriesQuery, AllCorrectionPeriodsQuery, CorrectionToCountryQuery} +import uk.gov.hmrc.domain.Vrn import uk.gov.hmrc.http.HeaderCarrier import javax.inject.Inject @@ -88,4 +94,61 @@ class CorrectionService @Inject()( getAllPeriodsInRange(Seq.empty, periodFrom, periodTo) } + + def fromUserAnswers(answers: UserAnswers, vrn: Vrn, period: Period): ValidationResult[CorrectionRequest] = { + getCorrections(answers).map { corrections => + CorrectionRequest(vrn, StandardPeriod.fromPeriod(period), corrections) + } + } + + private def getCorrections(answers: UserAnswers): ValidationResult[List[PeriodWithCorrections]] = { + answers.get(CorrectPreviousReturnPage(0)) match { + case Some(false) => + List.empty[PeriodWithCorrections].validNec + case Some(true) => + processCorrections(answers) + case None => + DataMissingError(CorrectPreviousReturnPage(0)).invalidNec + } + } + + private def processCorrections(answers: UserAnswers): ValidationResult[List[PeriodWithCorrections]] = { + answers.get(AllCorrectionPeriodsQuery) match { + case Some(periodWithCorrections) if periodWithCorrections.nonEmpty => + periodWithCorrections.zipWithIndex.map { + case (_, index) => + processCorrectionsToCountry(answers, Index(index)) + }.sequence.map { _ => + periodWithCorrections + } + case _ => + DataMissingError(AllCorrectionPeriodsQuery).invalidNec + } + } + + + private def processCorrectionsToCountry(answers: UserAnswers, periodIndex: Index): ValidationResult[List[CorrectionToCountry]] = { + answers.get(AllCorrectionCountriesQuery(periodIndex)) match { + case Some(value) if value.nonEmpty => + value.zipWithIndex.map { + case (_, index) => + processCorrectionToCountry(answers, periodIndex, Index(index)) + }.sequence + case _ => + DataMissingError(AllCorrectionCountriesQuery(periodIndex)).invalidNec + } + } + + private def processCorrectionToCountry(answers: UserAnswers, periodIndex: Index, countryIndex: Index): ValidationResult[CorrectionToCountry] = { + answers.get(CorrectionToCountryQuery(periodIndex, countryIndex)) match { + case Some(value) => + value match { + case CorrectionToCountry(_, Some(_)) => value.validNec + case CorrectionToCountry(_, None) => DataMissingError(CorrectionToCountryQuery(periodIndex, countryIndex)).invalidNec + + } + case _ => + DataMissingError(CorrectionToCountryQuery(periodIndex, countryIndex)).invalidNec + } + } } diff --git a/app/services/RedirectService.scala b/app/services/RedirectService.scala new file mode 100644 index 00000000..1deee7d2 --- /dev/null +++ b/app/services/RedirectService.scala @@ -0,0 +1,95 @@ +/* + * 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 services + +import cats.data.Validated.Invalid +import controllers._ +import controllers.actions.AuthenticatedControllerComponents +import controllers.corrections.{routes => correctionsRoutes} +import logging.Logging +import models.requests.DataRequest +import models.{DataMissingError, Index, Period, ValidationError} +import pages.corrections.CorrectPreviousReturnPage +import pages.{VatRatesFromCountryPage, Waypoints} +import play.api.i18n.I18nSupport +import play.api.mvc.{AnyContent, Call, MessagesControllerComponents} +import queries._ +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController + +import javax.inject.Inject +import scala.concurrent.ExecutionContext + +class RedirectService @Inject()( + cc: AuthenticatedControllerComponents, + correctionService: CorrectionService, + vatReturnService: VatReturnService + )(implicit ec:ExecutionContext) extends FrontendBaseController with I18nSupport with Logging { + + protected val controllerComponents: MessagesControllerComponents = cc + + def validate(period: Period)(implicit request: DataRequest[AnyContent]): List[ValidationError] = { + + val validateVatReturnRequest = vatReturnService.fromUserAnswers(request.userAnswers, request.vrn, period) + + val validateCorrectionRequest = request.userAnswers.get(CorrectPreviousReturnPage(0)).map(_ => + correctionService.fromUserAnswers(request.userAnswers, request.vrn, period)) + + (validateVatReturnRequest, validateCorrectionRequest) match { + case (Invalid(vatReturnErrors), Some(Invalid(correctionErrors))) => + (vatReturnErrors ++ correctionErrors).toChain.toList + case (Invalid(errors), _) => + errors.toChain.toList + case (_, Some(Invalid(errors))) => + errors.toChain.toList + case _ => List.empty[ValidationError] + } + } + + def getRedirect(waypoints: Waypoints, errors: List[ValidationError]): List[Call] = { + errors.flatMap { + case DataMissingError(AllSalesQuery) => + logger.error(s"Data missing - no data provided for sales") + Some(routes.SoldToCountryController.onPageLoad(waypoints, Index(0))) + case DataMissingError(VatRatesFromCountryPage(countryIndex, vatRateIndex)) => + logger.error(s"Data missing - vat rates with index ${vatRateIndex.position}") + Some(routes.VatRatesFromCountryController.onPageLoad(waypoints, countryIndex)) + case DataMissingError(SalesAtVatRateQuery(countryIndex, vatRateIndex)) => + logger.error(s"Data missing - vat rates with index ${vatRateIndex.position} for country ${countryIndex.position}") + Some(routes.SalesToCountryController.onPageLoad(waypoints, countryIndex, vatRateIndex)) + case DataMissingError(VatOnSalesFromQuery(countryIndex, vatRateIndex)) => + logger.error(s"Data missing - vat charged on sales at vat rate ${vatRateIndex.position} for country ${countryIndex.position}") + Some(routes.VatOnSalesController.onPageLoad(waypoints, countryIndex, vatRateIndex)) + + case DataMissingError(AllCorrectionPeriodsQuery) => + logger.error(s"Data missing - no data provided for corrections") + Some(correctionsRoutes.CorrectionReturnYearController.onPageLoad(waypoints, Index(0))) + case DataMissingError(AllCorrectionCountriesQuery(periodIndex)) => + logger.error(s"Data missing - no countries found for corrections to period ${periodIndex.position}") + Some(correctionsRoutes.CorrectionCountryController.onPageLoad(waypoints, periodIndex, Index(0))) + case DataMissingError(CorrectionToCountryQuery(periodIndex, countryIndex)) => + logger.error(s"Data missing - correction to country ${countryIndex.position} in period ${periodIndex.position}") + Some(correctionsRoutes.VatAmountCorrectionCountryController.onPageLoad(waypoints, periodIndex, Index(0))) + + case DataMissingError(_) => + logger.error(s"Unhandled DataMissingError") + None + case _ => + logger.error(s"Unhandled ValidationError") + None + } + } +} diff --git a/app/services/VatReturnService.scala b/app/services/VatReturnService.scala new file mode 100644 index 00000000..861a8b48 --- /dev/null +++ b/app/services/VatReturnService.scala @@ -0,0 +1,107 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services + +import cats.implicits.{catsSyntaxValidatedIdBinCompat0, toTraverseOps} +import models.requests.VatReturnRequest +import models.{DataMissingError, Index, Period, StandardPeriod, UserAnswers, ValidationResult, VatRateFromCountry, VatRateType} +import models.domain.{VatRate => DomainVatRate, VatRateType => DomainVatRateType} +import pages.{SoldGoodsPage, VatRatesFromCountryPage} +import queries.{AllSalesQuery, OptionalSalesAtVatRate, SalesAtVatRateQuery, SalesDetails, SalesToCountry, VatOnSalesFromQuery} +import uk.gov.hmrc.domain.Vrn + +import javax.inject.Inject + +class VatReturnService @Inject() { + + def fromUserAnswers(answers: UserAnswers, vrn: Vrn, period: Period): ValidationResult[VatReturnRequest] = { + getSales(answers).map(sales => + VatReturnRequest(vrn, StandardPeriod.fromPeriod(period), Some(period.firstDay), Some(period.lastDay), sales) + ) + } + + + private def getSales(answers: UserAnswers): ValidationResult[List[SalesToCountry]] = + answers.get(SoldGoodsPage) match { + case Some(true) => + processSales(answers) + case Some(false) => + List.empty[SalesToCountry].validNec + case None => + DataMissingError(SoldGoodsPage).invalidNec + } + + private def processSales(answers: UserAnswers): ValidationResult[List[SalesToCountry]] = { + answers.get(AllSalesQuery) match { + case Some(sales) if sales.nonEmpty => + sales.zipWithIndex.map { + case (_, index) => + processSalesToCountry(answers, Index(index), Index(index)) + }.sequence.map { + salesDetails => + sales.zip(salesDetails).map { + case (sales, salesDetails) => + SalesToCountry(sales.country, salesDetails) + } + } + case _ => + DataMissingError(AllSalesQuery).invalidNec + } + } + + private def processSalesToCountry(answers: UserAnswers, countryIndex: Index, vatRateIndex: Index): ValidationResult[List[SalesDetails]] = + answers.get(VatRatesFromCountryPage(countryIndex, vatRateIndex)) match { + case Some(list) if list.nonEmpty => + list.zipWithIndex.map { + case (vatRate, index) => + processSalesAtVatRate(answers, countryIndex, Index(index), vatRate) + }.sequence + case _ => + DataMissingError(VatRatesFromCountryPage(countryIndex, vatRateIndex)).invalidNec + } + + private def processSalesAtVatRate( + answers: UserAnswers, + countryIndex: Index, + vatRateIndex: Index, + vatRate: VatRateFromCountry + ): ValidationResult[SalesDetails] = + answers.get(SalesAtVatRateQuery(countryIndex, vatRateIndex)) match { + case Some(OptionalSalesAtVatRate(netValueOfSales, Some(vatOnSales))) => + SalesDetails( + vatRate = toDomainVatRate(vatRate), + netValueOfSales = netValueOfSales, + vatOnSales = vatOnSales + ).validNec + case Some(OptionalSalesAtVatRate(_, None)) => + DataMissingError(VatOnSalesFromQuery(countryIndex, vatRateIndex)).invalidNec + case None => + DataMissingError(SalesAtVatRateQuery(countryIndex, vatRateIndex)).invalidNec + } + + private def toDomainVatRate(vatRate: VatRateFromCountry): DomainVatRate = { + DomainVatRate( + vatRate.rate, + if(vatRate.rateType == VatRateType.Reduced) { + DomainVatRateType.Reduced + } else { + DomainVatRateType.Standard + } + ) + } + +} diff --git a/app/utils/CompletionChecks.scala b/app/utils/CompletionChecks.scala new file mode 100644 index 00000000..e88d646b --- /dev/null +++ b/app/utils/CompletionChecks.scala @@ -0,0 +1,122 @@ +/* + * 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 utils + +import models.{Country, Index} +import models.corrections.CorrectionToCountry +import models.requests.DataRequest +import play.api.mvc.{AnyContent, Result} +import queries._ + +import scala.concurrent.Future + +trait CompletionChecks { + + protected def withCompleteData[A](index: Index, data: Index => Seq[A], onFailure: Seq[A] => Result) + (onSuccess: => Result): Result = { + val incomplete = data(index) + if(incomplete.isEmpty) { + onSuccess + } else { + onFailure(incomplete) + } + } + + protected def withCompleteDataAsync[A](index: Index, data: Index => Seq[A], onFailure: Seq[A] => Future[Result]) + (onSuccess: => Future[Result]): Future[Result] = { + + val incomplete = data(index) + if(incomplete.isEmpty) { + onSuccess + } else { + onFailure(incomplete) + } + } + + + protected def withCompleteData[A](data: () => Seq[A], onFailure: Seq[A] => Result) + (onSuccess: => Result): Result = { + + val incomplete = data() + if(incomplete.isEmpty) { + onSuccess + } else { + onFailure(incomplete) + } + } + + protected def withCompleteDataAsync[A](data: () => Seq[A], onFailure: Seq[A] => Future[Result]) + (onSuccess: => Future[Result]): Future[Result] = { + + val incomplete = data() + if(incomplete.isEmpty) { + onSuccess + } else { + onFailure(incomplete) + } + } + + def getIncompleteCorrections(periodIndex: Index)(implicit request: DataRequest[AnyContent]): List[CorrectionToCountry] = { + request.userAnswers + .get(AllCorrectionCountriesQuery(periodIndex)) + .map(_.filter(_.countryVatCorrection.isEmpty)).getOrElse(List.empty) + } + + def firstIndexedIncompleteCorrection(periodIndex: Index, incompleteCorrections: Seq[CorrectionToCountry]) + (implicit request: DataRequest[AnyContent]): Option[(CorrectionToCountry, Int)] = { + request.userAnswers.get(AllCorrectionCountriesQuery(periodIndex)) + .getOrElse(List.empty).zipWithIndex + .find(indexedCorrection => incompleteCorrections.contains(indexedCorrection._1)) + } + + def getIncompleteVatRateAndSales(countryIndex: Index)(implicit request: DataRequest[AnyContent]): Seq[VatRateWithOptionalSalesFromCountry] = { + val noSales = request.userAnswers + .get(AllSalesWithOptionalVatQuery(countryIndex)) + .map(_.filter(_.salesAtVatRate.isEmpty)).getOrElse(List.empty) + + val noVat = request.userAnswers + .get(AllSalesWithOptionalVatQuery(countryIndex)) + .map( + _.filter( v => + v.salesAtVatRate.exists(_.vatOnSales.isEmpty) + ) + ).getOrElse(List.empty) + + noSales ++ noVat + } + + def getCountriesWithIncompleteSales()(implicit request: DataRequest[AnyContent]): Seq[Country] = { + request.userAnswers + .get(AllSalesWithTotalAndVatQuery) + .map(_.filter(sales => + sales.vatRatesFromCountry.isEmpty || + sales.vatRatesFromCountry.getOrElse(List.empty).exists( + rate => rate.salesAtVatRate.isEmpty || rate.salesAtVatRate.exists(_.vatOnSales.isEmpty) + ) + )) + .map(_.map(_.country)) + .getOrElse(Seq.empty) + } + + def firstIndexedIncompleteCountrySales(incompleteCountries: Seq[Country]) + (implicit request: DataRequest[AnyContent]): Option[(SalesToCountryWithOptionalSales, Int)] = { + request.userAnswers.get(AllSalesWithTotalAndVatQuery) + .getOrElse(List.empty).zipWithIndex + .find(indexedCorrection => incompleteCountries.contains(indexedCorrection._1.country)) + } + +} diff --git a/app/views/CheckSalesView.scala.html b/app/views/CheckSalesView.scala.html index 6a08fd24..bca880be 100644 --- a/app/views/CheckSalesView.scala.html +++ b/app/views/CheckSalesView.scala.html @@ -15,48 +15,82 @@ *@ @import viewmodels.LegendSize +@import queries.VatRateWithOptionalSalesFromCountry @this( layout: templates.Layout, formHelper: FormWithCSRF, govukErrorSummary: GovukErrorSummary, govukRadios: GovukRadios, + govukButton: GovukButton, govukSummaryList: GovukSummaryList, - button: ButtonGroup + button: ButtonGroup, + govukWarningText: GovukWarningText ) -@(form: Form[_], waypoints: Waypoints, period: Period, checkSalesSummaryLists: Seq[SummaryList], countryIndex: Index, country: Country, canAddAnotherVatRate: Boolean)(implicit request: Request[_], messages: Messages) +@( + form: Form[_], + waypoints: Waypoints, + period: Period, + checkSalesSummaryLists: Seq[SummaryList], + countryIndex: Index, + country: Country, + canAddAnotherVatRate: Boolean, + incompleteSales: Seq[VatRateWithOptionalSalesFromCountry] = List.empty +)(implicit request: Request[_], messages: Messages) @layout(pageTitle = title(form, messages("checkSales.title"), Some(messages("checkSales.caption", period.displayText, country.name)))) { - @formHelper(action = routes.CheckSalesController.onSubmit(waypoints, countryIndex), Symbol("autoComplete") -> "off") { - @if(form.errors.nonEmpty) { - @govukErrorSummary(ErrorSummaryViewModel(form)) - } + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } -
-

@messages("checkSales.caption", period.displayText, country.name)

-

@messages("checkSales.heading")

-
+
+

@messages("checkSales.caption", period.displayText, country.name)

+

@messages("checkSales.heading")

+
- @for(checkSalesSummaryList <- checkSalesSummaryLists) { - @govukSummaryList(checkSalesSummaryList) - } + @if(!incompleteSales.isEmpty) { + @govukWarningText(WarningText( + iconFallbackText = Option(messages("site.warning")), + content = Text(messages("error.missing_answers")) + )) + } + + @for(checkSalesSummaryList <- checkSalesSummaryLists) { + @govukSummaryList(checkSalesSummaryList) + } - @if(canAddAnotherVatRate) { - @govukRadios( - RadiosViewModel.yesNo( - field = form("value"), - legend = LegendViewModel( - HtmlContent(Html("""

""" + messages("checkSales.addAnother") + "

")) - ).withSize(LegendSize.Medium) + @if(incompleteSales.isEmpty) { + @formHelper(action = routes.CheckSalesController.onSubmit(waypoints, countryIndex, false), Symbol("autoComplete") -> "off") { + @if(canAddAnotherVatRate) { + @govukRadios( + RadiosViewModel.yesNo( + field = form("value"), + legend = LegendViewModel( + HtmlContent(Html("""

""" + messages("checkSales.addAnother") + "

")) + ).withSize(LegendSize.Medium) + ) ) - ) - } else { - + } else { + + } + + @button(messages("site.continue"), routes.CheckSalesController.onPageLoad(waypoints, countryIndex).url, period, waypoints) } + } else { +

@messages("error.missing_answers_header")

+

@messages("error.missing_answers_prompt", if(incompleteSales.size > 1) { + incompleteSales.map(_.rateForDisplay).reverse.tail.mkString(", ") + " and " + incompleteSales.last.rateForDisplay + " VAT rates" + } else { + incompleteSales.head.rateForDisplay + " VAT rate" + })

- @button(messages("site.continue"), routes.CheckSalesController.onPageLoad(waypoints, countryIndex).url, period, waypoints) + @formHelper(action = routes.CheckSalesController.onSubmit(waypoints, countryIndex, true)) { + @govukButton( + ButtonViewModel(messages("error.resolve_missing_answers")) + ) + } } } diff --git a/app/views/SoldToCountryListView.scala.html b/app/views/SoldToCountryListView.scala.html index 07ed65e7..98b0a13e 100644 --- a/app/views/SoldToCountryListView.scala.html +++ b/app/views/SoldToCountryListView.scala.html @@ -23,11 +23,20 @@ formHelper: FormWithCSRF, govukErrorSummary: GovukErrorSummary, govukRadios: GovukRadios, + govukButton: GovukButton, addToList: components.addToList, - button: ButtonGroup + button: ButtonGroup, + govukWarningText: GovukWarningText ) -@(form: Form[_], waypoints: Waypoints, period: Period, salesList: Seq[ListItem], canAddSales: Boolean)(implicit request: Request[_], messages: Messages) +@( + form: Form[_], + waypoints: Waypoints, + period: Period, + salesList: Seq[ListItem], + canAddSales: Boolean, + incompleteCountries: Seq[Country] = List.empty +)(implicit request: Request[_], messages: Messages) @titleText = @{ salesList.size match { @@ -45,32 +54,51 @@ @layout(pageTitle = title(form, titleText)) { - @formHelper(action = routes.SoldToCountryListController.onSubmit(waypoints), Symbol("autoComplete") -> "off") { - @if(form.errors.nonEmpty) { - @govukErrorSummary(ErrorSummaryViewModel(form)) - } + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + + @if(incompleteCountries.nonEmpty) { + @govukWarningText(WarningText( + iconFallbackText = Option(messages("site.warning")), + content = Text(messages("error.missing_answers")) + )) + } -

@headingText

+

@headingText

- @addToList(salesList, itemSize = Long, "soldToCountryList.change.hidden", "soldToCountryList.remove.hidden") + @addToList(salesList, itemSize = Long, "soldToCountryList.change.hidden", "soldToCountryList.remove.hidden") + @if(incompleteCountries.isEmpty) { + @formHelper(action = routes.SoldToCountryListController.onSubmit(waypoints, false), Symbol("autoComplete") -> "off") { - @if(canAddSales) { - @govukRadios( - RadiosViewModel.yesNo( - field = form("value"), - legend = LegendViewModel( - HtmlContent(Html("""

""" + messages("soldToCountryList.addAnother") + "

")) - ).withSize(LegendSize.Medium) - ).withHint(HintViewModel(messages("soldToCountryList.addAnother.hint"))) + @if(canAddSales) { + @govukRadios( + RadiosViewModel.yesNo( + field = form("value"), + legend = LegendViewModel( + HtmlContent(Html("""

""" + messages("soldToCountryList.addAnother") + "

")) + ).withSize(LegendSize.Medium) + ).withHint(HintViewModel(messages("soldToCountryList.addAnother.hint"))) + ) + } else { +

@messages("soldToCountryList.maximumReached")

+ + } + + @button( + "site.continue", routes.SoldToCountryListController.onPageLoad(waypoints).url, period, waypoints ) - } else { -

@messages("soldToCountryList.maximumReached")

- } + } else { + @formHelper(action = routes.SoldToCountryListController.onSubmit(waypoints, true)) { + +

@messages("error.missing_answers_header")

+

@messages("error.missing_answers_prompt", incompleteCountries.head.name)

- @button( - "site.continue", routes.SoldToCountryListController.onPageLoad(waypoints).url, period, waypoints - ) + @govukButton( + ButtonViewModel(messages("error.resolve_missing_answers")) + ) + } } } diff --git a/app/views/corrections/CorrectionListCountriesView.scala.html b/app/views/corrections/CorrectionListCountriesView.scala.html index 7c7486d8..8a875c51 100644 --- a/app/views/corrections/CorrectionListCountriesView.scala.html +++ b/app/views/corrections/CorrectionListCountriesView.scala.html @@ -51,8 +51,6 @@ Some(messages("correctionListCountries.section", correctionPeriod.displayText)) )) { - @formHelper(action = controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, periodIndex), Symbol("autoComplete") -> "off") { - @if(form.errors.nonEmpty) { @govukErrorSummary(ErrorSummaryViewModel(form)) } @@ -73,28 +71,32 @@

@headingText

@govukSummaryList(list) } @if(incompleteCountries.isEmpty) { - @if(canAddCountries) { - @govukRadios( - RadiosViewModel.yesNo( - field = form("value"), - legend = LegendViewModel( - HtmlContent(Html(messages("correctionListCountries.addAnother"))) - ).withSize(LegendSize.Medium) + @formHelper(action = controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, periodIndex, false), Symbol("autoComplete") -> "off") { + @if(canAddCountries) { + @govukRadios( + RadiosViewModel.yesNo( + field = form("value"), + legend = LegendViewModel( + HtmlContent(Html(messages("correctionListCountries.addAnother"))) + ).withSize(LegendSize.Medium) + ) ) + } else { +

@messages("correctionListCountries.maximumReached")

+ + } + + @button("site.continue", controllers.corrections.routes.CorrectionListCountriesController.onPageLoad(waypoints, periodIndex).url, period, waypoints ) - } else { -

@messages("correctionListCountries.maximumReached")

- } - - @button("site.continue", controllers.corrections.routes.CorrectionListCountriesController.onPageLoad(waypoints, periodIndex).url, period, waypoints) } else { -

@messages("error.missing_answers_header")

-

@messages("error.missing_answers_prompt", incompleteCountries.head)

+ @formHelper(action = controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, periodIndex, true), Symbol("autoComplete") -> "off") { +

@messages("error.missing_answers_header")

+

@messages("error.missing_answers_prompt", incompleteCountries.head)

- @govukButton( - ButtonViewModel(messages("error.resolve_missing_answers")) - ) + @govukButton( + ButtonViewModel(messages("error.resolve_missing_answers")) + ) + } } - } } diff --git a/conf/app.routes b/conf/app.routes index e3a28363..553f46b3 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -48,13 +48,13 @@ GET /cannot-start-return GET /cannot-start-excluded-return controllers.CannotStartExcludedReturnController.onPageLoad() GET /add-sales-country-list controllers.SoldToCountryListController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints) -POST /add-sales-country-list controllers.SoldToCountryListController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints) +POST /add-sales-country-list controllers.SoldToCountryListController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, incompletePromptShown: Boolean) GET /remove-sales-country/:index controllers.DeleteSoldToCountryController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints, index: Index) POST /remove-sales-country/:index controllers.DeleteSoldToCountryController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, index: Index) GET /check-sales/:index controllers.CheckSalesController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints, index: Index) -POST /check-sales/:index controllers.CheckSalesController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, index: Index) +POST /check-sales/:index controllers.CheckSalesController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, index: Index, incompletePromptShown: Boolean) GET /remove-vat-rate-sales-for-country/:countryIndex/:nextVatRateIndex controllers.DeleteVatRateSalesForCountryController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints, countryIndex: Index, nextVatRateIndex: Index) POST /remove-vat-rate-sales-for-country/:countryIndex/:nextVatRateIndex controllers.DeleteVatRateSalesForCountryController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, countryIndex: Index, nextVatRateIndex: Index) @@ -93,7 +93,7 @@ GET /vat-payable-confirm/:periodIndex/:countryIndex POST /vat-payable-confirm/:periodIndex/:countryIndex controllers.corrections.VatPayableForCountryController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, periodIndex: Index, countryIndex: Index) GET /correction-list-countries/:periodIndex controllers.corrections.CorrectionListCountriesController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints, periodIndex: Index) -POST /correction-list-countries/:periodIndex controllers.corrections.CorrectionListCountriesController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, periodIndex: Index) +POST /correction-list-countries/:periodIndex controllers.corrections.CorrectionListCountriesController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, periodIndex: Index, incompletePromptShown: Boolean) GET /:period/vat-correction-periods controllers.corrections.VatPeriodCorrectionsListController.onPageLoad(waypoints: Waypoints ?= EmptyWaypoints, period: Period) POST /:period/vat-correction-periods controllers.corrections.VatPeriodCorrectionsListController.onSubmit(waypoints: Waypoints ?= EmptyWaypoints, period: Period, incompletePromptShown: Boolean) diff --git a/conf/messages.en b/conf/messages.en index 80d7a749..a99de30d 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -37,6 +37,12 @@ error.number = Please enter a valid number error.required = Please enter a value error.summary.title = There is a problem +error.missing_answers = Some of your information is missing. You must complete this before you can submit your changes. +error.missing_answers_header = Some information is missing. +error.missing_answers_prompt = You need to complete the missing information for {0} before you continue. +error.missing_answers_prompt.general = You need to complete the missing information before you continue. +error.resolve_missing_answers = Resolve missing answers + yourAccount.title = Your Import One Stop Shop Account yourAccount.heading = Your Import One Stop Shop Account yourAccount.iossNumber = IOSS number: {0} diff --git a/test/controllers/CheckSalesControllerSpec.scala b/test/controllers/CheckSalesControllerSpec.scala index 7def4d91..b16912e6 100644 --- a/test/controllers/CheckSalesControllerSpec.scala +++ b/test/controllers/CheckSalesControllerSpec.scala @@ -59,7 +59,19 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi .set(SalesToCountryPage(index, index), salesValue).success.value .set(VatOnSalesPage(index, index), vatOnSalesValue).success.value + private val vatRateFromCountry = Gen.listOfN(1, arbitrary[VatRateFromCountry]).sample.value + private val remainingVatRateForCountry = List(arbitrary[VatRateFromCountry].sample.value) + + private val completeAnswers: UserAnswers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + .set(SoldToCountryPage(index), country).success.value + .set(VatRatesFromCountryPage(index, index), vatRateFromCountry).success.value + .set(SalesToCountryPage(index, index), salesValue).success.value + .set(VatOnSalesPage(index, index), vatOnSalesValue).success.value + private lazy val checkSalesRoute: String = CheckSalesPage(index, Some(index)).route(waypoints).url + private lazy val postCheckSalesRoute: String = controllers.routes.CheckSalesController.onSubmit(waypoints, index, incompletePromptShown = false).url + override def beforeEach(): Unit = { reset(mockVatRateService) @@ -70,9 +82,9 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi "must return OK and the correct view for a GET" in { - when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn vatRatesFromCountry + when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn remainingVatRateForCountry - val application = applicationBuilder(userAnswers = Some(baseAnswers)) + val application = applicationBuilder(userAnswers = Some(completeAnswers)) .overrides(bind[VatRateService].toInstance(mockVatRateService)) .build() @@ -85,7 +97,7 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi val view = application.injector.instanceOf[CheckSalesView] - val list = CheckSalesSummary.rows(baseAnswers, waypoints, index) + val list = CheckSalesSummary.rows(completeAnswers, waypoints, index) status(result) mustBe OK contentAsString(result) mustBe @@ -95,11 +107,9 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi "must populate the view correctly on a GET when the question has previously been answered" in { - val remainingVatRateForCountry = List(arbitrary[VatRateFromCountry].sample.value) - when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn remainingVatRateForCountry - val userAnswers = baseAnswers.set(CheckSalesPage(index), true).success.value + val userAnswers = completeAnswers.set(CheckSalesPage(index), true).success.value val application = applicationBuilder(userAnswers = Some(userAnswers)) .overrides(bind[VatRateService].toInstance(mockVatRateService)) @@ -153,7 +163,7 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi running(application) { val request = - FakeRequest(POST, checkSalesRoute) + FakeRequest(POST, postCheckSalesRoute) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value @@ -170,27 +180,27 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi "must save the answer and redirect to the next page when valid data is submitted" in { - when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn vatRatesFromCountry + when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn remainingVatRateForCountry val mockSessionRepository = mock[SessionRepository] when(mockSessionRepository.set(any())) thenReturn true.toFuture val application = - applicationBuilder(userAnswers = Some(baseAnswers)) + applicationBuilder(userAnswers = Some(completeAnswers)) .overrides(bind[SessionRepository].toInstance(mockSessionRepository)) .overrides(bind[VatRateService].toInstance(mockVatRateService)) .build() running(application) { val request = - FakeRequest(POST, checkSalesRoute) + FakeRequest(POST, postCheckSalesRoute) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value - val expectedAnswers = baseAnswers - .set(RemainingVatRatesFromCountryQuery(index), vatRatesFromCountry).success.value + val expectedAnswers = completeAnswers + .set(RemainingVatRatesFromCountryQuery(index), remainingVatRateForCountry).success.value .set(CheckSalesPage(index, Some(index)), true).success.value status(result) mustBe SEE_OTHER @@ -201,9 +211,9 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi "must return a Bad Request and errors when invalid data is submitted" in { - when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn vatRatesFromCountry + when(mockVatRateService.getRemainingVatRatesForCountry(any(), any(), any())) thenReturn remainingVatRateForCountry - val application = applicationBuilder(userAnswers = Some(baseAnswers)) + val application = applicationBuilder(userAnswers = Some(completeAnswers)) .overrides(bind[VatRateService].toInstance(mockVatRateService)) .build() @@ -211,7 +221,7 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi implicit val msgs: Messages = messages(application) val request = - FakeRequest(POST, checkSalesRoute) + FakeRequest(POST, postCheckSalesRoute) .withFormUrlEncodedBody(("value", "")) val boundForm = form.bind(Map("value" -> "")) @@ -220,11 +230,11 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi val result = route(application, request).value - val list = CheckSalesSummary.rows(baseAnswers, waypoints, index) + val list = CheckSalesSummary.rows(completeAnswers, waypoints, index) status(result) mustBe BAD_REQUEST contentAsString(result) mustBe - view(boundForm, waypoints, period, list, index, country, canAddAnotherVatRate = true)(request, messages(application)).toString + view(boundForm, waypoints, period, list, index, country, canAddAnotherVatRate = true, List.empty)(request, messages(application)).toString } } @@ -262,13 +272,13 @@ class CheckSalesControllerSpec extends SpecBase with MockitoSugar with SummaryLi running(application) { val request = - FakeRequest(POST, checkSalesRoute) + FakeRequest(POST, postCheckSalesRoute) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value status(result) mustBe SEE_OTHER - redirectLocation(result).value mustBe JourneyRecoveryPage.route(waypoints).url + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url } } } diff --git a/test/controllers/CheckYourAnswersControllerSpec.scala b/test/controllers/CheckYourAnswersControllerSpec.scala index 75193df1..44c4cb12 100644 --- a/test/controllers/CheckYourAnswersControllerSpec.scala +++ b/test/controllers/CheckYourAnswersControllerSpec.scala @@ -17,18 +17,20 @@ package controllers import base.SpecBase +import config.Constants.{maxCurrencyAmount, minCurrencyAmount} import connectors.{SaveForLaterConnector, SavedUserAnswers} import models.audit.{ReturnsAuditModel, SubmissionResult} import models.requests.DataRequest -import models.{Country, TotalVatToCountry, UserAnswers} +import models.{Country, TotalVatToCountry, UserAnswers, VatRateFromCountry} import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchersSugar.eqTo import org.mockito.Mockito import org.mockito.Mockito.{times, verify, when} +import org.scalacheck.Gen import org.scalatest.BeforeAndAfterEach import org.scalatestplus.mockito.MockitoSugar import pages.corrections._ -import pages.{CheckYourAnswersPage, SoldGoodsPage} +import pages.{CheckYourAnswersPage, SalesToCountryPage, SoldGoodsPage, SoldToCountryPage, VatRatesFromCountryPage} import play.api.i18n.Messages import play.api.inject.bind import play.api.test.FakeRequest @@ -52,6 +54,8 @@ class CheckYourAnswersControllerSpec extends SpecBase with MockitoSugar with Sum private val mockCoreVatReturnService = mock[CoreVatReturnService] private val mockAuditService = mock[AuditService] private val mockSaveForLaterConnector = mock[SaveForLaterConnector] + private val vatRateFromCountry: VatRateFromCountry = arbitraryVatRateFromCountry.arbitrary.sample.value + private val salesValue: BigDecimal = Gen.chooseNum(minCurrencyAmount, maxCurrencyAmount).sample.value override def beforeEach(): Unit = { Mockito.reset(mockSalesAtVatRateService) @@ -313,5 +317,148 @@ class CheckYourAnswersControllerSpec extends SpecBase with MockitoSugar with Sum } } + "when the user has not answered" - { + + "a question but the missing data prompt has not been shown, must refresh page" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = false).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.CheckYourAnswersController.onPageLoad(waypoints).url + } + } + + "country of consumption must redirect to SoldToCountryController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SoldToCountryController.onPageLoad(waypoints, index).url + } + } + + "vat rates, must redirect to VatRatesFromCountryController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + .set(SoldToCountryPage(index), Country.euCountries.head).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.VatRatesFromCountryController.onPageLoad(waypoints, index).url + } + } + + "net value of sales must redirect to SalesToCountryController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + .set(SoldToCountryPage(index), Country.euCountries.head).success.value + .set(VatRatesFromCountryPage(index, index), List[VatRateFromCountry](vatRateFromCountry)).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SalesToCountryController.onPageLoad(waypoints, index, index).url + } + } + + "vat on sales must redirect to VatOnSalesController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, true).success.value + .set(SoldToCountryPage(index), Country.euCountries.head).success.value + .set(VatRatesFromCountryPage(index, index), List[VatRateFromCountry](vatRateFromCountry)).success.value + .set(SalesToCountryPage(index, index), salesValue).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.VatOnSalesController.onPageLoad(waypoints, index, index).url + } + } + + "year of correct must redirect to CorrectionReturnYearController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, false).success.value + .set(CorrectPreviousReturnPage(0), true).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.corrections.routes.CorrectionReturnYearController.onPageLoad(waypoints, index).url + } + } + + "country of correction must redirect to CorrectionCountryController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, false).success.value + .set(CorrectPreviousReturnPage(0), true).success.value + .set(CorrectionReturnYearPage(index), period.year).success.value + .set(CorrectionReturnPeriodPage(index), period).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.corrections.routes.CorrectionCountryController.onPageLoad(waypoints, index, index).url + } + } + + "amount of correction must redirect to VatAmountCorrectionCountryController" in { + + val answers = emptyUserAnswers + .set(SoldGoodsPage, false).success.value + .set(CorrectPreviousReturnPage(0), true).success.value + .set(CorrectionReturnPeriodPage(index), period).success.value + .set(CorrectionCountryPage(index, index), Country.euCountries.head).success.value + + val app = applicationBuilder(Some(answers)).build() + + running(app) { + val request = FakeRequest(POST, routes.CheckYourAnswersController.onSubmit(waypoints, incompletePromptShown = true).url) + val result = route(app, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.corrections.routes.VatAmountCorrectionCountryController.onPageLoad(waypoints, index, index).url + } + } + } } } diff --git a/test/controllers/SoldToCountryListControllerSpec.scala b/test/controllers/SoldToCountryListControllerSpec.scala index c8948a20..9ced75ec 100644 --- a/test/controllers/SoldToCountryListControllerSpec.scala +++ b/test/controllers/SoldToCountryListControllerSpec.scala @@ -17,10 +17,12 @@ package controllers import base.SpecBase +import config.Constants.{maxCurrencyAmount, minCurrencyAmount} import forms.SoldToCountryListFormProvider import models.{Country, Index, UserAnswers, VatRateFromCountry} import org.mockito.ArgumentMatchers.{any, eq => eqTo} import org.mockito.Mockito.{times, verify, when} +import org.scalacheck.Gen import org.scalatestplus.mockito.MockitoSugar import pages.{JourneyRecoveryPage, SalesToCountryPage, SoldGoodsPage, SoldToCountryListPage, SoldToCountryPage, VatOnSalesPage, VatRatesFromCountryPage} import play.api.data.Form @@ -39,7 +41,7 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { private val country: Country = arbitraryCountry.arbitrary.sample.value private val vatRateFromCountry: VatRateFromCountry = arbitraryVatRateFromCountry.arbitrary.sample.value - private val salesValue: BigDecimal = BigDecimal(1234) + private val salesValue: BigDecimal = Gen.chooseNum(minCurrencyAmount, maxCurrencyAmount).sample.value private val baseAnswers: UserAnswers = emptyUserAnswers .set(SoldGoodsPage, true).success.value @@ -49,6 +51,7 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { .set(VatOnSalesPage(index, vatRateIndex), arbitraryVatOnSales.arbitrary.sample.value).success.value private lazy val soldToCountryListRoute: String = routes.SoldToCountryListController.onPageLoad(waypoints).url + private lazy val soldToCountryPostRoute: String = routes.SoldToCountryListController.onSubmit(waypoints, incompletePromptShown = false).url "SoldToCountryList Controller" - { @@ -95,7 +98,11 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { "must return OK and populate the view correctly on a GET when the maximum number of sold to countries have already been added" in { val userAnswers = (0 to Country.euCountriesWithNI.size).foldLeft(baseAnswers) { (userAnswers: UserAnswers, index: Int) => - userAnswers.set(SoldToCountryPage(Index(index)), country).success.value + userAnswers + .set(SoldToCountryPage(Index(index)), country).success.value + .set(VatRatesFromCountryPage(Index(index), Index(index)), List[VatRateFromCountry](vatRateFromCountry)).success.value + .set(SalesToCountryPage(Index(index), vatRateIndex), salesValue).success.value + .set(VatOnSalesPage(Index(index), vatRateIndex), arbitraryVatOnSales.arbitrary.sample.value).success.value } val application = applicationBuilder(userAnswers = Some(userAnswers)).build() @@ -118,7 +125,11 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { "must return OK and populate the view correctly on a GET when just below the maximum number of sold to countries have been added" in { val userAnswers = (0 until (Country.euCountriesWithNI.size -1)).foldLeft(baseAnswers) { (userAnswers: UserAnswers, index: Int) => - userAnswers.set(SoldToCountryPage(Index(index)), country).success.value + userAnswers + .set(SoldToCountryPage(Index(index)), country).success.value + .set(VatRatesFromCountryPage(Index(index), Index(index)), List[VatRateFromCountry](vatRateFromCountry)).success.value + .set(SalesToCountryPage(Index(index), vatRateIndex), salesValue).success.value + .set(VatOnSalesPage(Index(index), vatRateIndex), arbitraryVatOnSales.arbitrary.sample.value).success.value } val application = applicationBuilder(userAnswers = Some(userAnswers)).build() @@ -153,7 +164,7 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { running(application) { val request = - FakeRequest(POST, soldToCountryListRoute) + FakeRequest(POST, soldToCountryPostRoute) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value @@ -172,7 +183,7 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { running(application) { val request = - FakeRequest(POST, soldToCountryListRoute) + FakeRequest(POST, soldToCountryPostRoute) .withFormUrlEncodedBody(("value", "")) val boundForm = form.bind(Map("value" -> "")) @@ -222,7 +233,7 @@ class SoldToCountryListControllerSpec extends SpecBase with MockitoSugar { running(application) { val request = - FakeRequest(POST, soldToCountryListRoute) + FakeRequest(POST, soldToCountryPostRoute) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value diff --git a/test/controllers/StartReturnControllerSpec.scala b/test/controllers/StartReturnControllerSpec.scala index 941a8d83..6f9b3d43 100644 --- a/test/controllers/StartReturnControllerSpec.scala +++ b/test/controllers/StartReturnControllerSpec.scala @@ -20,10 +20,9 @@ import base.SpecBase import connectors.ReturnStatusConnector import forms.StartReturnFormProvider import models.SubmissionStatus.{Complete, Due, Excluded, Next, Overdue} -import models.{PartialReturnPeriod, Period, StandardPeriod, SubmissionStatus, WithName} -import models.etmp.{EtmpExclusion, EtmpObligationDetails} +import models.{PartialReturnPeriod, StandardPeriod, SubmissionStatus} +import models.etmp.EtmpExclusion import models.etmp.EtmpExclusionReason.NoLongerSupplies -import models.etmp.EtmpObligationsFulfilmentStatus.Open import org.mockito.ArgumentMatchers.any import org.mockito.{ArgumentMatchers, IdiomaticMockito, Mockito} import org.mockito.Mockito.when @@ -36,7 +35,7 @@ import play.api.data.Form import play.api.inject.bind import play.api.test.FakeRequest import play.api.test.Helpers._ -import services.{ObligationsService, PartialReturnPeriodService} +import services.PartialReturnPeriodService import viewmodels.yourAccount.{CurrentReturns, Return} import views.html.StartReturnView @@ -64,7 +63,6 @@ class StartReturnControllerSpec private val emptyCurrentReturns = CurrentReturns( returns = List.empty, - excluded = false, finalReturnsCompleted = false, completeOrExcludedReturns = List.empty ) @@ -166,7 +164,14 @@ class StartReturnControllerSpec val view = application.injector.instanceOf[StartReturnView] status(result) mustBe OK - contentAsString(result) mustBe view(form, waypoints, period, None, isFinalReturn = false, maybePartialReturn)(request, messages(application)).toString + contentAsString(result) mustBe view( + form, + waypoints, + period, + None, + isFinalReturn = false, + maybePartialReturn + )(request, messages(application)).toString } } } @@ -476,7 +481,7 @@ class StartReturnControllerSpec NoLongerSupplies, effectiveDate, effectiveDate, - false + quarantine = false ) val application = applicationBuilder( diff --git a/test/controllers/corrections/CorrectionListCountriesControllerSpec.scala b/test/controllers/corrections/CorrectionListCountriesControllerSpec.scala index 6ae2e613..53684cd0 100644 --- a/test/controllers/corrections/CorrectionListCountriesControllerSpec.scala +++ b/test/controllers/corrections/CorrectionListCountriesControllerSpec.scala @@ -46,6 +46,8 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu val form: Form[Boolean] = formProvider() lazy val correctionListCountriesRoute: String = controllers.corrections.routes.CorrectionListCountriesController.onPageLoad(waypoints, index).url + lazy val correctionListCountriesRoutePost: String = + controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, index, incompletePromptShown = false).url private val country = arbitrary[Country].sample.value private val obligationService: ObligationsService = mock[ObligationsService] @@ -62,6 +64,12 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu .set(CorrectionReturnPeriodPage(index), period).success.value .set(VatAmountCorrectionCountryPage(index, index), BigDecimal(100.0)).success.value + private val answersWithNoCorrectionValue = + emptyUserAnswers + .set(CorrectionCountryPage(index, index), country).success.value + .set(CorrectionReturnYearPage(index), 2023).success.value + .set(CorrectionReturnPeriodPage(index), period).success.value + "CorrectionListCountries Controller" - { "must return OK and the correct view for a GET" in { @@ -126,6 +134,37 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu } } + "must return OK and the correct view with missing data warning for a GET" in { + + when(obligationService.getFulfilledObligations(any())(any())) thenReturn etmpObligationDetails.toFuture + + val application = applicationBuilder(userAnswers = Some(answersWithNoCorrectionValue)) + .overrides(bind[ObligationsService].toInstance(obligationService)) + .build() + + running(application) { + implicit val msgs: Messages = messages(application) + val request = FakeRequest(GET, correctionListCountriesRoute) + + val result = route(application, request).value + + val view = application.injector.instanceOf[CorrectionListCountriesView] + val list = CorrectionListCountriesSummary.addToListRows(answersWithNoCorrectionValue, waypoints, index, CorrectionListCountriesPage(index)) + + status(result) mustEqual OK + contentAsString(result) mustEqual view( + form, + waypoints, + list, + period, + period, + index, + canAddCountries = true, + incompleteCountries = List(country.name) + )(request, messages(application)).toString + } + } + "must redirect to the next page when valid data is submitted" in { val mockSessionRepository = mock[SessionRepository] @@ -141,7 +180,7 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu .build() running(application) { - val request = FakeRequest(POST, correctionListCountriesRoute).withFormUrlEncodedBody(("value", "true")) + val request = FakeRequest(POST, correctionListCountriesRoutePost).withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value val expectedAnswers = emptyUserAnswers.set(CorrectionListCountriesPage(index), true).success.value @@ -162,7 +201,7 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu running(application) { implicit val msgs: Messages = messages(application) val request = - FakeRequest(POST, correctionListCountriesRoute) + FakeRequest(POST, correctionListCountriesRoutePost) .withFormUrlEncodedBody(("value", "")) val boundForm = form.bind(Map("value" -> "")) @@ -208,14 +247,49 @@ class CorrectionListCountriesControllerSpec extends SpecBase with SummaryListFlu running(application) { val request = - FakeRequest(POST, correctionListCountriesRoute) + FakeRequest(POST, correctionListCountriesRoutePost) .withFormUrlEncodedBody(("value", "true")) val result = route(application, request).value status(result) mustEqual SEE_OTHER - redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url + } + } + + "must refresh if there is incomplete data and the prompt has not been shown before" in { + + val application = applicationBuilder(Some(answersWithNoCorrectionValue)).build() + + running(application) { + val request = + FakeRequest(POST, correctionListCountriesRoutePost) + .withFormUrlEncodedBody(("value", "true")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.corrections.routes.CorrectionListCountriesController.onPageLoad(waypoints, index).url + } + } + + "must redirect to VatAmountCorrectionCountry if there is incomplete data and the prompt has been shown before" in { + + val application = applicationBuilder(userAnswers = Some(answersWithNoCorrectionValue)).build() + + running(application) { + val routePost = controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, index, incompletePromptShown = true).url + val request = + FakeRequest(POST, routePost) + .withFormUrlEncodedBody(("value", "true")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.corrections.routes.VatAmountCorrectionCountryController.onPageLoad(waypoints, index, index).url } } } + + }