From f050230cd0774e027f0576fc5b6e7d5f684a6c8f Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 12:02:11 +0100 Subject: [PATCH 1/8] [DTR-292] Adding the pagination component --- app/viewmodels/govuk/PaginationFluency.scala | 182 ++++++++++++++++ app/viewmodels/govuk/package.scala | 1 + app/views/components/Pagination.scala.html | 24 +++ .../govuk/PaginationViewModelSpec.scala | 203 ++++++++++++++++++ test/views/components/PaginationSpec.scala | 196 +++++++++++++++++ 5 files changed, 606 insertions(+) create mode 100644 app/viewmodels/govuk/PaginationFluency.scala create mode 100644 app/views/components/Pagination.scala.html create mode 100644 test/viewmodels/govuk/PaginationViewModelSpec.scala create mode 100644 test/views/components/PaginationSpec.scala diff --git a/app/viewmodels/govuk/PaginationFluency.scala b/app/viewmodels/govuk/PaginationFluency.scala new file mode 100644 index 00000000..3cff6906 --- /dev/null +++ b/app/viewmodels/govuk/PaginationFluency.scala @@ -0,0 +1,182 @@ +/* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package viewmodels.govuk + +import play.api.i18n.Messages +import uk.gov.hmrc.govukfrontend.views.viewmodels.pagination.{Pagination, PaginationItem, PaginationLink} + +object PaginationFluency { + + object PaginationViewModel { + + def apply(): PaginationViewModel = + PaginationViewModel( + items = Nil, + previous = None, + next = None, + landmarkLabel = "Pagination", + classes = "", + attributes = Map.empty + ) + + def apply(items: Seq[PaginationItemViewModel]): PaginationViewModel = + PaginationViewModel( + items = items, + previous = None, + next = None, + landmarkLabel = "Pagination", + classes = "", + attributes = Map.empty + ) + } + + case class PaginationViewModel( + items: Seq[PaginationItemViewModel] = Nil, + previous: Option[PaginationLinkViewModel] = None, + next: Option[PaginationLinkViewModel] = None, + landmarkLabel: String = "Pagination", + classes: String = "", + attributes: Map[String, String] = Map.empty + ) { + + def withItems(items: Seq[PaginationItemViewModel]): PaginationViewModel = + copy(items = items) + + def withPrevious(previous: PaginationLinkViewModel): PaginationViewModel = + copy(previous = Some(previous)) + + def withNext(next: PaginationLinkViewModel): PaginationViewModel = + copy(next = Some(next)) + + def withLandmarkLabel(landmarkLabel: String): PaginationViewModel = + copy(landmarkLabel = landmarkLabel) + + def withClasses(classes: String): PaginationViewModel = + copy(classes = classes) + + def withAttributes(attributes: Map[String, String]): PaginationViewModel = + copy(attributes = attributes) + + def asPagination(implicit messages: Messages): Pagination = { + Pagination( + items = Some(items.map(_.asPaginationItem)), + previous = previous.map(_.asPaginationLink), + next = next.map(_.asPaginationLink), + landmarkLabel = Some(landmarkLabel), + classes = classes, + attributes = attributes + ) + } + } + + object PaginationItemViewModel { + + def apply(number: String, href: String): PaginationItemViewModel = + PaginationItemViewModel( + number = number, + href = href, + visuallyHiddenText = None, + current = false, + ellipsis = false, + attributes = Map.empty + ) + + def ellipsis(): PaginationItemViewModel = + PaginationItemViewModel( + number = "", + href = "", + visuallyHiddenText = None, + current = false, + ellipsis = true, + attributes = Map.empty + ) + } + + case class PaginationItemViewModel( + number: String, + href: String, + visuallyHiddenText: Option[String], + current: Boolean, + ellipsis: Boolean, + attributes: Map[String, String] + ) { + + def withVisuallyHiddenText(visuallyHiddenText: String): PaginationItemViewModel = + copy(visuallyHiddenText = Some(visuallyHiddenText)) + + def withCurrent(current: Boolean): PaginationItemViewModel = + copy(current = current) + + def withAttributes(attributes: Map[String, String]): PaginationItemViewModel = + copy(attributes = attributes) + + def asPaginationItem(implicit messages: Messages): PaginationItem = { + PaginationItem( + number = if (ellipsis) None else Some(number), + href = if (ellipsis) "" else href, + visuallyHiddenText = visuallyHiddenText.map(messages(_)), + current = if (current) Some(true) else None, + ellipsis = if (ellipsis) Some(true) else None, + attributes = attributes + ) + } + } + + object PaginationLinkViewModel { + + def apply(href: String): PaginationLinkViewModel = + PaginationLinkViewModel( + href = href, + text = None, + html = None, + labelText = None, + attributes = Map.empty + ) + } + + case class PaginationLinkViewModel( + href: String, + text: Option[String], + html: Option[String], + labelText: Option[String], + attributes: Map[String, String] + ) { + + def withText(text: String): PaginationLinkViewModel = + copy(text = Some(text)) + + def withHtml(html: String): PaginationLinkViewModel = + copy(html = Some(html)) + + def withLabelText(labelText: String): PaginationLinkViewModel = + copy(labelText = Some(labelText)) + + def withAttributes(attributes: Map[String, String]): PaginationLinkViewModel = + copy(attributes = attributes) + + def asPaginationLink(implicit messages: Messages): PaginationLink = { + PaginationLink( + href = href, + text = text.map(messages(_)), + labelText = labelText.map(messages(_)), + attributes = attributes + ) + } + } +} + +trait PaginationFluency diff --git a/app/viewmodels/govuk/package.scala b/app/viewmodels/govuk/package.scala index 02a6e799..9df9cf34 100644 --- a/app/viewmodels/govuk/package.scala +++ b/app/viewmodels/govuk/package.scala @@ -29,6 +29,7 @@ package object govuk { with HintFluency with InputFluency with LabelFluency + with PaginationFluency with RadiosFluency with SummaryListFluency with TagFluency diff --git a/app/views/components/Pagination.scala.html b/app/views/components/Pagination.scala.html new file mode 100644 index 00000000..17442318 --- /dev/null +++ b/app/views/components/Pagination.scala.html @@ -0,0 +1,24 @@ +@* + * Copyright 2025 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +@import uk.gov.hmrc.govukfrontend.views.html.components.GovukPagination +@import viewmodels.govuk.PaginationFluency + +@this(govukPagination: GovukPagination) + +@(pagination: PaginationFluency.PaginationViewModel)(implicit messages: Messages) + +@govukPagination(pagination.asPagination) diff --git a/test/viewmodels/govuk/PaginationViewModelSpec.scala b/test/viewmodels/govuk/PaginationViewModelSpec.scala new file mode 100644 index 00000000..2e9a8124 --- /dev/null +++ b/test/viewmodels/govuk/PaginationViewModelSpec.scala @@ -0,0 +1,203 @@ +/* + * Copyright 2025 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 base.SpecBase +import org.scalatest.matchers.must.Matchers +import viewmodels.govuk.PaginationFluency._ +import play.api.test.FakeRequest +import play.api.i18n.Messages + +class PaginationViewModelSpec extends SpecBase with Matchers { + + "PaginationViewModel" - { + + "must create basic pagination with default values" in new Setup { + val pagination = PaginationViewModel() + + pagination.items mustBe Nil + pagination.previous mustBe None + pagination.next mustBe None + pagination.landmarkLabel mustBe "Pagination" + pagination.classes mustBe "" + pagination.attributes mustBe Map.empty + } + + "must create pagination with items" in new Setup { + val items = Seq( + PaginationItemViewModel("1", "/page/1"), + PaginationItemViewModel("2", "/page/2") + ) + val pagination = PaginationViewModel(items) + + pagination.items mustBe items + } + + "must support fluent API for building pagination" in new Setup { + val pagination = PaginationViewModel() + .withItems(Seq(PaginationItemViewModel("1", "/page/1"))) + .withPrevious(PaginationLinkViewModel("/prev")) + .withNext(PaginationLinkViewModel("/next")) + .withLandmarkLabel("Custom Label") + .withClasses("custom-class") + .withAttributes(Map("data-test" -> "pagination")) + + pagination.items.size mustBe 1 + pagination.previous mustBe defined + pagination.next mustBe defined + pagination.landmarkLabel mustBe "Custom Label" + pagination.classes mustBe "custom-class" + pagination.attributes mustBe Map("data-test" -> "pagination") + } + + "must convert to GovUK Pagination correctly" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1"), + PaginationItemViewModel("2", "/page/2").withCurrent(true) + ), + previous = Some(PaginationLinkViewModel("/prev").withText("Previous")), + next = Some(PaginationLinkViewModel("/next").withText("Next")), + landmarkLabel = "Test Pagination", + classes = "test-class", + attributes = Map("data-test" -> "value") + ) + + val govukPagination = pagination.asPagination + + govukPagination.items.get.size mustBe 2 + govukPagination.previous mustBe defined + govukPagination.next mustBe defined + govukPagination.landmarkLabel mustBe Some("Test Pagination") + govukPagination.classes mustBe "test-class" + govukPagination.attributes mustBe Map("data-test" -> "value") + } + } + + "PaginationItemViewModel" - { + + "must create basic item with number and href" in new Setup { + val item = PaginationItemViewModel("1", "/page/1") + + item.number mustBe "1" + item.href mustBe "/page/1" + item.visuallyHiddenText mustBe None + item.current mustBe false + item.ellipsis mustBe false + item.attributes mustBe Map.empty + } + + "must create ellipsis item" in new Setup { + val item = PaginationItemViewModel.ellipsis() + + item.number mustBe "" + item.href mustBe "" + item.ellipsis mustBe true + } + + "must support fluent API for building items" in new Setup { + val item = PaginationItemViewModel("1", "/page/1") + .withVisuallyHiddenText("Go to page 1") + .withCurrent(true) + .withAttributes(Map("data-test" -> "item")) + + item.visuallyHiddenText mustBe Some("Go to page 1") + item.current mustBe true + item.attributes mustBe Map("data-test" -> "item") + } + + "must convert to GovUK PaginationItem correctly" in new Setup { + val item = PaginationItemViewModel("1", "/page/1") + .withVisuallyHiddenText("Go to page 1") + .withCurrent(true) + .withAttributes(Map("data-test" -> "item")) + + val govukItem = item.asPaginationItem + + govukItem.number mustBe Some("1") + govukItem.href mustBe "/page/1" + govukItem.visuallyHiddenText mustBe Some("Go to page 1") + govukItem.current mustBe Some(true) + govukItem.ellipsis mustBe None + govukItem.attributes mustBe Map("data-test" -> "item") + } + + "must convert ellipsis item to GovUK PaginationItem correctly" in new Setup { + val item = PaginationItemViewModel.ellipsis() + val govukItem = item.asPaginationItem + + govukItem.number mustBe None + govukItem.href mustBe "" + govukItem.ellipsis mustBe Some(true) + } + } + + "PaginationLinkViewModel" - { + + "must create basic link with href" in new Setup { + val link = PaginationLinkViewModel("/page/1") + + link.href mustBe "/page/1" + link.text mustBe None + link.html mustBe None + link.labelText mustBe None + link.attributes mustBe Map.empty + } + + "must support fluent API for building links" in new Setup { + val link = PaginationLinkViewModel("/page/1") + .withText("Next page") + .withLabelText("More results") + .withAttributes(Map("data-test" -> "link")) + + link.text mustBe Some("Next page") + link.labelText mustBe Some("More results") + link.attributes mustBe Map("data-test" -> "link") + } + + "must convert to GovUK PaginationLink correctly" in new Setup { + val link = PaginationLinkViewModel("/page/1") + .withText("Next page") + .withLabelText("More results") + .withAttributes(Map("data-test" -> "link")) + + val govukLink = link.asPaginationLink + + govukLink.href mustBe "/page/1" + govukLink.text mustBe Some("Next page") + govukLink.labelText mustBe Some("More results") + govukLink.attributes mustBe Map("data-test" -> "link") + } + + "must handle HTML content in links" in new Setup { + val link = PaginationLinkViewModel("/page/1") + .withHtml("Next") + + val govukLink = link.asPaginationLink + + govukLink.href mustBe "/page/1" + govukLink.text mustBe None + } + } + + trait Setup { + val app = applicationBuilder().build() + implicit val request: play.api.mvc.Request[?] = FakeRequest() + implicit val messages: Messages = play.api.i18n.MessagesImpl( + play.api.i18n.Lang.defaultLang, + app.injector.instanceOf[play.api.i18n.MessagesApi] + ) + } +} diff --git a/test/views/components/PaginationSpec.scala b/test/views/components/PaginationSpec.scala new file mode 100644 index 00000000..e18cc64b --- /dev/null +++ b/test/views/components/PaginationSpec.scala @@ -0,0 +1,196 @@ +/* + * Copyright 2025 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 base.SpecBase +import org.scalatest.matchers.must.Matchers +import views.html.components.Pagination +import play.api.test.FakeRequest +import play.api.i18n.Messages +import org.jsoup.Jsoup +import viewmodels.govuk.PaginationFluency._ + +class PaginationSpec extends SpecBase with Matchers { + + "Pagination component" - { + + "must render basic pagination with page numbers" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1"), + PaginationItemViewModel("2", "/page/2").withCurrent(true), + PaginationItemViewModel("3", "/page/3") + ) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination").size mustBe 1 + doc.select(".govuk-pagination__list").size mustBe 1 + doc.select(".govuk-pagination__item").size mustBe 3 + doc.select(".govuk-pagination__item--current").size mustBe 1 + doc.select(".govuk-pagination__item--current a").attr("aria-current") mustBe "page" + } + + "must render pagination with previous link" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1"), + PaginationItemViewModel("2", "/page/2").withCurrent(true) + ), + previous = Some(PaginationLinkViewModel("/page/1").withText("Previous page")) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__prev").size mustBe 1 + doc.select(".govuk-pagination__prev a").attr("href") mustBe "/page/1" + doc.select(".govuk-pagination__prev a").attr("rel") mustBe "prev" + doc.select(".govuk-pagination__icon--prev").size mustBe 1 + } + + "must render pagination with next link" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1").withCurrent(true), + PaginationItemViewModel("2", "/page/2") + ), + next = Some(PaginationLinkViewModel("/page/2").withText("Next page")) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__next").size mustBe 1 + doc.select(".govuk-pagination__next a").attr("href") mustBe "/page/2" + doc.select(".govuk-pagination__next a").attr("rel") mustBe "next" + doc.select(".govuk-pagination__icon--next").size mustBe 1 + } + + "must render pagination with ellipsis" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1"), + PaginationItemViewModel.ellipsis(), + PaginationItemViewModel("5", "/page/5").withCurrent(true), + PaginationItemViewModel("6", "/page/6") + ) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__item").size mustBe 4 + doc.select(".govuk-pagination__item").get(1).text() mustBe "⋯" + } + + "must render pagination with custom landmark label" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1") + ) + ).withLandmarkLabel("Search results") + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination").attr("aria-label") mustBe "Search results" + } + + "must render pagination with custom classes" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1") + ) + ).withClasses("custom-pagination") + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination").hasClass("custom-pagination") mustBe true + } + + "must render pagination with visually hidden text for accessibility" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("1", "/page/1").withVisuallyHiddenText("Go to page 1"), + PaginationItemViewModel("2", "/page/2").withCurrent(true).withVisuallyHiddenText("Current page, page 2") + ) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__item a").first().attr("aria-label") mustBe "Go to page 1" + doc.select(".govuk-pagination__item--current a").attr("aria-label") mustBe "Current page, page 2" + } + + "must render pagination with previous and next links having custom text" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("2", "/page/2").withCurrent(true) + ), + previous = Some(PaginationLinkViewModel("/page/1").withText("Go back")), + next = Some(PaginationLinkViewModel("/page/3").withText("Continue")) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__prev .govuk-pagination__link-title").text() mustBe "Go back" + doc.select(".govuk-pagination__next .govuk-pagination__link-title").text() mustBe "Continue" + } + + "must render pagination with label text for previous and next links" in new Setup { + val pagination = PaginationViewModel( + items = Seq( + PaginationItemViewModel("2", "/page/2").withCurrent(true) + ), + previous = Some(PaginationLinkViewModel("/page/1").withText("Previous").withLabelText("Search results")), + next = Some(PaginationLinkViewModel("/page/3").withText("Next").withLabelText("More results")) + ) + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination__prev .govuk-pagination__link-label").text() mustBe "" + doc.select(".govuk-pagination__next .govuk-pagination__link-label").text() mustBe "" + } + + "must render empty pagination when no items provided" in new Setup { + val pagination = PaginationViewModel() + + val html = paginationComponent(pagination) + val doc = Jsoup.parse(html.body) + + doc.select(".govuk-pagination").size mustBe 1 + doc.select(".govuk-pagination__list").size mustBe 0 + doc.select(".govuk-pagination__prev").size mustBe 0 + doc.select(".govuk-pagination__next").size mustBe 0 + } + } + + trait Setup { + val app = applicationBuilder().build() + val paginationComponent = app.injector.instanceOf[Pagination] + implicit val request: play.api.mvc.Request[?] = FakeRequest() + implicit val messages: Messages = play.api.i18n.MessagesImpl( + play.api.i18n.Lang.defaultLang, + app.injector.instanceOf[play.api.i18n.MessagesApi] + ) + } +} From b5093620ce66fa6ea56a09d3ce46d0b5aac8facd Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 12:26:00 +0100 Subject: [PATCH 2/8] [DTR-292] Rename ambiguous pagination reference --- app/viewmodels/govuk/PaginationFluency.scala | 74 +++++++++---------- ....html => DirectDebitPagination.scala.html} | 0 .../govuk/PaginationViewModelSpec.scala | 10 +-- ....scala => DirectDebitPaginationSpec.scala} | 8 +- 4 files changed, 46 insertions(+), 46 deletions(-) rename app/views/components/{Pagination.scala.html => DirectDebitPagination.scala.html} (100%) rename test/views/components/{PaginationSpec.scala => DirectDebitPaginationSpec.scala} (96%) diff --git a/app/viewmodels/govuk/PaginationFluency.scala b/app/viewmodels/govuk/PaginationFluency.scala index 3cff6906..8cfdd072 100644 --- a/app/viewmodels/govuk/PaginationFluency.scala +++ b/app/viewmodels/govuk/PaginationFluency.scala @@ -25,22 +25,22 @@ object PaginationFluency { def apply(): PaginationViewModel = PaginationViewModel( - items = Nil, - previous = None, - next = None, + items = Nil, + previous = None, + next = None, landmarkLabel = "Pagination", - classes = "", - attributes = Map.empty + classes = "", + attributes = Map.empty ) def apply(items: Seq[PaginationItemViewModel]): PaginationViewModel = PaginationViewModel( - items = items, - previous = None, - next = None, + items = items, + previous = None, + next = None, landmarkLabel = "Pagination", - classes = "", - attributes = Map.empty + classes = "", + attributes = Map.empty ) } @@ -73,12 +73,12 @@ object PaginationFluency { def asPagination(implicit messages: Messages): Pagination = { Pagination( - items = Some(items.map(_.asPaginationItem)), - previous = previous.map(_.asPaginationLink), - next = next.map(_.asPaginationLink), + items = Some(items.map(_.asPaginationItem)), + previous = previous.map(_.asPaginationLink), + next = next.map(_.asPaginationLink), landmarkLabel = Some(landmarkLabel), - classes = classes, - attributes = attributes + classes = classes, + attributes = attributes ) } } @@ -87,22 +87,22 @@ object PaginationFluency { def apply(number: String, href: String): PaginationItemViewModel = PaginationItemViewModel( - number = number, - href = href, + number = number, + href = href, visuallyHiddenText = None, - current = false, - ellipsis = false, - attributes = Map.empty + current = false, + ellipsis = false, + attributes = Map.empty ) def ellipsis(): PaginationItemViewModel = PaginationItemViewModel( - number = "", - href = "", + number = "", + href = "", visuallyHiddenText = None, - current = false, - ellipsis = true, - attributes = Map.empty + current = false, + ellipsis = true, + attributes = Map.empty ) } @@ -126,12 +126,12 @@ object PaginationFluency { def asPaginationItem(implicit messages: Messages): PaginationItem = { PaginationItem( - number = if (ellipsis) None else Some(number), - href = if (ellipsis) "" else href, + number = if (ellipsis) None else Some(number), + href = if (ellipsis) "" else href, visuallyHiddenText = visuallyHiddenText.map(messages(_)), - current = if (current) Some(true) else None, - ellipsis = if (ellipsis) Some(true) else None, - attributes = attributes + current = if (current) Some(true) else None, + ellipsis = if (ellipsis) Some(true) else None, + attributes = attributes ) } } @@ -140,10 +140,10 @@ object PaginationFluency { def apply(href: String): PaginationLinkViewModel = PaginationLinkViewModel( - href = href, - text = None, - html = None, - labelText = None, + href = href, + text = None, + html = None, + labelText = None, attributes = Map.empty ) } @@ -170,9 +170,9 @@ object PaginationFluency { def asPaginationLink(implicit messages: Messages): PaginationLink = { PaginationLink( - href = href, - text = text.map(messages(_)), - labelText = labelText.map(messages(_)), + href = href, + text = text.map(messages(_)), + labelText = labelText.map(messages(_)), attributes = attributes ) } diff --git a/app/views/components/Pagination.scala.html b/app/views/components/DirectDebitPagination.scala.html similarity index 100% rename from app/views/components/Pagination.scala.html rename to app/views/components/DirectDebitPagination.scala.html diff --git a/test/viewmodels/govuk/PaginationViewModelSpec.scala b/test/viewmodels/govuk/PaginationViewModelSpec.scala index 2e9a8124..2862c8bb 100644 --- a/test/viewmodels/govuk/PaginationViewModelSpec.scala +++ b/test/viewmodels/govuk/PaginationViewModelSpec.scala @@ -16,7 +16,7 @@ import base.SpecBase import org.scalatest.matchers.must.Matchers -import viewmodels.govuk.PaginationFluency._ +import viewmodels.govuk.PaginationFluency.* import play.api.test.FakeRequest import play.api.i18n.Messages @@ -68,11 +68,11 @@ class PaginationViewModelSpec extends SpecBase with Matchers { PaginationItemViewModel("1", "/page/1"), PaginationItemViewModel("2", "/page/2").withCurrent(true) ), - previous = Some(PaginationLinkViewModel("/prev").withText("Previous")), - next = Some(PaginationLinkViewModel("/next").withText("Next")), + previous = Some(PaginationLinkViewModel("/prev").withText("Previous")), + next = Some(PaginationLinkViewModel("/next").withText("Next")), landmarkLabel = "Test Pagination", - classes = "test-class", - attributes = Map("data-test" -> "value") + classes = "test-class", + attributes = Map("data-test" -> "value") ) val govukPagination = pagination.asPagination diff --git a/test/views/components/PaginationSpec.scala b/test/views/components/DirectDebitPaginationSpec.scala similarity index 96% rename from test/views/components/PaginationSpec.scala rename to test/views/components/DirectDebitPaginationSpec.scala index e18cc64b..2175ef10 100644 --- a/test/views/components/PaginationSpec.scala +++ b/test/views/components/DirectDebitPaginationSpec.scala @@ -16,15 +16,15 @@ import base.SpecBase import org.scalatest.matchers.must.Matchers -import views.html.components.Pagination +import views.html.components.DirectDebitPagination import play.api.test.FakeRequest import play.api.i18n.Messages import org.jsoup.Jsoup import viewmodels.govuk.PaginationFluency._ -class PaginationSpec extends SpecBase with Matchers { +class DirectDebitPaginationSpec extends SpecBase with Matchers { - "Pagination component" - { + "DirectDebitPagination component" - { "must render basic pagination with page numbers" in new Setup { val pagination = PaginationViewModel( @@ -186,7 +186,7 @@ class PaginationSpec extends SpecBase with Matchers { trait Setup { val app = applicationBuilder().build() - val paginationComponent = app.injector.instanceOf[Pagination] + val paginationComponent = app.injector.instanceOf[DirectDebitPagination] implicit val request: play.api.mvc.Request[?] = FakeRequest() implicit val messages: Messages = play.api.i18n.MessagesImpl( play.api.i18n.Lang.defaultLang, From 316199a4d41cca6ca627dceb80789a63f705f772 Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 13:32:24 +0100 Subject: [PATCH 3/8] [DTR-292] PaginationService added --- ...ourDirectDebitInstructionsController.scala | 22 +- app/services/PaginationService.scala | 128 +++++++++++ ...YourDirectDebitInstructionsView.scala.html | 8 +- conf/messages.en | 2 + ...irectDebitInstructionsControllerSpec.scala | 213 ++++++++++++------ test/services/PaginationServiceSpec.scala | 149 ++++++++++++ .../govuk/PaginationViewModelSpec.scala | 203 ----------------- .../DirectDebitPaginationSpec.scala | 6 +- 8 files changed, 455 insertions(+), 276 deletions(-) create mode 100644 app/services/PaginationService.scala create mode 100644 test/services/PaginationServiceSpec.scala delete mode 100644 test/viewmodels/govuk/PaginationViewModelSpec.scala diff --git a/app/controllers/YourDirectDebitInstructionsController.scala b/app/controllers/YourDirectDebitInstructionsController.scala index 9d643591..9b1d846a 100644 --- a/app/controllers/YourDirectDebitInstructionsController.scala +++ b/app/controllers/YourDirectDebitInstructionsController.scala @@ -20,10 +20,10 @@ import config.FrontendAppConfig import controllers.actions.* import models.UserAnswers import play.api.i18n.{I18nSupport, MessagesApi} -import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import play.api.mvc.{Action, AnyContent, Call, MessagesControllerComponents} import queries.{DirectDebitReferenceQuery, PaymentPlanReferenceQuery} import repositories.SessionRepository -import services.NationalDirectDebitService +import services.{NationalDirectDebitService, PaginationService} import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import views.html.YourDirectDebitInstructionsView @@ -38,6 +38,7 @@ class YourDirectDebitInstructionsController @Inject() ( view: YourDirectDebitInstructionsView, appConfig: FrontendAppConfig, nddService: NationalDirectDebitService, + paginationService: PaginationService, sessionRepository: SessionRepository )(implicit ec: ExecutionContext) extends FrontendBaseController @@ -45,10 +46,25 @@ class YourDirectDebitInstructionsController @Inject() ( def onPageLoad: Action[AnyContent] = (identify andThen getData).async { implicit request => val userAnswers = request.userAnswers.getOrElse(UserAnswers(request.userId)) + val currentPage = request.getQueryString("page").flatMap(_.toIntOption).getOrElse(1) + cleanseDirectDebitReference(userAnswers).flatMap { _ => nddService.retrieveAllDirectDebits(request.userId) map { directDebitDetailsData => val maxLimitReached = directDebitDetailsData.directDebitCount > appConfig.maxNumberDDIsAllowed - Ok(view(directDebitDetailsData.directDebitList.map(_.toDirectDebitDetails), maxLimitReached)) + + val paginationResult = paginationService.paginateDirectDebits( + allDirectDebits = directDebitDetailsData.directDebitList, + currentPage = currentPage, + baseUrl = routes.YourDirectDebitInstructionsController.onPageLoad().url + ) + + Ok( + view( + directDebitDetails = paginationResult.paginatedData, + maxLimitReached = maxLimitReached, + paginationViewModel = paginationResult.paginationViewModel + ) + ) } } } diff --git a/app/services/PaginationService.scala b/app/services/PaginationService.scala new file mode 100644 index 00000000..77e286aa --- /dev/null +++ b/app/services/PaginationService.scala @@ -0,0 +1,128 @@ +/* + * Copyright 2025 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 models.{DirectDebitDetails, NddDetails} +import viewmodels.govuk.PaginationFluency.* + +import java.time.LocalDateTime +import javax.inject.{Inject, Singleton} + +case class PaginationResult( + paginatedData: Seq[DirectDebitDetails], + paginationViewModel: PaginationViewModel, + totalRecords: Int, + currentPage: Int, + totalPages: Int +) + +@Singleton +class PaginationService @Inject() { + + val recordsPerPage = 3 + val maxRecords = 99 + + def paginateDirectDebits( + allDirectDebits: Seq[NddDetails], + currentPage: Int = 1, + baseUrl: String + ): PaginationResult = { + + val sortedDirectDebits = allDirectDebits + .sortBy(_.submissionDateTime)(Ordering[LocalDateTime].reverse) + .take(maxRecords) + + val totalRecords = sortedDirectDebits.length + val totalPages = Math.ceil(totalRecords.toDouble / recordsPerPage).toInt + val validCurrentPage = Math.max(1, Math.min(currentPage, totalPages)) + + val startIndex = (validCurrentPage - 1) * recordsPerPage + val endIndex = Math.min(startIndex + recordsPerPage, totalRecords) + + val paginatedData = sortedDirectDebits + .slice(startIndex, endIndex) + .map(_.toDirectDebitDetails) + + val paginationViewModel = createPaginationViewModel( + currentPage = validCurrentPage, + totalPages = totalPages, + baseUrl = baseUrl + ) + + PaginationResult( + paginatedData = paginatedData, + paginationViewModel = paginationViewModel, + totalRecords = totalRecords, + currentPage = validCurrentPage, + totalPages = totalPages + ) + } + + private def createPaginationViewModel( + currentPage: Int, + totalPages: Int, + baseUrl: String + ): PaginationViewModel = { + + if (totalPages <= 1) { + return PaginationViewModel() + } + + val items = generatePageItems(currentPage, totalPages, baseUrl) + val previous = if (currentPage > 1) { + Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage - 1}").withText("Previous page")) + } else None + + val next = if (currentPage < totalPages) { + Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage + 1}").withText("Next page")) + } else None + + PaginationViewModel( + items = items, + previous = previous, + next = next + ) + } + + private def generatePageItems( + currentPage: Int, + totalPages: Int, + baseUrl: String + ): Seq[PaginationItemViewModel] = { + + val maxVisiblePages = 5 + val halfVisible = maxVisiblePages / 2 + + val startPage = Math.max(1, currentPage - halfVisible) + val endPage = Math.min(totalPages, startPage + maxVisiblePages - 1) + + val adjustedStartPage = if (endPage - startPage < maxVisiblePages - 1) { + Math.max(1, endPage - maxVisiblePages + 1) + } else startPage + + val pages = (adjustedStartPage to endPage).toList + + val items = pages.map { page => + PaginationItemViewModel( + number = page.toString, + href = s"$baseUrl?page=$page" + ).withCurrent(page == currentPage) + } + + items + } +} diff --git a/app/views/YourDirectDebitInstructionsView.scala.html b/app/views/YourDirectDebitInstructionsView.scala.html index 4f718169..d8091f82 100644 --- a/app/views/YourDirectDebitInstructionsView.scala.html +++ b/app/views/YourDirectDebitInstructionsView.scala.html @@ -16,6 +16,7 @@ @import views.html.components.* @import utils.MaskAndFormatUtils.* +@import viewmodels.govuk.PaginationFluency.* @this( layout: templates.Layout, @@ -24,10 +25,11 @@ paragraphMessageWithLink: ParagraphMessageWithLink, govukButton: GovukButton, govukErrorSummary: GovukErrorSummary, - govukSummaryList: GovukSummaryList + govukSummaryList: GovukSummaryList, + directDebitPagination: DirectDebitPagination ) -@(directDebitDetails: Seq[DirectDebitDetails], maxLimitReached: Boolean = false)(implicit request: Request[_], messages: Messages) +@(directDebitDetails: Seq[DirectDebitDetails], maxLimitReached: Boolean = false, paginationViewModel: PaginationViewModel = PaginationViewModel())(implicit request: Request[_], messages: Messages) @layout(pageTitle = titleForSetupJourneyNoForm(messages("yourDirectDebitInstructions.title")), showBackLink = false) { @@ -96,4 +98,6 @@ ) )) } + + @directDebitPagination(paginationViewModel) } diff --git a/conf/messages.en b/conf/messages.en index 6eb97c93..65d818e1 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -15,6 +15,8 @@ site.startAgain = Start again site.signIn = Sign in site.govuk = GOV.UK site.accept.continue = Accept and continue +site.next = Next +site.previous = Previous date.day = Day date.month = Month diff --git a/test/controllers/YourDirectDebitInstructionsControllerSpec.scala b/test/controllers/YourDirectDebitInstructionsControllerSpec.scala index 244b31c6..e3fcd439 100644 --- a/test/controllers/YourDirectDebitInstructionsControllerSpec.scala +++ b/test/controllers/YourDirectDebitInstructionsControllerSpec.scala @@ -17,90 +17,173 @@ package controllers import base.SpecBase +import models.{NddDetails, NddResponse, UserAnswers} +import org.scalatest.matchers.must.Matchers +import org.scalatestplus.mockito.MockitoSugar import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when -import org.scalatestplus.mockito.MockitoSugar.mock +import org.mockito.ArgumentMatchers.eq as mockitoEq +import org.mockito.Mockito.* import play.api.inject.bind import play.api.test.FakeRequest import play.api.test.Helpers.* -import services.NationalDirectDebitService -import utils.DirectDebitDetailsData -import views.html.YourDirectDebitInstructionsView +import repositories.SessionRepository +import services.{NationalDirectDebitService, PaginationResult, PaginationService} +import viewmodels.govuk.PaginationFluency.* +import java.time.LocalDateTime import scala.concurrent.Future -class YourDirectDebitInstructionsControllerSpec extends SpecBase with DirectDebitDetailsData { +class YourDirectDebitInstructionsControllerSpec extends SpecBase with Matchers with MockitoSugar { - "YourDirectDebitInstructions Controller" - { + "YourDirectDebitInstructionsController" - { - val mockService = mock[NationalDirectDebitService] + "onPageLoad" - { - "must return OK and the correct view for a GET" in { + "must return OK and the correct view for a GET with no page parameter" in { + val testData = createTestNddResponse(5) + val mockNddService = mock[NationalDirectDebitService] + val mockPaginationService = mock[PaginationService] + val mockSessionRepository = mock[SessionRepository] - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides( - bind[NationalDirectDebitService].toInstance(mockService) - ) - .build() + when(mockNddService.retrieveAllDirectDebits(any())(any(), any())) + .thenReturn(Future.successful(testData)) + when(mockPaginationService.paginateDirectDebits(any(), any(), any())) + .thenReturn(createTestPaginationResult(1, 3, 2)) + when(mockSessionRepository.set(any())) + .thenReturn(Future.successful(true)) - running(application) { + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[NationalDirectDebitService].toInstance(mockNddService), + bind[PaginationService].toInstance(mockPaginationService), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() - when(mockService.retrieveAllDirectDebits(any())(any(), any())) - .thenReturn(Future.successful(nddResponse)) + running(application) { + val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url) + val result = route(application, request).value - val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url) + status(result) mustEqual OK + verify(mockPaginationService).paginateDirectDebits(mockitoEq(testData.directDebitList), mockitoEq(1), any()) + } + } - val result = route(application, request).value + "must return OK and the correct view for a GET with page parameter" in { + val testData = createTestNddResponse(10) + val mockNddService = mock[NationalDirectDebitService] + val mockPaginationService = mock[PaginationService] + val mockSessionRepository = mock[SessionRepository] + + when(mockNddService.retrieveAllDirectDebits(any())(any(), any())) + .thenReturn(Future.successful(testData)) + when(mockPaginationService.paginateDirectDebits(any(), any(), any())) + .thenReturn(createTestPaginationResult(2, 3, 4)) + when(mockSessionRepository.set(any())) + .thenReturn(Future.successful(true)) + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[NationalDirectDebitService].toInstance(mockNddService), + bind[PaginationService].toInstance(mockPaginationService), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url + "?page=2") + val result = route(application, request).value + + status(result) mustEqual OK + verify(mockPaginationService).paginateDirectDebits(mockitoEq(testData.directDebitList), mockitoEq(2), any()) + } + } - val view = application.injector.instanceOf[YourDirectDebitInstructionsView] - val directDebits = directDebitDetailsData - status(result) mustEqual OK - contentAsString(result) mustEqual view(directDebits)(request, messages(application)).toString - contentAsString(result) must include("Your Direct Debit instructions") - contentAsString(result) must include("You can add a new payment plan to existing Direct Debit Instructions (DDI).") - contentAsString(result) must include("Direct Debit reference") - contentAsString(result) must include("Date set up") - contentAsString(result) must include("Account Number") - contentAsString(result) must include("Number of payment plans") - contentAsString(result) must include("View or add to") - contentAsString(result) must include( - "Note: If you want to cancel a Direct Debit you must contact the HMRC Payment Helpline on 0845 366 1208." - ) + "must handle invalid page parameter gracefully" in { + val testData = createTestNddResponse(5) + val mockNddService = mock[NationalDirectDebitService] + val mockPaginationService = mock[PaginationService] + val mockSessionRepository = mock[SessionRepository] + + when(mockNddService.retrieveAllDirectDebits(any())(any(), any())) + .thenReturn(Future.successful(testData)) + when(mockPaginationService.paginateDirectDebits(any(), any(), any())) + .thenReturn(createTestPaginationResult(1, 3, 2)) + when(mockSessionRepository.set(any())) + .thenReturn(Future.successful(true)) + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[NationalDirectDebitService].toInstance(mockNddService), + bind[PaginationService].toInstance(mockPaginationService), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url + "?page=invalid") + val result = route(application, request).value + + status(result) mustEqual OK + verify(mockPaginationService).paginateDirectDebits(mockitoEq(testData.directDebitList), mockitoEq(1), any()) + } } - } - "must return OK and the correct view for a GET when UserAnswers is None" in { - - val application = applicationBuilder(userAnswers = None) - .overrides( - bind[NationalDirectDebitService].toInstance(mockService) - ) - .build() - - running(application) { - - when(mockService.retrieveAllDirectDebits(any())(any(), any())) - .thenReturn(Future.successful(nddResponse)) - - val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url) - - val result = route(application, request).value - - val view = application.injector.instanceOf[YourDirectDebitInstructionsView] - val directDebits = directDebitDetailsData - status(result) mustEqual OK - contentAsString(result) mustEqual view(directDebits)(request, messages(application)).toString - contentAsString(result) must include("Your Direct Debit instructions") - contentAsString(result) must include("You can add a new payment plan to existing Direct Debit Instructions (DDI).") - contentAsString(result) must include("Direct Debit reference") - contentAsString(result) must include("Date set up") - contentAsString(result) must include("Account Number") - contentAsString(result) must include("Number of payment plans") - contentAsString(result) must include("View or add to") - contentAsString(result) must include( - "Note: If you want to cancel a Direct Debit you must contact the HMRC Payment Helpline on 0845 366 1208." - ) + "must handle empty direct debit list" in { + val emptyData = NddResponse(0, Seq.empty) + val mockNddService = mock[NationalDirectDebitService] + val mockPaginationService = mock[PaginationService] + val mockSessionRepository = mock[SessionRepository] + + when(mockNddService.retrieveAllDirectDebits(any())(any(), any())) + .thenReturn(Future.successful(emptyData)) + when(mockPaginationService.paginateDirectDebits(any(), any(), any())) + .thenReturn(createTestPaginationResult(1, 0, 0)) + when(mockSessionRepository.set(any())) + .thenReturn(Future.successful(true)) + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[NationalDirectDebitService].toInstance(mockNddService), + bind[PaginationService].toInstance(mockPaginationService), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + running(application) { + val request = FakeRequest(GET, routes.YourDirectDebitInstructionsController.onPageLoad().url) + val result = route(application, request).value + + status(result) mustEqual OK + verify(mockPaginationService).paginateDirectDebits(mockitoEq(Seq.empty), mockitoEq(1), any()) + } } } } + + private def createTestNddResponse(count: Int): NddResponse = { + val now = LocalDateTime.now() + val testDetails = (1 to count).map { i => + NddDetails( + ddiRefNumber = s"DD$i", + submissionDateTime = now.minusDays(i), + bankSortCode = "123456", + bankAccountNumber = "12345678", + bankAccountName = s"Test Account $i", + auDdisFlag = false, + numberOfPayPlans = 1 + ) + } + NddResponse(count, testDetails) + } + + private def createTestPaginationResult(currentPage: Int, totalRecords: Int, totalPages: Int): PaginationResult = { + PaginationResult( + paginatedData = Seq.empty, + paginationViewModel = PaginationViewModel(), + totalRecords = totalRecords, + currentPage = currentPage, + totalPages = totalPages + ) + } } diff --git a/test/services/PaginationServiceSpec.scala b/test/services/PaginationServiceSpec.scala new file mode 100644 index 00000000..019bfa30 --- /dev/null +++ b/test/services/PaginationServiceSpec.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2025 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 base.SpecBase +import models.{DirectDebitDetails, NddDetails} +import org.scalatest.matchers.must.Matchers +import viewmodels.govuk.PaginationFluency.* + +import java.time.LocalDateTime + +class PaginationServiceSpec extends SpecBase with Matchers { + + val paginationService = new PaginationService() + + "PaginationService" - { + + "paginateDirectDebits" - { + + "must return correct pagination for first page with 3 records per page" in { + val testData = createTestNddDetails(5) + val result = paginationService.paginateDirectDebits(testData, currentPage = 1, baseUrl = "/test") + + result.paginatedData.length mustBe 3 + result.currentPage mustBe 1 + result.totalPages mustBe 2 + result.totalRecords mustBe 5 + result.paginationViewModel.previous mustBe None + result.paginationViewModel.next mustBe defined + } + + "must return correct pagination for last page" in { + val testData = createTestNddDetails(5) + val result = paginationService.paginateDirectDebits(testData, currentPage = 2, baseUrl = "/test") + + result.paginatedData.length mustBe 2 + result.currentPage mustBe 2 + result.totalPages mustBe 2 + result.totalRecords mustBe 5 + result.paginationViewModel.previous mustBe defined + result.paginationViewModel.next mustBe None + } + + "must return correct pagination for middle page" in { + val testData = createTestNddDetails(10) + val result = paginationService.paginateDirectDebits(testData, currentPage = 2, baseUrl = "/test") + + result.paginatedData.length mustBe 3 + result.currentPage mustBe 2 + result.totalPages mustBe 4 + result.totalRecords mustBe 10 + result.paginationViewModel.previous mustBe defined + result.paginationViewModel.next mustBe defined + } + + "must handle empty data" in { + val result = paginationService.paginateDirectDebits(Seq.empty, baseUrl = "/test") + + result.paginatedData.length mustBe 0 + result.currentPage mustBe 1 + result.totalPages mustBe 0 + result.totalRecords mustBe 0 + result.paginationViewModel.items.length mustBe 0 + } + + "must limit records to maximum of 99" in { + val testData = createTestNddDetails(150) + val result = paginationService.paginateDirectDebits(testData, baseUrl = "/test") + + result.totalRecords mustBe 99 + result.totalPages mustBe 33 + } + + "must sort by submission date in descending order (newest first)" in { + val now = LocalDateTime.now() + val testData = Seq( + NddDetails("DD001", now.minusDays(2), "123456", "12345678", "Test Account", false, 1), + NddDetails("DD002", now.minusDays(1), "123456", "12345678", "Test Account", false, 1), + NddDetails("DD003", now, "123456", "12345678", "Test Account", false, 1) + ) + + val result = paginationService.paginateDirectDebits(testData, baseUrl = "/test") + + result.paginatedData.head.directDebitReference mustBe "DD003" + result.paginatedData(1).directDebitReference mustBe "DD002" + result.paginatedData(2).directDebitReference mustBe "DD001" + } + + "must handle invalid page numbers gracefully" in { + val testData = createTestNddDetails(5) + + val resultNegative = paginationService.paginateDirectDebits(testData, currentPage = -1, baseUrl = "/test") + resultNegative.currentPage mustBe 1 + + val resultTooHigh = paginationService.paginateDirectDebits(testData, currentPage = 999, baseUrl = "/test") + resultTooHigh.currentPage mustBe 2 + } + + "must generate correct pagination links" in { + val testData = createTestNddDetails(10) + val result = paginationService.paginateDirectDebits(testData, currentPage = 2, baseUrl = "/test") + + result.paginationViewModel.previous.get.href mustBe "/test?page=1" + result.paginationViewModel.next.get.href mustBe "/test?page=3" + result.paginationViewModel.items.exists(_.current) mustBe true + result.paginationViewModel.items.find(_.current).get.number mustBe "2" + } + + "must not show pagination when only one page" in { + val testData = createTestNddDetails(2) + val result = paginationService.paginateDirectDebits(testData, baseUrl = "/test") + + result.paginationViewModel.items.length mustBe 0 + result.paginationViewModel.previous mustBe None + result.paginationViewModel.next mustBe None + } + } + } + + + private def createTestNddDetails(count: Int): Seq[NddDetails] = { + val now = LocalDateTime.now() + (1 to count).map { i => + NddDetails( + ddiRefNumber = s"DD$i", + submissionDateTime = now.minusDays(i), + bankSortCode = "123456", + bankAccountNumber = "12345678", + bankAccountName = s"Test Account $i", + auDdisFlag = false, + numberOfPayPlans = 1 + ) + } + } +} diff --git a/test/viewmodels/govuk/PaginationViewModelSpec.scala b/test/viewmodels/govuk/PaginationViewModelSpec.scala deleted file mode 100644 index 2862c8bb..00000000 --- a/test/viewmodels/govuk/PaginationViewModelSpec.scala +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2025 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 base.SpecBase -import org.scalatest.matchers.must.Matchers -import viewmodels.govuk.PaginationFluency.* -import play.api.test.FakeRequest -import play.api.i18n.Messages - -class PaginationViewModelSpec extends SpecBase with Matchers { - - "PaginationViewModel" - { - - "must create basic pagination with default values" in new Setup { - val pagination = PaginationViewModel() - - pagination.items mustBe Nil - pagination.previous mustBe None - pagination.next mustBe None - pagination.landmarkLabel mustBe "Pagination" - pagination.classes mustBe "" - pagination.attributes mustBe Map.empty - } - - "must create pagination with items" in new Setup { - val items = Seq( - PaginationItemViewModel("1", "/page/1"), - PaginationItemViewModel("2", "/page/2") - ) - val pagination = PaginationViewModel(items) - - pagination.items mustBe items - } - - "must support fluent API for building pagination" in new Setup { - val pagination = PaginationViewModel() - .withItems(Seq(PaginationItemViewModel("1", "/page/1"))) - .withPrevious(PaginationLinkViewModel("/prev")) - .withNext(PaginationLinkViewModel("/next")) - .withLandmarkLabel("Custom Label") - .withClasses("custom-class") - .withAttributes(Map("data-test" -> "pagination")) - - pagination.items.size mustBe 1 - pagination.previous mustBe defined - pagination.next mustBe defined - pagination.landmarkLabel mustBe "Custom Label" - pagination.classes mustBe "custom-class" - pagination.attributes mustBe Map("data-test" -> "pagination") - } - - "must convert to GovUK Pagination correctly" in new Setup { - val pagination = PaginationViewModel( - items = Seq( - PaginationItemViewModel("1", "/page/1"), - PaginationItemViewModel("2", "/page/2").withCurrent(true) - ), - previous = Some(PaginationLinkViewModel("/prev").withText("Previous")), - next = Some(PaginationLinkViewModel("/next").withText("Next")), - landmarkLabel = "Test Pagination", - classes = "test-class", - attributes = Map("data-test" -> "value") - ) - - val govukPagination = pagination.asPagination - - govukPagination.items.get.size mustBe 2 - govukPagination.previous mustBe defined - govukPagination.next mustBe defined - govukPagination.landmarkLabel mustBe Some("Test Pagination") - govukPagination.classes mustBe "test-class" - govukPagination.attributes mustBe Map("data-test" -> "value") - } - } - - "PaginationItemViewModel" - { - - "must create basic item with number and href" in new Setup { - val item = PaginationItemViewModel("1", "/page/1") - - item.number mustBe "1" - item.href mustBe "/page/1" - item.visuallyHiddenText mustBe None - item.current mustBe false - item.ellipsis mustBe false - item.attributes mustBe Map.empty - } - - "must create ellipsis item" in new Setup { - val item = PaginationItemViewModel.ellipsis() - - item.number mustBe "" - item.href mustBe "" - item.ellipsis mustBe true - } - - "must support fluent API for building items" in new Setup { - val item = PaginationItemViewModel("1", "/page/1") - .withVisuallyHiddenText("Go to page 1") - .withCurrent(true) - .withAttributes(Map("data-test" -> "item")) - - item.visuallyHiddenText mustBe Some("Go to page 1") - item.current mustBe true - item.attributes mustBe Map("data-test" -> "item") - } - - "must convert to GovUK PaginationItem correctly" in new Setup { - val item = PaginationItemViewModel("1", "/page/1") - .withVisuallyHiddenText("Go to page 1") - .withCurrent(true) - .withAttributes(Map("data-test" -> "item")) - - val govukItem = item.asPaginationItem - - govukItem.number mustBe Some("1") - govukItem.href mustBe "/page/1" - govukItem.visuallyHiddenText mustBe Some("Go to page 1") - govukItem.current mustBe Some(true) - govukItem.ellipsis mustBe None - govukItem.attributes mustBe Map("data-test" -> "item") - } - - "must convert ellipsis item to GovUK PaginationItem correctly" in new Setup { - val item = PaginationItemViewModel.ellipsis() - val govukItem = item.asPaginationItem - - govukItem.number mustBe None - govukItem.href mustBe "" - govukItem.ellipsis mustBe Some(true) - } - } - - "PaginationLinkViewModel" - { - - "must create basic link with href" in new Setup { - val link = PaginationLinkViewModel("/page/1") - - link.href mustBe "/page/1" - link.text mustBe None - link.html mustBe None - link.labelText mustBe None - link.attributes mustBe Map.empty - } - - "must support fluent API for building links" in new Setup { - val link = PaginationLinkViewModel("/page/1") - .withText("Next page") - .withLabelText("More results") - .withAttributes(Map("data-test" -> "link")) - - link.text mustBe Some("Next page") - link.labelText mustBe Some("More results") - link.attributes mustBe Map("data-test" -> "link") - } - - "must convert to GovUK PaginationLink correctly" in new Setup { - val link = PaginationLinkViewModel("/page/1") - .withText("Next page") - .withLabelText("More results") - .withAttributes(Map("data-test" -> "link")) - - val govukLink = link.asPaginationLink - - govukLink.href mustBe "/page/1" - govukLink.text mustBe Some("Next page") - govukLink.labelText mustBe Some("More results") - govukLink.attributes mustBe Map("data-test" -> "link") - } - - "must handle HTML content in links" in new Setup { - val link = PaginationLinkViewModel("/page/1") - .withHtml("Next") - - val govukLink = link.asPaginationLink - - govukLink.href mustBe "/page/1" - govukLink.text mustBe None - } - } - - trait Setup { - val app = applicationBuilder().build() - implicit val request: play.api.mvc.Request[?] = FakeRequest() - implicit val messages: Messages = play.api.i18n.MessagesImpl( - play.api.i18n.Lang.defaultLang, - app.injector.instanceOf[play.api.i18n.MessagesApi] - ) - } -} diff --git a/test/views/components/DirectDebitPaginationSpec.scala b/test/views/components/DirectDebitPaginationSpec.scala index 2175ef10..9dcec2a9 100644 --- a/test/views/components/DirectDebitPaginationSpec.scala +++ b/test/views/components/DirectDebitPaginationSpec.scala @@ -20,7 +20,7 @@ import views.html.components.DirectDebitPagination import play.api.test.FakeRequest import play.api.i18n.Messages import org.jsoup.Jsoup -import viewmodels.govuk.PaginationFluency._ +import viewmodels.govuk.PaginationFluency.* class DirectDebitPaginationSpec extends SpecBase with Matchers { @@ -145,7 +145,7 @@ class DirectDebitPaginationSpec extends SpecBase with Matchers { PaginationItemViewModel("2", "/page/2").withCurrent(true) ), previous = Some(PaginationLinkViewModel("/page/1").withText("Go back")), - next = Some(PaginationLinkViewModel("/page/3").withText("Continue")) + next = Some(PaginationLinkViewModel("/page/3").withText("Continue")) ) val html = paginationComponent(pagination) @@ -161,7 +161,7 @@ class DirectDebitPaginationSpec extends SpecBase with Matchers { PaginationItemViewModel("2", "/page/2").withCurrent(true) ), previous = Some(PaginationLinkViewModel("/page/1").withText("Previous").withLabelText("Search results")), - next = Some(PaginationLinkViewModel("/page/3").withText("Next").withLabelText("More results")) + next = Some(PaginationLinkViewModel("/page/3").withText("Next").withLabelText("More results")) ) val html = paginationComponent(pagination) From 0fdb64d879b2f19190cb32ea42de389cf1a0240f Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 13:36:31 +0100 Subject: [PATCH 4/8] [DTR-292] Static text in translation file --- app/services/PaginationService.scala | 5 +++-- app/viewmodels/govuk/PaginationFluency.scala | 21 ++++++++++---------- conf/messages.en | 3 +++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/services/PaginationService.scala b/app/services/PaginationService.scala index 77e286aa..82638291 100644 --- a/app/services/PaginationService.scala +++ b/app/services/PaginationService.scala @@ -18,6 +18,7 @@ package services import models.{DirectDebitDetails, NddDetails} import viewmodels.govuk.PaginationFluency.* +import utils.Utils.emptyString import java.time.LocalDateTime import javax.inject.{Inject, Singleton} @@ -84,11 +85,11 @@ class PaginationService @Inject() { val items = generatePageItems(currentPage, totalPages, baseUrl) val previous = if (currentPage > 1) { - Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage - 1}").withText("Previous page")) + Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage - 1}").withText("site.pagination.previous")) } else None val next = if (currentPage < totalPages) { - Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage + 1}").withText("Next page")) + Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage + 1}").withText("site.pagination.next")) } else None PaginationViewModel( diff --git a/app/viewmodels/govuk/PaginationFluency.scala b/app/viewmodels/govuk/PaginationFluency.scala index 8cfdd072..1c506f4b 100644 --- a/app/viewmodels/govuk/PaginationFluency.scala +++ b/app/viewmodels/govuk/PaginationFluency.scala @@ -18,6 +18,7 @@ package viewmodels.govuk import play.api.i18n.Messages import uk.gov.hmrc.govukfrontend.views.viewmodels.pagination.{Pagination, PaginationItem, PaginationLink} +import utils.Utils.emptyString object PaginationFluency { @@ -28,8 +29,8 @@ object PaginationFluency { items = Nil, previous = None, next = None, - landmarkLabel = "Pagination", - classes = "", + landmarkLabel = "site.pagination.landmark", + classes = emptyString, attributes = Map.empty ) @@ -38,8 +39,8 @@ object PaginationFluency { items = items, previous = None, next = None, - landmarkLabel = "Pagination", - classes = "", + landmarkLabel = "site.pagination.landmark", + classes = emptyString, attributes = Map.empty ) } @@ -48,8 +49,8 @@ object PaginationFluency { items: Seq[PaginationItemViewModel] = Nil, previous: Option[PaginationLinkViewModel] = None, next: Option[PaginationLinkViewModel] = None, - landmarkLabel: String = "Pagination", - classes: String = "", + landmarkLabel: String = "site.pagination.landmark", + classes: String = emptyString, attributes: Map[String, String] = Map.empty ) { @@ -97,8 +98,8 @@ object PaginationFluency { def ellipsis(): PaginationItemViewModel = PaginationItemViewModel( - number = "", - href = "", + number = emptyString, + href = emptyString, visuallyHiddenText = None, current = false, ellipsis = true, @@ -127,7 +128,7 @@ object PaginationFluency { def asPaginationItem(implicit messages: Messages): PaginationItem = { PaginationItem( number = if (ellipsis) None else Some(number), - href = if (ellipsis) "" else href, + href = if (ellipsis) emptyString else href, visuallyHiddenText = visuallyHiddenText.map(messages(_)), current = if (current) Some(true) else None, ellipsis = if (ellipsis) Some(true) else None, @@ -179,4 +180,4 @@ object PaginationFluency { } } -trait PaginationFluency +trait PaginationFluency \ No newline at end of file diff --git a/conf/messages.en b/conf/messages.en index 65d818e1..63599870 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -17,6 +17,9 @@ site.govuk = GOV.UK site.accept.continue = Accept and continue site.next = Next site.previous = Previous +site.pagination.landmark = Pagination +site.pagination.previous = Previous page +site.pagination.next = Next page date.day = Day date.month = Month From 10263c160264e45f0153bc66aee80d27b0c2b022 Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 13:58:40 +0100 Subject: [PATCH 5/8] [DTR-292] Some refactoring --- app/services/PaginationService.scala | 120 +++++++++++-------- app/viewmodels/govuk/PaginationFluency.scala | 2 +- 2 files changed, 70 insertions(+), 52 deletions(-) diff --git a/app/services/PaginationService.scala b/app/services/PaginationService.scala index 82638291..7c094dbb 100644 --- a/app/services/PaginationService.scala +++ b/app/services/PaginationService.scala @@ -18,11 +18,16 @@ package services import models.{DirectDebitDetails, NddDetails} import viewmodels.govuk.PaginationFluency.* -import utils.Utils.emptyString import java.time.LocalDateTime import javax.inject.{Inject, Singleton} +case class PaginationConfig( + recordsPerPage: Int = 3, + maxRecords: Int = 99, + maxVisiblePages: Int = 5 +) + case class PaginationResult( paginatedData: Seq[DirectDebitDetails], paginationViewModel: PaginationViewModel, @@ -34,96 +39,109 @@ case class PaginationResult( @Singleton class PaginationService @Inject() { - val recordsPerPage = 3 - val maxRecords = 99 + private val config = PaginationConfig() def paginateDirectDebits( allDirectDebits: Seq[NddDetails], currentPage: Int = 1, baseUrl: String ): PaginationResult = { - + val sortedDirectDebits = allDirectDebits .sortBy(_.submissionDateTime)(Ordering[LocalDateTime].reverse) - .take(maxRecords) - + .take(config.maxRecords) + val totalRecords = sortedDirectDebits.length - val totalPages = Math.ceil(totalRecords.toDouble / recordsPerPage).toInt - val validCurrentPage = Math.max(1, Math.min(currentPage, totalPages)) - - val startIndex = (validCurrentPage - 1) * recordsPerPage - val endIndex = Math.min(startIndex + recordsPerPage, totalRecords) - + val totalPages = calculateTotalPages(totalRecords) + val validCurrentPage = validateCurrentPage(currentPage, totalPages) + + val (startIndex, endIndex) = calculatePageIndices(validCurrentPage, totalRecords) + val paginatedData = sortedDirectDebits .slice(startIndex, endIndex) .map(_.toDirectDebitDetails) - + val paginationViewModel = createPaginationViewModel( currentPage = validCurrentPage, - totalPages = totalPages, - baseUrl = baseUrl + totalPages = totalPages, + baseUrl = baseUrl ) - + PaginationResult( - paginatedData = paginatedData, + paginatedData = paginatedData, paginationViewModel = paginationViewModel, - totalRecords = totalRecords, - currentPage = validCurrentPage, - totalPages = totalPages + totalRecords = totalRecords, + currentPage = validCurrentPage, + totalPages = totalPages ) } + private def calculateTotalPages(totalRecords: Int): Int = + Math.ceil(totalRecords.toDouble / config.recordsPerPage).toInt + + private def validateCurrentPage(currentPage: Int, totalPages: Int): Int = + Math.max(1, Math.min(currentPage, totalPages)) + + private def calculatePageIndices(currentPage: Int, totalRecords: Int): (Int, Int) = { + val startIndex = (currentPage - 1) * config.recordsPerPage + val endIndex = Math.min(startIndex + config.recordsPerPage, totalRecords) + (startIndex, endIndex) + } + private def createPaginationViewModel( currentPage: Int, totalPages: Int, baseUrl: String ): PaginationViewModel = { - if (totalPages <= 1) { - return PaginationViewModel() + PaginationViewModel() + } else { + val items = generatePageItems(currentPage, totalPages, baseUrl) + val previous = createPreviousLink(currentPage, baseUrl) + val next = createNextLink(currentPage, totalPages, baseUrl) + + PaginationViewModel( + items = items, + previous = previous, + next = next + ) } - - val items = generatePageItems(currentPage, totalPages, baseUrl) - val previous = if (currentPage > 1) { + } + + private def createPreviousLink(currentPage: Int, baseUrl: String): Option[PaginationLinkViewModel] = + if (currentPage > 1) { Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage - 1}").withText("site.pagination.previous")) } else None - - val next = if (currentPage < totalPages) { + + private def createNextLink(currentPage: Int, totalPages: Int, baseUrl: String): Option[PaginationLinkViewModel] = + if (currentPage < totalPages) { Some(PaginationLinkViewModel(s"$baseUrl?page=${currentPage + 1}").withText("site.pagination.next")) } else None - - PaginationViewModel( - items = items, - previous = previous, - next = next - ) - } private def generatePageItems( currentPage: Int, totalPages: Int, baseUrl: String ): Seq[PaginationItemViewModel] = { - - val maxVisiblePages = 5 - val halfVisible = maxVisiblePages / 2 - - val startPage = Math.max(1, currentPage - halfVisible) - val endPage = Math.min(totalPages, startPage + maxVisiblePages - 1) - - val adjustedStartPage = if (endPage - startPage < maxVisiblePages - 1) { - Math.max(1, endPage - maxVisiblePages + 1) - } else startPage - - val pages = (adjustedStartPage to endPage).toList - - val items = pages.map { page => + val pageRange = calculatePageRange(currentPage, totalPages) + + pageRange.map { page => PaginationItemViewModel( number = page.toString, - href = s"$baseUrl?page=$page" + href = s"$baseUrl?page=$page" ).withCurrent(page == currentPage) } - - items + } + + private def calculatePageRange(currentPage: Int, totalPages: Int): Range = { + val halfVisible = config.maxVisiblePages / 2 + val startPage = Math.max(1, currentPage - halfVisible) + val endPage = Math.min(totalPages, startPage + config.maxVisiblePages - 1) + + val adjustedStartPage = if (endPage - startPage < config.maxVisiblePages - 1) { + Math.max(1, endPage - config.maxVisiblePages + 1) + } else startPage + + adjustedStartPage to endPage } } diff --git a/app/viewmodels/govuk/PaginationFluency.scala b/app/viewmodels/govuk/PaginationFluency.scala index 1c506f4b..14eff99d 100644 --- a/app/viewmodels/govuk/PaginationFluency.scala +++ b/app/viewmodels/govuk/PaginationFluency.scala @@ -180,4 +180,4 @@ object PaginationFluency { } } -trait PaginationFluency \ No newline at end of file +trait PaginationFluency From 893167d6144e9eeb7ed1513005bce455c9d7fac5 Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 14:08:05 +0100 Subject: [PATCH 6/8] [DTR-292] Adding ellipsis logic --- app/services/PaginationService.scala | 21 +++++++++++++++++++-- conf/messages.en | 4 ++-- test/services/PaginationServiceSpec.scala | 21 ++++++++++----------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/services/PaginationService.scala b/app/services/PaginationService.scala index 7c094dbb..52d1de00 100644 --- a/app/services/PaginationService.scala +++ b/app/services/PaginationService.scala @@ -124,13 +124,30 @@ class PaginationService @Inject() { baseUrl: String ): Seq[PaginationItemViewModel] = { val pageRange = calculatePageRange(currentPage, totalPages) + val items = scala.collection.mutable.ListBuffer[PaginationItemViewModel]() - pageRange.map { page => - PaginationItemViewModel( + if (pageRange.head > 1) { + items += PaginationItemViewModel("1", s"$baseUrl?page=1").withCurrent(1 == currentPage) + if (pageRange.head > 2) { + items += PaginationItemViewModel.ellipsis() + } + } + + pageRange.foreach { page => + items += PaginationItemViewModel( number = page.toString, href = s"$baseUrl?page=$page" ).withCurrent(page == currentPage) } + + if (pageRange.last < totalPages) { + if (pageRange.last < totalPages - 1) { + items += PaginationItemViewModel.ellipsis() + } + items += PaginationItemViewModel(totalPages.toString, s"$baseUrl?page=$totalPages").withCurrent(totalPages == currentPage) + } + + items.toSeq } private def calculatePageRange(currentPage: Int, totalPages: Int): Range = { diff --git a/conf/messages.en b/conf/messages.en index 63599870..c55ca3f1 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -18,8 +18,8 @@ site.accept.continue = Accept and continue site.next = Next site.previous = Previous site.pagination.landmark = Pagination -site.pagination.previous = Previous page -site.pagination.next = Next page +site.pagination.previous = Previous +site.pagination.next = Next date.day = Day date.month = Month diff --git a/test/services/PaginationServiceSpec.scala b/test/services/PaginationServiceSpec.scala index 019bfa30..110edc65 100644 --- a/test/services/PaginationServiceSpec.scala +++ b/test/services/PaginationServiceSpec.scala @@ -17,7 +17,7 @@ package services import base.SpecBase -import models.{DirectDebitDetails, NddDetails} +import models.NddDetails import org.scalatest.matchers.must.Matchers import viewmodels.govuk.PaginationFluency.* @@ -92,7 +92,7 @@ class PaginationServiceSpec extends SpecBase with Matchers { NddDetails("DD002", now.minusDays(1), "123456", "12345678", "Test Account", false, 1), NddDetails("DD003", now, "123456", "12345678", "Test Account", false, 1) ) - + val result = paginationService.paginateDirectDebits(testData, baseUrl = "/test") result.paginatedData.head.directDebitReference mustBe "DD003" @@ -102,10 +102,10 @@ class PaginationServiceSpec extends SpecBase with Matchers { "must handle invalid page numbers gracefully" in { val testData = createTestNddDetails(5) - + val resultNegative = paginationService.paginateDirectDebits(testData, currentPage = -1, baseUrl = "/test") resultNegative.currentPage mustBe 1 - + val resultTooHigh = paginationService.paginateDirectDebits(testData, currentPage = 999, baseUrl = "/test") resultTooHigh.currentPage mustBe 2 } @@ -131,18 +131,17 @@ class PaginationServiceSpec extends SpecBase with Matchers { } } - private def createTestNddDetails(count: Int): Seq[NddDetails] = { val now = LocalDateTime.now() (1 to count).map { i => NddDetails( - ddiRefNumber = s"DD$i", + ddiRefNumber = s"DD$i", submissionDateTime = now.minusDays(i), - bankSortCode = "123456", - bankAccountNumber = "12345678", - bankAccountName = s"Test Account $i", - auDdisFlag = false, - numberOfPayPlans = 1 + bankSortCode = "123456", + bankAccountNumber = "12345678", + bankAccountName = s"Test Account $i", + auDdisFlag = false, + numberOfPayPlans = 1 ) } } From 9082d2c011f28a6aa50fda9d62a54bb38fbb7262 Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 14:19:52 +0100 Subject: [PATCH 7/8] [DTR-292] Removing duplicates --- conf/messages.en | 2 -- 1 file changed, 2 deletions(-) diff --git a/conf/messages.en b/conf/messages.en index c55ca3f1..74bddc96 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -15,8 +15,6 @@ site.startAgain = Start again site.signIn = Sign in site.govuk = GOV.UK site.accept.continue = Accept and continue -site.next = Next -site.previous = Previous site.pagination.landmark = Pagination site.pagination.previous = Previous site.pagination.next = Next From d92dd8f07225367445660673291d5b1ddda875ea Mon Sep 17 00:00:00 2001 From: wg-hmrc Date: Sun, 19 Oct 2025 18:40:02 +0100 Subject: [PATCH 8/8] [DTR-292] Fix pagination landmark label --- app/viewmodels/govuk/PaginationFluency.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/viewmodels/govuk/PaginationFluency.scala b/app/viewmodels/govuk/PaginationFluency.scala index 14eff99d..6b34240a 100644 --- a/app/viewmodels/govuk/PaginationFluency.scala +++ b/app/viewmodels/govuk/PaginationFluency.scala @@ -77,7 +77,7 @@ object PaginationFluency { items = Some(items.map(_.asPaginationItem)), previous = previous.map(_.asPaginationLink), next = next.map(_.asPaginationLink), - landmarkLabel = Some(landmarkLabel), + landmarkLabel = Some(messages(landmarkLabel)), classes = classes, attributes = attributes )