diff --git a/app/controllers/DeclareDutySuspendedReceivedController.scala b/app/controllers/DeclareDutySuspendedReceivedController.scala new file mode 100644 index 00000000..0bfa91f8 --- /dev/null +++ b/app/controllers/DeclareDutySuspendedReceivedController.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers + +import connectors.CacheConnector +import controllers.actions._ +import forms.DeclareDutySuspendedReceivedFormProvider +import javax.inject.Inject +import models.Mode +import navigation.Navigator +import pages.DeclareDutySuspendedReceivedPage +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import views.html.DeclareDutySuspendedReceivedView + +import scala.concurrent.{ExecutionContext, Future} + +class DeclareDutySuspendedReceivedController @Inject() ( + override val messagesApi: MessagesApi, + cacheConnector: CacheConnector, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: DeclareDutySuspendedReceivedFormProvider, + val controllerComponents: MessagesControllerComponents, + view: DeclareDutySuspendedReceivedView +)(implicit ec: ExecutionContext) + extends FrontendBaseController + with I18nSupport { + + val form = formProvider() + + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => + val preparedForm = request.userAnswers.get(DeclareDutySuspendedReceivedPage) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, mode)) + } + + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + form + .bindFromRequest() + .fold( + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), + value => + for { + updatedAnswers <- + Future.fromTry(request.userAnswers.set(DeclareDutySuspendedReceivedPage, value)) + _ <- cacheConnector.set(updatedAnswers) + } yield Redirect(navigator.nextPage(DeclareDutySuspendedReceivedPage, mode, updatedAnswers)) + ) + } +} diff --git a/app/forms/DeclareDutySuspendedReceivedFormProvider.scala b/app/forms/DeclareDutySuspendedReceivedFormProvider.scala new file mode 100644 index 00000000..50736217 --- /dev/null +++ b/app/forms/DeclareDutySuspendedReceivedFormProvider.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package forms + +import forms.mappings.Mappings +import javax.inject.Inject +import play.api.data.Form + +class DeclareDutySuspendedReceivedFormProvider @Inject() extends Mappings { + + def apply(): Form[BigDecimal] = + Form( + "declare-duty-suspended-received-input" -> bigDecimal( + "declareDutySuspendedReceived.error.required", + "declareDutySuspendedReceived.error.nonNumeric", + "declareDutySuspendedReceived.error.twoDecimalPlaces" + ) + .verifying(minimumValue(BigDecimal(0.00), "declareDutySuspendedReceived.error.minimumRequired")) + .verifying( + maximumValue(BigDecimal(999999999.99), "declareDutySuspendedReceived.error.maximumRequired") + ) + ) +} diff --git a/app/navigation/Navigator.scala b/app/navigation/Navigator.scala index 7f4d428c..06069ca0 100644 --- a/app/navigation/Navigator.scala +++ b/app/navigation/Navigator.scala @@ -32,6 +32,7 @@ class Navigator @Inject() () { case DraughtReliefQuestionPage => _ => routes.SmallProducerReliefQuestionController.onPageLoad(NormalMode) case DeclareDutySuspendedDeliveriesOutsideUkPage => _ => routes.DutySuspendedDeliveriesController.onPageLoad(NormalMode) + case DutySuspendedDeliveriesPage => _ => routes.DeclareDutySuspendedReceivedController.onPageLoad(NormalMode) case _ => _ => routes.IndexController.onPageLoad diff --git a/app/pages/DeclareDutySuspendedReceivedPage.scala b/app/pages/DeclareDutySuspendedReceivedPage.scala new file mode 100644 index 00000000..7deb9195 --- /dev/null +++ b/app/pages/DeclareDutySuspendedReceivedPage.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pages + +import play.api.libs.json.JsPath + +case object DeclareDutySuspendedReceivedPage extends QuestionPage[BigDecimal] { + + override def path: JsPath = JsPath \ toString + + override def toString: String = "declareDutySuspendedReceived" +} diff --git a/app/viewmodels/checkAnswers/DeclareDutySuspendedReceivedSummary.scala b/app/viewmodels/checkAnswers/DeclareDutySuspendedReceivedSummary.scala new file mode 100644 index 00000000..12c81f5f --- /dev/null +++ b/app/viewmodels/checkAnswers/DeclareDutySuspendedReceivedSummary.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package viewmodels.checkAnswers + +import controllers.routes +import models.{CheckMode, UserAnswers} +import pages.DeclareDutySuspendedReceivedPage +import play.api.i18n.Messages +import uk.gov.hmrc.govukfrontend.views.viewmodels.summarylist.SummaryListRow +import viewmodels.govuk.summarylist._ +import viewmodels.implicits._ + +object DeclareDutySuspendedReceivedSummary { + + def row(answers: UserAnswers)(implicit messages: Messages): Option[SummaryListRow] = + answers.get(DeclareDutySuspendedReceivedPage).map { answer => + SummaryListRowViewModel( + key = "declareDutySuspendedReceived.checkYourAnswersLabel", + value = ValueViewModel(answer.toString), + actions = Seq( + ActionItemViewModel("site.change", routes.DeclareDutySuspendedReceivedController.onPageLoad(CheckMode).url) + .withVisuallyHiddenText(messages("declareDutySuspendedReceived.change.hidden")) + ) + ) + } +} diff --git a/app/views/DeclareDutySuspendedReceivedView.scala.html b/app/views/DeclareDutySuspendedReceivedView.scala.html new file mode 100644 index 00000000..7a846314 --- /dev/null +++ b/app/views/DeclareDutySuspendedReceivedView.scala.html @@ -0,0 +1,71 @@ +@* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +@import viewmodels.InputWidth._ +@import components.{Link, SectionHeading, PageHeading} +@import uk.gov.hmrc.govukfrontend.views.viewmodels.content.Empty + +@this( + layout: templates.Layout, + formHelper: FormWithCSRF, + govukErrorSummary: GovukErrorSummary, + govukInput: GovukInput, + govukButton: GovukButton, + sectionHeading: SectionHeading, + pageHeading: PageHeading +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) + +@layout(pageTitle = title(form, messages("declareDutySuspendedReceived.title"))) { + + @formHelper(action = routes.DeclareDutySuspendedReceivedController.onSubmit(mode), Symbol("autoComplete") -> "off") { + + @if(form.errors.nonEmpty) { + @govukErrorSummary(ErrorSummaryViewModel(form)) + } + + @sectionHeading( + id = "declare-duty-suspended-received-section", + text = messages("section.alcoholDutyReturn"), + ) + + @pageHeading(messages("declareDutySuspendedReceived.heading")) + +

@messages("declareDutySuspendedReceived.p1")

+ +

@messages("declareDutySuspendedReceived.p2")

+ + @govukInput( + InputViewModel( + field = form("declare-duty-suspended-received-input"), + label = LabelViewModel(messages("declareDutySuspendedReceived.heading")).asVisuallyHidden(), + ) + .asNumeric() + .withWidth(Fixed10) + .withSuffix(PrefixOrSuffix(content = messages("declareDutySuspendedReceived.inputSuffix"))) + .withHint(Hint(content = messages("declareDutySuspendedReceived.hint"))) + + ) + + @govukButton( + ButtonViewModel("saveAndContinueButton", messages("site.saveAndContinue")) + ) + } +} diff --git a/conf/app.routes b/conf/app.routes index f3a3427f..262260c8 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -56,6 +56,11 @@ POST /declareDutySuspendedDeliveriesOutsideUk controllers GET /changeDeclareDutySuspendedDeliveriesOutsideUk controllers.DeclareDutySuspendedDeliveriesOutsideUkController.onPageLoad(mode: Mode = CheckMode) POST /changeDeclareDutySuspendedDeliveriesOutsideUk controllers.DeclareDutySuspendedDeliveriesOutsideUkController.onSubmit(mode: Mode = CheckMode) +GET /declareDutySuspendedReceived controllers.DeclareDutySuspendedReceivedController.onPageLoad(mode: Mode = NormalMode) +POST /declareDutySuspendedReceived controllers.DeclareDutySuspendedReceivedController.onSubmit(mode: Mode = NormalMode) +GET /changeDeclareDutySuspendedReceived controllers.DeclareDutySuspendedReceivedController.onPageLoad(mode: Mode = CheckMode) +POST /changeDeclareDutySuspendedReceived controllers.DeclareDutySuspendedReceivedController.onSubmit(mode: Mode = CheckMode) + GET /dutySuspendedDeliveries controllers.DutySuspendedDeliveriesController.onPageLoad(mode: Mode = NormalMode) POST /dutySuspendedDeliveries controllers.DutySuspendedDeliveriesController.onSubmit(mode: Mode = NormalMode) GET /dutySuspendedDeliveries controllers.DutySuspendedDeliveriesController.onPageLoad(mode: Mode = CheckMode) diff --git a/conf/messages.en b/conf/messages.en index 40bff52f..5d558dbb 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -122,6 +122,22 @@ declareDutySuspendedDeliveriesOutsideUk.inputSuffix = litres declareDutySuspendedDeliveriesOutsideUk.p1 = This includes to sites in EU countries, sites in countries outside of the EU and ship stores. This will not change your duty calculation. declareDutySuspendedDeliveriesOutsideUk.hint = Give your answers in litres, to 2 decimal places. +declareDutySuspendedReceived.title = How much have you received duty suspended? +declareDutySuspendedReceived.heading = How much have you received duty suspended? +declareDutySuspendedReceived.checkYourAnswersLabel = DeclareDutySuspendedReceived +declareDutySuspendedReceived.error.nonNumeric = How much you have received duty suspended must be a number +declareDutySuspendedReceived.error.required = Enter how much you have received duty suspended +declareDutySuspendedReceived.error.twoDecimalPlaces = How much you have received duty suspended must be a number to 2 decimal places +declareDutySuspendedReceived.inputSuffix = litres +declareDutySuspendedReceived.change.hidden = DeclareDutySuspendedReceived +declareDutySuspendedReceived.hint = Give your answers in litres, to 2 decimal places. +declareDutySuspendedReceived.error.minimumRequired = How much you have received duty suspended must be 0.00 or more +declareDutySuspendedReceived.error.maximumRequired = How much you have received duty suspended must be 999999999.99 or less +declareDutySuspendedReceived.p1 = For example, because: +declareDutySuspendedReceived.p2 = This will not change your duty calculation. +declareDutySuspendedReceived.para.list.1 = you received it from a UK site that is registered to send it duty suspended +declareDutySuspendedReceived.para.list.2 = the beer is imported and all duties have already been paid + dutySuspendedDeliveries.title = How much have you delivered duty suspended within the UK? dutySuspendedDeliveries.heading = How much have you delivered duty suspended within the UK? dutySuspendedDeliveries.p1 = For example, to UK sites that are registered to hold it duty suspended. This will not change your duty calculation. @@ -134,3 +150,4 @@ dutySuspendedDeliveries.error.minimumRequired = How much you have delivered duty dutySuspendedDeliveries.error.maximumRequired = How much you have delivered duty suspended must be 999999999.99 or less dutySuspendedDeliveries.error.twoDecimalPlaces = How much you have delivered duty suspended must be a number to 2 decimal places dutySuspendedDeliveries.change.hidden = Duty Suspended Deliveries + diff --git a/test/controllers/DeclareDutySuspendedReceivedControllerSpec.scala b/test/controllers/DeclareDutySuspendedReceivedControllerSpec.scala new file mode 100644 index 00000000..5d0565b0 --- /dev/null +++ b/test/controllers/DeclareDutySuspendedReceivedControllerSpec.scala @@ -0,0 +1,166 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers + +import base.SpecBase +import connectors.CacheConnector +import forms.DeclareDutySuspendedReceivedFormProvider +import models.{NormalMode, UserAnswers} +import navigation.{FakeNavigator, Navigator} +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import pages.DeclareDutySuspendedReceivedPage +import play.api.inject.bind +import play.api.mvc.Call +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import uk.gov.hmrc.http.HttpResponse +import views.html.DeclareDutySuspendedReceivedView + +import scala.concurrent.Future + +class DeclareDutySuspendedReceivedControllerSpec extends SpecBase with MockitoSugar { + + val formProvider = new DeclareDutySuspendedReceivedFormProvider() + val form = formProvider() + + def onwardRoute = Call("GET", "/foo") + + val validAnswer = BigDecimal(10.23) + + lazy val declareDutySuspendedReceivedRoute = + routes.DeclareDutySuspendedReceivedController.onPageLoad(NormalMode).url + + "DeclareDutySuspendedReceived Controller" - { + + "must return OK and the correct view for a GET" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + running(application) { + val request = FakeRequest(GET, declareDutySuspendedReceivedRoute) + + val result = route(application, request).value + + val view = application.injector.instanceOf[DeclareDutySuspendedReceivedView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form, NormalMode)(request, messages(application)).toString + } + } + + "must populate the view correctly on a GET when the question has previously been answered" in { + + val userAnswers = + UserAnswers(userAnswersId).set(DeclareDutySuspendedReceivedPage, validAnswer).success.value + + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() + + running(application) { + val request = FakeRequest(GET, declareDutySuspendedReceivedRoute) + + val view = application.injector.instanceOf[DeclareDutySuspendedReceivedView] + + val result = route(application, request).value + + status(result) mustEqual OK + contentAsString(result) mustEqual view(form.fill(validAnswer), NormalMode)( + request, + messages(application) + ).toString + } + } + + "must redirect to the next page when valid data is submitted" in { + + val mockCacheConnector = mock[CacheConnector] + + when(mockCacheConnector.set(any())(any())) thenReturn Future.successful(mock[HttpResponse]) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[CacheConnector].toInstance(mockCacheConnector) + ) + .build() + + running(application) { + val request = + FakeRequest(POST, declareDutySuspendedReceivedRoute) + .withFormUrlEncodedBody(("declare-duty-suspended-received-input", validAnswer.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url + } + } + + "must return a Bad Request and errors when invalid data is submitted" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + running(application) { + val request = + FakeRequest(POST, declareDutySuspendedReceivedRoute) + .withFormUrlEncodedBody(("value", "invalid value")) + + val boundForm = form.bind(Map("value" -> "invalid value")) + + val view = application.injector.instanceOf[DeclareDutySuspendedReceivedView] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + contentAsString(result) mustEqual view(boundForm, NormalMode)(request, messages(application)).toString + } + } + + "must redirect to Journey Recovery for a GET if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + running(application) { + val request = FakeRequest(GET, declareDutySuspendedReceivedRoute) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + } + } + + "must redirect to Journey Recovery for a POST if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + running(application) { + val request = + FakeRequest(POST, declareDutySuspendedReceivedRoute) + .withFormUrlEncodedBody(("value", validAnswer.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.JourneyRecoveryController.onPageLoad().url + } + } + } +} diff --git a/test/forms/DeclareDutySuspendedReceivedFormProviderSpec.scala b/test/forms/DeclareDutySuspendedReceivedFormProviderSpec.scala new file mode 100644 index 00000000..f91f2c5e --- /dev/null +++ b/test/forms/DeclareDutySuspendedReceivedFormProviderSpec.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package forms + +import forms.behaviours.BigDecimalFieldBehaviours +import play.api.data.FormError +import scala.collection.immutable.ArraySeq + +class DDeclareDutySuspendedReceivedFormProviderSpec extends BigDecimalFieldBehaviours { + + val form = new DeclareDutySuspendedReceivedFormProvider()() + + ".value" - { + + val fieldName = "declare-duty-suspended-received-input" + + val minimum = 0.00 + val maximum = 999999999.99 + + val validDataGenerator = bigDecimalsInRangeWithCommas(minimum, maximum) + + behave like fieldThatBindsValidData( + form, + fieldName, + validDataGenerator + ) + + behave like bigDecimalField( + form, + fieldName, + nonNumericError = FormError(fieldName, "declareDutySuspendedReceived.error.nonNumeric"), + twoDecimalPlacesError = FormError(fieldName, "declareDutySuspendedReceived.error.twoDecimalPlaces") + ) + + behave like bigDecimalFieldWithMinimum( + form, + fieldName, + minimum = minimum, + expectedError = FormError(fieldName, "declareDutySuspendedReceived.error.minimumRequired", ArraySeq(minimum)) + ) + + behave like bigDecimalFieldWithMaximum( + form, + fieldName, + maximum = maximum, + expectedError = FormError(fieldName, "declareDutySuspendedReceived.error.maximumRequired", ArraySeq(maximum)) + ) + + behave like mandatoryField( + form, + fieldName, + requiredError = FormError(fieldName, "declareDutySuspendedReceived.error.required") + ) + } +} diff --git a/test/navigation/NavigatorSpec.scala b/test/navigation/NavigatorSpec.scala index 47efb5c7..3c17417b 100644 --- a/test/navigation/NavigatorSpec.scala +++ b/test/navigation/NavigatorSpec.scala @@ -70,6 +70,15 @@ class NavigatorSpec extends SpecBase { UserAnswers("id") ) mustBe routes.DutySuspendedDeliveriesController.onPageLoad(NormalMode) } + + "must go from theDeclare duty suspended deliveries inside UK page to Declare duty suspended received page" in { + + navigator.nextPage( + DutySuspendedDeliveriesPage, + NormalMode, + UserAnswers("id") + ) mustBe routes.DeclareDutySuspendedReceivedController.onPageLoad(NormalMode) + } } "in Check mode" - {