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.heading")
- @messages("checkSales.heading")
+
@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")) + )) + } -@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_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 @@@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_prompt", incompleteCountries.head)
+ @formHelper(action = controllers.corrections.routes.CorrectionListCountriesController.onSubmit(waypoints, periodIndex, true), Symbol("autoComplete") -> "off") { +@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 } } } + + }