diff --git a/app/controllers/returns/ViewReturnController.scala b/app/controllers/returns/ViewReturnController.scala index 6ccc7a0b..52261d17 100644 --- a/app/controllers/returns/ViewReturnController.scala +++ b/app/controllers/returns/ViewReturnController.scala @@ -28,7 +28,7 @@ import viewmodels.returns.ViewReturnViewModel import views.html.returns.ViewReturnView import javax.inject.Inject -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} class ViewReturnController @Inject() ( override val messagesApi: MessagesApi, @@ -47,50 +47,68 @@ class ViewReturnController @Inject() ( def onPageLoad(periodKey: String): Action[AnyContent] = identify.async { implicit request => val appaId = request.appaId - (for { - returnDetails <- alcoholDutyReturnsConnector.getReturn(appaId, periodKey) - periodKey = returnDetails.identification.periodKey - returnPeriodsAndTaxCodes = - returnDetails.alcoholDeclared.taxCodes.map((periodKey, _)) ++ returnDetails.adjustments.returnPeriodsAndTaxCodes - ratePeriodsAndTaxCodes = returnPeriodsAndTaxCodes.flatMap { case (periodKey, taxCode) => - ReturnPeriod - .fromPeriodKey(periodKey) - .map(returnPeriod => (returnPeriod.period, taxCode)) - } - ratePeriodsAndTaxCodesToRateBands <- calculatorConnector.rateBands(ratePeriodsAndTaxCodes) - } yield ReturnPeriod + ReturnPeriod .fromPeriodKey(periodKey) .fold { logger.warn(s"Cannot parse period key $periodKey for $appaId on return") - Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + Future.successful(Redirect(controllers.routes.JourneyRecoveryController.onPageLoad())) } { returnPeriod => - val dutyToDeclareViewModel = - viewModel.createAlcoholDeclaredViewModel(returnDetails, ratePeriodsAndTaxCodesToRateBands) - val adjustmentsViewModel = - viewModel.createAdjustmentsViewModel(returnDetails, ratePeriodsAndTaxCodesToRateBands) - val totalDueViewModel = viewModel.createTotalDueViewModel(returnDetails) - val netDutySuspension = viewModel.createNetDutySuspensionViewModel(returnDetails) - val returnPeriodStr = dateTimeHelper.formatMonthYear(returnPeriod.period) - val submittedDate = dateTimeHelper.instantToLocalDate(returnDetails.identification.submittedTime) - val submittedDateStr = dateTimeHelper.formatDateMonthYear(submittedDate) - val submittedTime = dateTimeHelper.instantToLocalTime(returnDetails.identification.submittedTime) - val submittedTimeStr = dateTimeHelper.formatHourMinuteMeridiem(submittedTime) + (for { + returnDetails <- alcoholDutyReturnsConnector.getReturn(appaId, periodKey) + _ <- if (returnDetails.identification.periodKey != periodKey) { + val error = + s"Period key on the return ${returnDetails.identification.periodKey} does not match the return $periodKey requested for $appaId" + logger.warn(error) + Future.failed(new RuntimeException(error)) + } else { + Future.unit + } + periodKey = returnDetails.identification.periodKey + returnPeriodsAndTaxCodes = + returnDetails.alcoholDeclared.taxCodes.map( + (periodKey, _) + ) ++ returnDetails.adjustments.returnPeriodsAndTaxCodes + ratePeriodsAndTaxCodes = returnPeriodsAndTaxCodes.flatMap { case (periodKey, taxCode) => + ReturnPeriod + .fromPeriodKey(periodKey) + .map(returnPeriod => (returnPeriod.period, taxCode)) + } + ratePeriodsAndTaxCodesToRateBands <- calculatorConnector.rateBands(ratePeriodsAndTaxCodes) + } yield { + val dutyToDeclareViewModel = + viewModel.createAlcoholDeclaredViewModel(returnDetails, ratePeriodsAndTaxCodesToRateBands) + val adjustmentsViewModel = + viewModel.createAdjustmentsViewModel(returnDetails, ratePeriodsAndTaxCodesToRateBands) + val totalDueViewModel = viewModel.createTotalDueViewModel(returnDetails) + val netDutySuspension = viewModel.createNetDutySuspensionViewModel(returnDetails) + val spirits = if (returnPeriod.hasQuarterlySpirits) { + viewModel.createSpiritsViewModels(returnDetails) + } else { + Seq.empty + } + val returnPeriodStr = dateTimeHelper.formatMonthYear(returnPeriod.period) + val submittedDate = dateTimeHelper.instantToLocalDate(returnDetails.identification.submittedTime) + val submittedDateStr = dateTimeHelper.formatDateMonthYear(submittedDate) + val submittedTime = dateTimeHelper.instantToLocalTime(returnDetails.identification.submittedTime) + val submittedTimeStr = dateTimeHelper.formatHourMinuteMeridiem(submittedTime) - Ok( - view( - returnPeriodStr, - submittedDateStr, - submittedTimeStr, - dutyToDeclareViewModel, - adjustmentsViewModel, - totalDueViewModel, - netDutySuspension + Ok( + view( + returnPeriodStr, + submittedDateStr, + submittedTimeStr, + dutyToDeclareViewModel, + adjustmentsViewModel, + totalDueViewModel, + netDutySuspension, + spirits + ) ) - ) - }) - .recover { case _ => - logger.warn(s"Unable to fetch return $appaId $periodKey") - Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + }) + .recover { case e => + logger.warn(s"Unable to fetch return $appaId $periodKey: ${e.getMessage}") + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + } } } } diff --git a/app/models/checkAndSubmit/AdrReturnSubmission.scala b/app/models/checkAndSubmit/AdrReturnSubmission.scala index cd3ce27c..20beee26 100644 --- a/app/models/checkAndSubmit/AdrReturnSubmission.scala +++ b/app/models/checkAndSubmit/AdrReturnSubmission.scala @@ -136,8 +136,8 @@ object AdrDutySuspended { case class AdrSpiritsVolumes( totalSpirits: BigDecimal, - scotchWhiskey: BigDecimal, - irishWhisky: BigDecimal + scotchWhisky: BigDecimal, + irishWhiskey: BigDecimal ) object AdrSpiritsVolumes { diff --git a/app/models/returns/ReturnDetails.scala b/app/models/returns/ReturnDetails.scala index 481745dd..abdb251e 100644 --- a/app/models/returns/ReturnDetails.scala +++ b/app/models/returns/ReturnDetails.scala @@ -16,6 +16,7 @@ package models.returns +import models.checkAndSubmit.AdrTypeOfSpirit import play.api.libs.json.{Json, OFormat} import java.time.Instant @@ -25,7 +26,8 @@ case class ReturnDetails( alcoholDeclared: ReturnAlcoholDeclared, adjustments: ReturnAdjustments, totalDutyDue: ReturnTotalDutyDue, - netDutySuspension: Option[ReturnNetDutySuspension] + netDutySuspension: Option[ReturnNetDutySuspension], + spirits: Option[ReturnSpirits] ) object ReturnDetails { @@ -134,3 +136,23 @@ case class ReturnNetDutySuspension( object ReturnNetDutySuspension { implicit val returnTotalDutyDueFormat: OFormat[ReturnNetDutySuspension] = Json.format[ReturnNetDutySuspension] } + +case class ReturnSpiritsVolumes( + totalSpirits: BigDecimal, + scotchWhisky: BigDecimal, + irishWhiskey: BigDecimal +) + +case object ReturnSpiritsVolumes { + implicit val returnSpiritsVolumesFormat: OFormat[ReturnSpiritsVolumes] = Json.format[ReturnSpiritsVolumes] +} + +case class ReturnSpirits( + spiritsVolumes: ReturnSpiritsVolumes, + typesOfSpirit: Set[AdrTypeOfSpirit], + otherSpiritTypeName: Option[String] +) + +case object ReturnSpirits { + implicit val returnSpiritsFormat: OFormat[ReturnSpirits] = Json.format[ReturnSpirits] +} diff --git a/app/services/checkAndSubmit/AdrReturnSubmissionService.scala b/app/services/checkAndSubmit/AdrReturnSubmissionService.scala index 4f50d0b3..e283060b 100644 --- a/app/services/checkAndSubmit/AdrReturnSubmissionService.scala +++ b/app/services/checkAndSubmit/AdrReturnSubmissionService.scala @@ -378,11 +378,11 @@ class AdrReturnSubmissionServiceImpl @Inject() ( def declareSpiritsTotal(userAnswers: UserAnswers): EitherT[Future, String, AdrSpiritsVolumes] = for { totalSpirits <- getValue(userAnswers, DeclareSpiritsTotalPage) - whiskey <- getValue(userAnswers, WhiskyPage) + whisky <- getValue(userAnswers, WhiskyPage) } yield AdrSpiritsVolumes( totalSpirits = totalSpirits, - scotchWhiskey = whiskey.scotchWhisky, - irishWhisky = whiskey.irishWhiskey + scotchWhisky = whisky.scotchWhisky, + irishWhiskey = whisky.irishWhiskey ) def getTypeOfSpirits(userAnswers: UserAnswers): EitherT[Future, String, Set[AdrTypeOfSpirit]] = diff --git a/app/viewmodels/TableViewModel.scala b/app/viewmodels/TableViewModel.scala index f17591a3..a65e2c71 100644 --- a/app/viewmodels/TableViewModel.scala +++ b/app/viewmodels/TableViewModel.scala @@ -22,12 +22,14 @@ import uk.gov.hmrc.govukfrontend.views.viewmodels.table.{HeadCell, TableRow} case class TableViewModel( head: Seq[HeadCell], rows: Seq[TableRowViewModel], - total: Option[TableTotalViewModel] = None + total: Option[TableTotalViewModel] = None, + caption: Option[String] = None ) object TableViewModel { - def empty(): TableViewModel = TableViewModel(Seq.empty, Seq.empty, None) + def empty(): TableViewModel = TableViewModel(Seq.empty, Seq.empty, None, None) } + case class TableRowViewModel(cells: Seq[TableRow], actions: Seq[TableRowActionViewModel] = Seq.empty) case class TableTotalViewModel(legend: HeadCell, total: HeadCell) { diff --git a/app/viewmodels/returns/ViewReturnViewModel.scala b/app/viewmodels/returns/ViewReturnViewModel.scala index 69484f17..25121c85 100644 --- a/app/viewmodels/returns/ViewReturnViewModel.scala +++ b/app/viewmodels/returns/ViewReturnViewModel.scala @@ -18,6 +18,8 @@ package viewmodels.returns import config.Constants.Css import config.FrontendAppConfig +import models.checkAndSubmit.AdrTypeOfSpirit +import models.checkAndSubmit.AdrTypeOfSpirit._ import models.returns._ import models.{RateBand, ReturnPeriod} import play.api.i18n.Messages @@ -361,4 +363,111 @@ class ViewReturnViewModel @Inject() (appConfig: FrontendAppConfig) { content = Text(messages("viewReturn.table.description.legend")) ) ) + + def createSpiritsViewModels( + returnDetails: ReturnDetails + )(implicit messages: Messages): Seq[TableViewModel] = + returnDetails.spirits match { + case Some(spirits) => + Seq( + TableViewModel( + head = spiritsDeclaredTableHeader(), + rows = spiritsDeclaredRows(spirits), + caption = Some(messages("viewReturn.spirits.caption")) + ), + TableViewModel( + head = spiritsTypesDeclaredTableHeader(), + rows = spiritsTypesDeclaredRows(spirits) + ) + ) + case None => + Seq( + TableViewModel( + head = spiritsNotDeclaredTableHeader(), + rows = spiritsNotDeclaredRow(), + caption = Some(messages("viewReturn.spirits.caption")) + ) + ) + } + + private def spiritsDeclaredTableHeader()(implicit messages: Messages): Seq[HeadCell] = + Seq( + HeadCell( + content = Text(messages("viewReturn.table.description.legend")) + ), + HeadCell( + content = Text(messages("viewReturn.table.totalVolume.lpa.legend")), + classes = Css.textAlignRightWrapCssClass + ) + ) + + private def spiritsDeclaredRows(spirits: ReturnSpirits)(implicit messages: Messages): Seq[TableRowViewModel] = + Seq( + ("viewReturn.spirits.totalVolume", spirits.spiritsVolumes.totalSpirits), + ("viewReturn.spirits.scotchWhisky", spirits.spiritsVolumes.scotchWhisky), + ("viewReturn.spirits.irishWhiskey", spirits.spiritsVolumes.irishWhiskey) + ).map { case (legendKey, value) => + TableRowViewModel( + cells = Seq( + TableRow(content = Text(messages(legendKey))), + TableRow( + content = Text(messages("site.2DP", value)), + classes = s"${Css.textAlignRightCssClass} ${Css.numericCellClass}" + ) + ) + ) + } + + private def spiritsTypesDeclaredTableHeader()(implicit messages: Messages): Seq[HeadCell] = + Seq( + HeadCell( + content = Text(messages("viewReturn.table.typesOfSpirits.legend")) + ) + ) + + private val spiritsTypeToMessageKey: Map[AdrTypeOfSpirit, String] = + Map( + Malt -> "viewReturn.spirits.type.malt", + Grain -> "viewReturn.spirits.type.grain", + NeutralAgricultural -> "viewReturn.spirits.type.neutralAgricultural", + NeutralIndustrial -> "viewReturn.spirits.type.neutralIndustrial", + Beer -> "viewReturn.spirits.type.beer", + CiderOrPerry -> "viewReturn.spirits.type.cider", + WineOrMadeWine -> "viewReturn.spirits.type.wine" + ) + + private def spiritsTypesDeclaredRows(spirits: ReturnSpirits)(implicit messages: Messages): Seq[TableRowViewModel] = { + val typesOfSpirit = spirits.typesOfSpirit + + val spiritsTypesDetails = AdrTypeOfSpirit.values + .flatMap { + case Other if typesOfSpirit.contains(Other) => spirits.otherSpiritTypeName + case typeOfSpirit if typesOfSpirit.contains(typeOfSpirit) => + spiritsTypeToMessageKey.get(typeOfSpirit).map(messages(_)) + case _ => None + } + .mkString(", ") + + Seq( + TableRowViewModel( + cells = Seq( + TableRow(content = Text(spiritsTypesDetails)) + ) + ) + ) + } + + private def spiritsNotDeclaredTableHeader()(implicit messages: Messages): Seq[HeadCell] = Seq( + HeadCell( + content = Text(messages("viewReturn.table.description.legend")) + ) + ) + + private def spiritsNotDeclaredRow()(implicit messages: Messages) = Seq( + TableRowViewModel( + cells = Seq( + TableRow(content = Text(messages("viewReturn.spirits.noneDeclared"))) + ) + ) + ) } diff --git a/app/views/returns/ViewReturnView.scala.html b/app/views/returns/ViewReturnView.scala.html index 97a0020f..91b41505 100644 --- a/app/views/returns/ViewReturnView.scala.html +++ b/app/views/returns/ViewReturnView.scala.html @@ -28,7 +28,16 @@ printPage: PrintPage ) -@(period: String, submittedAtDate: String, submittedAtTime: String, dutyToDeclare: TableViewModel, adjustments: TableViewModel, totalDue: TableTotalViewModel, netDutySuspension: TableViewModel)(implicit request: Request[_], messages: Messages) +@( + period: String, + submittedAtDate: String, + submittedAtTime: String, + dutyToDeclare: TableViewModel, + adjustments: TableViewModel, + totalDue: TableTotalViewModel, + netDutySuspension: TableViewModel, + spirits: Seq[TableViewModel] +)(implicit request: Request[_], messages: Messages) @layout(pageTitle = titleNoForm(messages("viewReturn.title", period)), fullWidth = true, withPrintCss = true) { @@ -85,5 +94,16 @@
+ @spirits.map { spiritsTable => + @govukTable(Table( + caption = spiritsTable.caption, + captionClasses = Css.tableCaptionMCssClass, + head = Some(spiritsTable.head), + rows = spiritsTable.rows.map(_.cells) + )) + } + +
+ @printPage("print-past-payments-link", messages("viewReturn.printYourReturn")) } \ No newline at end of file diff --git a/conf/messages.en b/conf/messages.en index f338655b..5e193bbf 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -140,16 +140,30 @@ viewReturn.adjustments.type.spoilt = Spoilt viewReturn.adjustments.type.drawback = Drawback viewReturn.dutyDue.caption = Total viewReturn.dutyDue.total.legend = Total duty value +viewReturn.netDutySuspension.caption = Duty suspended deliveries +viewReturn.netDutySuspension.noneDeclared = Nothing declared +viewReturn.spirits.caption = Spirits production in the last 3 months +viewReturn.spirits.totalVolume = Total volume of spirits +viewReturn.spirits.scotchWhisky = Scotch whisky +viewReturn.spirits.irishWhiskey = Irish whiskey +viewReturn.spirits.type.malt = Malt spirit +viewReturn.spirits.type.grain = Grain spirit +viewReturn.spirits.type.neutralAgricultural = Neutral spirit (agricultural origin) +viewReturn.spirits.type.neutralIndustrial = Neutral spirit (industrial origin) +viewReturn.spirits.type.beer = Beer-based spirit +viewReturn.spirits.type.wine = Wine or made-wine-based spirit +viewReturn.spirits.type.cider = Cider or perry-based spirit +viewReturn.spirits.noneDeclared = Nothing declared viewReturn.table.adjustmentType.legend = Adjustment viewReturn.table.description.legend = Description viewReturn.table.totalVolume.legend = Total volume (litres) viewReturn.table.lpa.legend = Litres of pure alcohol (LPA) +viewReturn.table.totalVolume.lpa.legend = Total volume (LPA) viewReturn.table.dutyRate.legend = Duty rate (per litre) viewReturn.table.dutyValue.legend = Duty value viewReturn.table.dutyDue.legend = Duty value +viewReturn.table.typesOfSpirits.legend = Types of spirits produced viewReturn.printYourReturn = Print your return -viewReturn.netDutySuspension.caption = Duty suspended deliveries -viewReturn.netDutySuspension.noneDeclared = Nothing declared viewPastPayments.heading = Alcohol Duty payments viewPastPayments.title = Alcohol Duty payments diff --git a/test-utils/common/TestData.scala b/test-utils/common/TestData.scala index 9df58bad..5b45551c 100644 --- a/test-utils/common/TestData.scala +++ b/test-utils/common/TestData.scala @@ -275,6 +275,17 @@ trait TestData extends ModelGenerators { totalLtsPureAlcoholWine = Some(BigDecimal("0.5965")), totalLtsPureAlcoholOtherFermented = Some(BigDecimal("0.1894")) ) + ), + spirits = Some( + ReturnSpirits( + ReturnSpiritsVolumes( + totalSpirits = BigDecimal("0.05"), + scotchWhisky = BigDecimal("0.26"), + irishWhiskey = BigDecimal("0.16") + ), + typesOfSpirit = Set(AdrTypeOfSpirit.NeutralAgricultural), + otherSpiritTypeName = Some("Coco Pops Vodka") + ) ) ) } @@ -291,7 +302,8 @@ trait TestData extends ModelGenerators { total = BigDecimal("0") ), totalDutyDue = ReturnTotalDutyDue(totalDue = BigDecimal("0")), - netDutySuspension = None + netDutySuspension = None, + spirits = None ) def nilReturnDetailsWithEmptySections(periodKey: String, now: Instant): ReturnDetails = @@ -306,7 +318,8 @@ trait TestData extends ModelGenerators { total = BigDecimal("0") ), totalDutyDue = ReturnTotalDutyDue(totalDue = BigDecimal("0")), - netDutySuspension = None + netDutySuspension = None, + spirits = None ) def returnWithSpoiltAdjustment(periodKey: String, now: Instant): ReturnDetails = { @@ -341,7 +354,8 @@ trait TestData extends ModelGenerators { total = BigDecimal("-3151.50") ), totalDutyDue = ReturnTotalDutyDue(totalDue = BigDecimal("-6303.00")), - netDutySuspension = None + netDutySuspension = None, + spirits = None ) } diff --git a/test/controllers/returns/ViewReturnControllerSpec.scala b/test/controllers/returns/ViewReturnControllerSpec.scala index eb35fd57..d30dad7a 100644 --- a/test/controllers/returns/ViewReturnControllerSpec.scala +++ b/test/controllers/returns/ViewReturnControllerSpec.scala @@ -18,6 +18,7 @@ package controllers.returns import base.SpecBase import connectors.{AlcoholDutyCalculatorConnector, AlcoholDutyReturnsConnector} +import models.ReturnPeriod import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchersSugar.eqTo import play.api.inject.bind @@ -32,8 +33,63 @@ import scala.concurrent.Future class ViewReturnControllerSpec extends SpecBase { "ViewReturnController" - { - "should return a view if able to fetch the return" in new SetUp { - when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKey))(any)) + "should return a view if able to fetch the return and a spirits month" in new SetUp { + override def periodKeyUnderTest: String = periodKeyForSpirits + + when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKeyUnderTest))(any)) + .thenReturn(Future.successful(returnDetails)) + when(mockCalculatorConnector.rateBands(any())(any)) + .thenReturn(Future.successful(rateBands)) + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[AlcoholDutyReturnsConnector].toInstance(mockReturnsConnector)) + .overrides(bind[AlcoholDutyCalculatorConnector].toInstance(mockCalculatorConnector)) + .overrides(bind[ViewReturnViewModel].toInstance(mockViewModel)) + .build() + running(application) { + implicit val messages = getMessages(application) + + when(mockViewModel.createTotalDueViewModel(returnDetails)).thenReturn(totalTableModel) + when(mockViewModel.createAlcoholDeclaredViewModel(eqTo(returnDetails), any())(any())) + .thenReturn(tableModel) + when(mockViewModel.createAdjustmentsViewModel(eqTo(returnDetails), any())(any())) + .thenReturn(tableModel) + when(mockViewModel.createNetDutySuspensionViewModel(eqTo(returnDetails))(any())).thenReturn(tableModel) + when(mockViewModel.createSpiritsViewModels(eqTo(returnDetails))(any())).thenReturn(Seq(tableModel)) + when(mockViewModel.createAlcoholDeclaredViewModel(eqTo(returnDetails), any())(any())) + .thenReturn(tableModel) + when(mockViewModel.createAdjustmentsViewModel(eqTo(returnDetails), any())(any())) + .thenReturn(tableModel) + + val request = + FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKeyUnderTest).url) + val result = route(application, request).value + + val view = application.injector.instanceOf[ViewReturnView] + + status(result) mustEqual OK + contentAsString(result) mustEqual view( + returnPeriodStr, + submittedAtDateStr, + submittedAtTimeStr, + tableModel, + tableModel, + totalTableModel, + tableModel, + Seq(tableModel) + )( + request, + messages + ).toString + } + + verify(mockViewModel, times(1)).createSpiritsViewModels(any)(any) + } + + "should return a view if able to fetch the return and not a spirits month" in new SetUp { + override def periodKeyUnderTest: String = periodKeyNotForSpirits + + when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKeyUnderTest))(any)) .thenReturn(Future.successful(returnDetails)) when(mockCalculatorConnector.rateBands(any())(any)) .thenReturn(Future.successful(rateBands)) @@ -43,6 +99,7 @@ class ViewReturnControllerSpec extends SpecBase { .overrides(bind[AlcoholDutyCalculatorConnector].toInstance(mockCalculatorConnector)) .overrides(bind[ViewReturnViewModel].toInstance(mockViewModel)) .build() + running(application) { implicit val messages = getMessages(application) @@ -52,12 +109,14 @@ class ViewReturnControllerSpec extends SpecBase { when(mockViewModel.createAdjustmentsViewModel(eqTo(returnDetails), any())(any())) .thenReturn(tableModel) when(mockViewModel.createNetDutySuspensionViewModel(returnDetails)).thenReturn(tableModel) + when(mockViewModel.createSpiritsViewModels(returnDetails)).thenReturn(Seq(tableModel)) when(mockViewModel.createAlcoholDeclaredViewModel(eqTo(returnDetails), any())(any())) .thenReturn(tableModel) when(mockViewModel.createAdjustmentsViewModel(eqTo(returnDetails), any())(any())) .thenReturn(tableModel) - val request = FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKey).url) + val request = + FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKeyUnderTest).url) val result = route(application, request).value val view = application.injector.instanceOf[ViewReturnView] @@ -70,63 +129,101 @@ class ViewReturnControllerSpec extends SpecBase { tableModel, tableModel, totalTableModel, - tableModel + tableModel, + Seq.empty )( request, messages ).toString } + + verify(mockViewModel, never).createSpiritsViewModels(any)(any) + } + + "should redirect to the journey recovery page if unable to parse the period key" in new SetUp { + override def periodKeyUnderTest: String = periodKeyForSpirits + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides(bind[AlcoholDutyReturnsConnector].toInstance(mockReturnsConnector)) + .overrides(bind[AlcoholDutyCalculatorConnector].toInstance(mockCalculatorConnector)) + .build() + + running(application) { + val request = FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(badPeriodKey).url) + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + verify(mockReturnsConnector, never).getReturn(any, any)(any) } "should redirect to the journey recovery page if unable to fetch the return" in new SetUp { - when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKey))(any)) + override def periodKeyUnderTest: String = periodKeyForSpirits + + when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKeyUnderTest))(any)) .thenReturn(Future.failed(new IllegalArgumentException("error"))) val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[AlcoholDutyReturnsConnector].toInstance(mockReturnsConnector)) .build() running(application) { - val request = FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKey).url) + val request = + FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKeyUnderTest).url) val result = route(application, request).value status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url } + + verify(mockReturnsConnector, times(1)).getReturn(any, any)(any) + verify(mockCalculatorConnector, never).rateBands(any())(any) } - "should redirect to the journey recovery page if unable to parse the returned period key" in new SetUp { - when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKey))(any)) - .thenReturn(Future.successful(returnDetailsWithBadPeriodKey)) + "should redirect to the journey recovery page if the period key on the return doesn't match that of the request" in new SetUp { + override def periodKeyUnderTest: String = periodKeyForSpirits + + when(mockReturnsConnector.getReturn(eqTo(appaId), eqTo(periodKeyUnderTest))(any)) + .thenReturn( + Future.successful( + returnDetails.copy(identification = returnDetails.identification.copy(periodKey = periodKeyNotForSpirits)) + ) + ) when(mockCalculatorConnector.rateBands(any())(any)) .thenReturn(Future.successful(rateBands)) val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[AlcoholDutyReturnsConnector].toInstance(mockReturnsConnector)) - .overrides(bind[AlcoholDutyCalculatorConnector].toInstance(mockCalculatorConnector)) .build() running(application) { - val request = FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKey).url) + val request = + FakeRequest(GET, controllers.returns.routes.ViewReturnController.onPageLoad(periodKeyUnderTest).url) val result = route(application, request).value status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual controllers.routes.JourneyRecoveryController.onPageLoad().url } + + verify(mockReturnsConnector, times(1)).getReturn(any, any)(any) + verify(mockCalculatorConnector, never).rateBands(any())(any) } } - class SetUp { - val periodKey = periodKeyJan - val returnDetails = exampleReturnDetails(periodKey, Instant.now(clock)) - val returnDetailsWithBadPeriodKey = - returnDetails.copy(identification = returnDetails.identification.copy(periodKey = badPeriodKey)) - val returnPeriodStr = dateTimeHelper.formatMonthYear(returnPeriodJan.period) - val submittedAtDateStr = dateTimeHelper.formatDateMonthYear( + abstract class SetUp { + val periodKeyForSpirits = periodKeyJan + val periodKeyNotForSpirits = periodKeyFeb + def periodKeyUnderTest: String + val returnPeriodUnderTest = ReturnPeriod.fromPeriodKeyOrThrow(periodKeyUnderTest) + val returnDetails = exampleReturnDetails(periodKeyUnderTest, Instant.now(clock)) + val returnPeriodStr = dateTimeHelper.formatMonthYear(returnPeriodUnderTest.period) + val submittedAtDateStr = dateTimeHelper.formatDateMonthYear( dateTimeHelper.instantToLocalDate(returnDetails.identification.submittedTime) ) - val submittedAtTimeStr = dateTimeHelper.formatHourMinuteMeridiem( + val submittedAtTimeStr = dateTimeHelper.formatHourMinuteMeridiem( dateTimeHelper.instantToLocalTime(returnDetails.identification.submittedTime) ) - val rateBands = exampleRateBands(periodKey) + val rateBands = exampleRateBands(periodKeyUnderTest) val tableModel = TableViewModel.empty() val totalTableModel = TableTotalViewModel(HeadCell(), HeadCell()) diff --git a/test/viewmodels/returns/ViewReturnViewModelSpec.scala b/test/viewmodels/returns/ViewReturnViewModelSpec.scala index 17ecfa1f..3512aa93 100644 --- a/test/viewmodels/returns/ViewReturnViewModelSpec.scala +++ b/test/viewmodels/returns/ViewReturnViewModelSpec.scala @@ -18,8 +18,8 @@ package viewmodels.returns import base.SpecBase import config.FrontendAppConfig +import models.checkAndSubmit.AdrTypeOfSpirit import models.returns.{ReturnAdjustments, ReturnAlcoholDeclared, ReturnDetails, ReturnTotalDutyDue} -import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper import play.api.Application import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.Aliases.Text @@ -34,12 +34,12 @@ class ViewReturnViewModelSpec extends SpecBase { val alcoholDeclaredViewModel = viewModel.createAlcoholDeclaredViewModel(returnDetails, exampleRateBands(periodKey)) - alcoholDeclaredViewModel.rows.size shouldBe returnDetails.alcoholDeclared.alcoholDeclaredDetails.get.size - alcoholDeclaredViewModel.total.get.total.content shouldBe Text( + alcoholDeclaredViewModel.rows.size mustBe returnDetails.alcoholDeclared.alcoholDeclaredDetails.get.size + alcoholDeclaredViewModel.total.get.total.content mustBe Text( messages("site.currency.2DP", returnDetails.alcoholDeclared.total) ) - alcoholDeclaredViewModel.rows.head.cells.head.content shouldBe Text("311") - alcoholDeclaredViewModel.rows(3).cells.head.content shouldBe Text( + alcoholDeclaredViewModel.rows.head.cells.head.content mustBe Text("311") + alcoholDeclaredViewModel.rows(3).cells.head.content mustBe Text( "Non-draught beer between 1% and 2% ABV (123)" ) } @@ -47,15 +47,15 @@ class ViewReturnViewModelSpec extends SpecBase { "should return a model with no entries when a nil return" in new SetUp { val alcoholDeclaredViewModel = viewModel.createAlcoholDeclaredViewModel(nilReturn, emptyRateBands) - alcoholDeclaredViewModel.rows.size shouldBe 1 - alcoholDeclaredViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + alcoholDeclaredViewModel.rows.size mustBe 1 + alcoholDeclaredViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } "should return a model with no entries when a nil return with empty sections" in new SetUp { val alcoholDeclaredViewModel = viewModel.createAlcoholDeclaredViewModel(emptyReturnDetails, emptyRateBands) - alcoholDeclaredViewModel.rows.size shouldBe 1 - alcoholDeclaredViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + alcoholDeclaredViewModel.rows.size mustBe 1 + alcoholDeclaredViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } } @@ -63,13 +63,13 @@ class ViewReturnViewModelSpec extends SpecBase { "should return a model with data when adjustments declared" in new SetUp { val adjustmentsViewModel = viewModel.createAdjustmentsViewModel(returnDetails, exampleRateBands(periodKey2)) - adjustmentsViewModel.rows.size shouldBe 4 - adjustmentsViewModel.total.get.total.content shouldBe Text( + adjustmentsViewModel.rows.size mustBe 4 + adjustmentsViewModel.total.get.total.content mustBe Text( s"$minus${messages("site.currency.2DP", returnDetails.adjustments.total.abs)}" ) - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text("321") - adjustmentsViewModel.rows(3).cells(1).content shouldBe Text("Non-draught beer between 1% and 2% ABV (125)") + adjustmentsViewModel.rows.head.cells(1).content mustBe Text("321") + adjustmentsViewModel.rows(3).cells(1).content mustBe Text("Non-draught beer between 1% and 2% ABV (125)") } "should return a model with data when a spoilt adjustment declared" in new SetUp { @@ -80,26 +80,26 @@ class ViewReturnViewModelSpec extends SpecBase { exampleRateBands(periodKey2) ) - adjustmentsViewModel.rows.size shouldBe 2 - adjustmentsViewModel.total.get.total.content shouldBe Text( + adjustmentsViewModel.rows.size mustBe 2 + adjustmentsViewModel.total.get.total.content mustBe Text( s"$minus${messages("site.currency.2DP", returnDetailWithSpoilt.adjustments.total.abs)}" ) - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text("123") - adjustmentsViewModel.rows(1).cells(1).content shouldBe Text("Wine") + adjustmentsViewModel.rows.head.cells(1).content mustBe Text("123") + adjustmentsViewModel.rows(1).cells(1).content mustBe Text("Wine") } "should return a model with no entries when a nil return" in new SetUp { val adjustmentsViewModel = viewModel.createAdjustmentsViewModel(nilReturn, emptyRateBands) - adjustmentsViewModel.rows.size shouldBe 1 - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + adjustmentsViewModel.rows.size mustBe 1 + adjustmentsViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } "should return a model with no entries when a nil return with empty sections" in new SetUp { val adjustmentsViewModel = viewModel.createAdjustmentsViewModel(emptyReturnDetails, emptyRateBands) - adjustmentsViewModel.rows.size shouldBe 1 - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + adjustmentsViewModel.rows.size mustBe 1 + adjustmentsViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } } @@ -107,13 +107,13 @@ class ViewReturnViewModelSpec extends SpecBase { "should return a model with a total when a total exists" in new SetUp { val totalViewModel = viewModel.createTotalDueViewModel(returnDetails) - totalViewModel.total.content shouldBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) + totalViewModel.total.content mustBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) } "should return a model with no entries when a nil return" in new SetUp { val totalViewModel = viewModel.createTotalDueViewModel(nilReturn) - totalViewModel.total.content shouldBe Text(messages("site.nil")) + totalViewModel.total.content mustBe Text(messages("site.nil")) } "should return a model with a total when a total exists even if no declarations" in new SetUp { @@ -121,7 +121,7 @@ class ViewReturnViewModelSpec extends SpecBase { emptyReturnDetails.copy(totalDutyDue = ReturnTotalDutyDue(totalDue = nonZeroAmount)) ) - totalViewModel.total.content shouldBe Text(messages("site.currency.2DP", nonZeroAmount)) + totalViewModel.total.content mustBe Text(messages("site.currency.2DP", nonZeroAmount)) } "should return a model with a total when a total exists even if no alcohol is declared" in new SetUp { @@ -131,7 +131,7 @@ class ViewReturnViewModelSpec extends SpecBase { ) ) - totalViewModel.total.content shouldBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) + totalViewModel.total.content mustBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) } "should return a model with a total when a total exists when no adjustments exist" in new SetUp { @@ -141,21 +141,21 @@ class ViewReturnViewModelSpec extends SpecBase { ) ) - totalViewModel.total.content shouldBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) + totalViewModel.total.content mustBe Text(messages("site.currency.2DP", returnDetails.totalDutyDue.totalDue)) } "should return a model with no entries when a nil return (nothing declared, no total)" in new SetUp { val adjustmentsViewModel = viewModel.createAdjustmentsViewModel(nilReturn, emptyRateBands) - adjustmentsViewModel.rows.size shouldBe 1 - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + adjustmentsViewModel.rows.size mustBe 1 + adjustmentsViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } "should return a model with no entries when a nil return with empty sections (nothing declared, no total)" in new SetUp { val adjustmentsViewModel = viewModel.createAdjustmentsViewModel(emptyReturnDetails, emptyRateBands) - adjustmentsViewModel.rows.size shouldBe 1 - adjustmentsViewModel.rows.head.cells(1).content shouldBe Text(messages("site.nil")) + adjustmentsViewModel.rows.size mustBe 1 + adjustmentsViewModel.rows.head.cells(1).content mustBe Text(messages("site.nil")) } } @@ -163,10 +163,10 @@ class ViewReturnViewModelSpec extends SpecBase { "should return a model with data when duty suspension is declared" in new SetUp { val netDutySuspensionViewModel = viewModel.createNetDutySuspensionViewModel(returnDetails) - netDutySuspensionViewModel.head.size shouldBe 3 - netDutySuspensionViewModel.rows.size shouldBe 5 + netDutySuspensionViewModel.head.size mustBe 3 + netDutySuspensionViewModel.rows.size mustBe 5 netDutySuspensionViewModel.rows.foreach { row => - row.cells.size shouldBe 3 + row.cells.size mustBe 3 } } @@ -183,20 +183,97 @@ class ViewReturnViewModelSpec extends SpecBase { val netDutySuspensionViewModel = viewModel.createNetDutySuspensionViewModel(returnDetailsWithoutCider) - netDutySuspensionViewModel.head.size shouldBe 3 - netDutySuspensionViewModel.rows.size shouldBe 4 + netDutySuspensionViewModel.head.size mustBe 3 + netDutySuspensionViewModel.rows.size mustBe 4 } "should return a model with the right label when nothing declared" in new SetUp { val netDutySuspensionViewModel = viewModel.createNetDutySuspensionViewModel(nilReturn) - netDutySuspensionViewModel.rows.size shouldBe 1 - netDutySuspensionViewModel.rows.head.cells.head.content shouldBe Text( + netDutySuspensionViewModel.rows.size mustBe 1 + netDutySuspensionViewModel.rows.head.cells.head.content mustBe Text( messages("viewReturn.netDutySuspension.noneDeclared") ) } } + + "createSpiritsViewModels" - { + "should return a model with data when quarterly spirits are declared" in new SetUp { + val spiritsViewModels = viewModel.createSpiritsViewModels( + returnDetails.copy(spirits = + Some(returnDetails.spirits.get.copy(typesOfSpirit = AdrTypeOfSpirit.values.toSet)) + ) + ) + + spiritsViewModels.size mustBe 2 + spiritsViewModels.head.head.size mustBe 2 + spiritsViewModels.head.head.map(_.content) mustBe Seq( + Text(messages("viewReturn.table.description.legend")), + Text(messages("viewReturn.table.totalVolume.lpa.legend")) + ) + spiritsViewModels.head.rows.size mustBe 3 + spiritsViewModels.head.rows.foreach { row => + row.cells.size mustBe 2 + } + spiritsViewModels.head.rows.map(_.cells.head.content) mustBe Seq( + Text(messages("viewReturn.spirits.totalVolume")), + Text(messages("viewReturn.spirits.scotchWhisky")), + Text(messages("viewReturn.spirits.irishWhiskey")) + ) + spiritsViewModels.head.caption mustBe Some(messages("viewReturn.spirits.caption")) + spiritsViewModels.last.head.size mustBe 1 + spiritsViewModels.last.head.map(_.content) mustBe Seq(Text(messages("viewReturn.table.typesOfSpirits.legend"))) + spiritsViewModels.last.rows.size mustBe 1 + spiritsViewModels.last.rows.head.cells.size mustBe 1 + spiritsViewModels.last.rows.head.cells.head.content mustBe Text( + Seq( + messages("viewReturn.spirits.type.malt"), + messages("viewReturn.spirits.type.grain"), + messages("viewReturn.spirits.type.neutralAgricultural"), + messages("viewReturn.spirits.type.neutralIndustrial"), + messages("viewReturn.spirits.type.beer"), + messages("viewReturn.spirits.type.cider"), + messages("viewReturn.spirits.type.wine"), + "Coco Pops Vodka" + ).mkString(", ") + ) + spiritsViewModels.last.caption mustBe None + } + + "should return a model with data when quarterly spirits is declared and handling missing other spirits type name gracefully" in new SetUp { + val spiritsViewModels = viewModel.createSpiritsViewModels( + returnDetails.copy(spirits = + Some( + returnDetails.spirits.get.copy(typesOfSpirit = AdrTypeOfSpirit.values.toSet, otherSpiritTypeName = None) + ) + ) + ) + + spiritsViewModels.last.rows.head.cells.head.content mustBe Text( + Seq( + messages("viewReturn.spirits.type.malt"), + messages("viewReturn.spirits.type.grain"), + messages("viewReturn.spirits.type.neutralAgricultural"), + messages("viewReturn.spirits.type.neutralIndustrial"), + messages("viewReturn.spirits.type.beer"), + messages("viewReturn.spirits.type.cider"), + messages("viewReturn.spirits.type.wine") + ).mkString(", ") + ) + } + + "should return a model with the right label when nothing declared" in new SetUp { + val spiritsViewModels = viewModel.createSpiritsViewModels(nilReturn) + + spiritsViewModels.size mustBe 1 + spiritsViewModels.head.rows.size mustBe 1 + spiritsViewModels.head.rows.head.cells.head.content mustBe Text( + messages("viewReturn.spirits.noneDeclared") + ) + spiritsViewModels.head.caption mustBe Some(messages("viewReturn.spirits.caption")) + } + } } class SetUp {