From e0df7d747d7536bfd144ea3179dfe497fbe0cd49 Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Wed, 5 Aug 2026 16:17:42 +0100 Subject: [PATCH 1/8] PTAD-239: Add LEPP tile behind feature toggle Implement Low Earners' Pension Payment (LEPP) tile integration with feature flag control, LEPP connector, service, and test coverage. --- app/config/ConfigDecorator.scala | 8 + app/config/HmrcModule.scala | 2 + app/connectors/LeppConnector.scala | 97 ++++++++++ app/models/LeppSummaryResponse.scala | 25 +++ app/models/admin/FeatureFlags.scala | 11 ++ app/services/HomePageServicesProvider.scala | 63 +++++-- app/services/LeppService.scala | 54 ++++++ app/util/RateLimiter.scala | 4 + conf/application.conf | 12 ++ conf/messages | 8 +- conf/messages.cy | 10 +- .../connectors/CachingLeppConnectorSpec.scala | 61 +++++++ .../connectors/DefaultLeppConnectorSpec.scala | 70 ++++++++ test/controllers/HomeControllerSpec.scala | 6 +- .../HomePageServicesProviderSpec.scala | 169 +++++------------- test/services/LeppServiceSpec.scala | 126 +++++++++++++ test/views/html/HomeViewSpec.scala | 35 ---- .../options/TaxesAndBenefitsViewSpec.scala | 16 -- 18 files changed, 579 insertions(+), 198 deletions(-) create mode 100644 app/connectors/LeppConnector.scala create mode 100644 app/models/LeppSummaryResponse.scala create mode 100644 app/services/LeppService.scala create mode 100644 test/connectors/CachingLeppConnectorSpec.scala create mode 100644 test/connectors/DefaultLeppConnectorSpec.scala create mode 100644 test/services/LeppServiceSpec.scala diff --git a/app/config/ConfigDecorator.scala b/app/config/ConfigDecorator.scala index 723831851..9c24392e0 100644 --- a/app/config/ConfigDecorator.scala +++ b/app/config/ConfigDecorator.scala @@ -267,6 +267,8 @@ class ConfigDecorator @Inject() ( servicesConfig.getInt("feature.preferences-frontend.timeoutInSec") lazy val enrolmentStoreProxyTimeoutInMilliseconds: Int = servicesConfig.getInt("microservice.services.enrolment-store-proxy.timeoutInMilliseconds") + lazy val leppTimeoutInMilliseconds: Int = + servicesConfig.getInt("microservice.services.low-earners-pensions-payment.timeoutInMilliseconds") lazy val ptaNinoSaveUrl: String = saveYourNationalInsuranceNumberHost + "/save-your-national-insurance-number" lazy val tellUsYourChildIsStayingInFullTimeEducation = "https://www.gov.uk/child-benefit-16-19" @@ -301,6 +303,12 @@ class ConfigDecorator @Inject() ( lazy val mtdGuidanceUrl: String = runModeConfiguration.get[String]("external-url.mtd-guidance.url") + lazy val leppStartUrl: String = + runModeConfiguration.get[String]("external-url.low-earners-pensions-payment.start") + + lazy val leppPaymentsUrl: String = + runModeConfiguration.get[String]("external-url.low-earners-pensions-payment.payments") + lazy val addressChangeBannerTextEn: String = runModeConfiguration.get[String]("feature.address-change-error.banner.paragraph.en") lazy val addressChangeBannerTextCy: String = diff --git a/app/config/HmrcModule.scala b/app/config/HmrcModule.scala index 1157e9834..bad4a38b4 100644 --- a/app/config/HmrcModule.scala +++ b/app/config/HmrcModule.scala @@ -33,6 +33,8 @@ class HmrcModule extends Module { .to[CachingCitizenDetailsConnector], // do not disable caching. The address change relies on the cache bind[TaiConnector].qualifiedWith("default").to[DefaultTaiConnector], bind[TaiConnector].to[CachingTaiConnector], + bind[LeppConnector].qualifiedWith("default").to[DefaultLeppConnector], + bind[LeppConnector].to[CachingLeppConnector], bind[EnrolmentsConnector].qualifiedWith("default").to[DefaultEnrolmentsConnector], bind[EnrolmentsConnector].to[CachingEnrolmentsConnector], bind[Encrypter with Decrypter].toProvider[CryptoProvider], diff --git a/app/connectors/LeppConnector.scala b/app/connectors/LeppConnector.scala new file mode 100644 index 000000000..ce6254859 --- /dev/null +++ b/app/connectors/LeppConnector.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2026 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 connectors + +import cats.data.EitherT +import com.google.common.util.concurrent.RateLimiter +import com.google.inject.name.Named +import com.google.inject.{Inject, Singleton} +import config.ConfigDecorator +import models.LeppSummaryResponse +import play.api.Logging +import play.api.mvc.Request +import services.CacheService +import uk.gov.hmrc.http.HttpReads.Implicits.* +import uk.gov.hmrc.http.client.HttpClientV2 +import uk.gov.hmrc.http.{HeaderCarrier, HttpResponse, StringContextOps, UpstreamErrorResponse} +import uk.gov.hmrc.play.bootstrap.config.ServicesConfig +import util.{Limiters, Throttle} + +import java.util.UUID.randomUUID +import scala.concurrent.duration.DurationInt +import scala.concurrent.{ExecutionContext, Future} + +trait LeppConnector { + def getLeppSummary(implicit + hc: HeaderCarrier, + ec: ExecutionContext, + request: Request[?] + ): EitherT[Future, UpstreamErrorResponse, LeppSummaryResponse] +} + +@Singleton +class DefaultLeppConnector @Inject() ( + val httpClientV2: HttpClientV2, + servicesConfig: ServicesConfig, + httpClientResponse: HttpClientResponse, + configDecorator: ConfigDecorator, + limiters: Limiters +) extends LeppConnector + with Throttle + with Logging { + + override val rateLimiter: RateLimiter = limiters.rateLimiterForLeppSummary + private lazy val baseUrl: String = servicesConfig.baseUrl("low-earners-pensions-payment") + + override def getLeppSummary(implicit + hc: HeaderCarrier, + ec: ExecutionContext, + request: Request[?] + ): EitherT[Future, UpstreamErrorResponse, LeppSummaryResponse] = { + val url = s"$baseUrl/low-earners-pensions-payment/get-lepp-summary" + + implicit val leppHeaderCarrier: HeaderCarrier = hc.withExtraHeaders( + "correlationId" -> randomUUID.toString + ) + + val response: Future[Either[UpstreamErrorResponse, HttpResponse]] = + withThrottle { + httpClientV2 + .get(url"$url")(leppHeaderCarrier) + .transform(_.withRequestTimeout(configDecorator.leppTimeoutInMilliseconds.milliseconds)) + .execute[Either[UpstreamErrorResponse, HttpResponse]](readEitherOf(readRaw), ec) + } + + httpClientResponse.read(response).map(_.json.as[LeppSummaryResponse]) + } +} + +@Singleton +class CachingLeppConnector @Inject() ( + @Named("default") underlying: LeppConnector, + cacheService: CacheService +) extends LeppConnector { + + override def getLeppSummary(implicit + hc: HeaderCarrier, + ec: ExecutionContext, + request: Request[?] + ): EitherT[Future, UpstreamErrorResponse, LeppSummaryResponse] = + cacheService.cache("leppSummary") { + underlying.getLeppSummary + } +} diff --git a/app/models/LeppSummaryResponse.scala b/app/models/LeppSummaryResponse.scala new file mode 100644 index 000000000..521b6f433 --- /dev/null +++ b/app/models/LeppSummaryResponse.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models + +import play.api.libs.json.{Json, OFormat} + +case class LeppSummaryResponse(status: String) + +object LeppSummaryResponse { + implicit val formats: OFormat[LeppSummaryResponse] = Json.format[LeppSummaryResponse] +} diff --git a/app/models/admin/FeatureFlags.scala b/app/models/admin/FeatureFlags.scala index 13ebcf9f0..67680e57d 100644 --- a/app/models/admin/FeatureFlags.scala +++ b/app/models/admin/FeatureFlags.scala @@ -40,6 +40,7 @@ object AllFeatureFlags { MTDUserStatusToggle, GetMatchingFromCitizenDetailsToggle, ClaimMtdFromPtaToggle, + LowEarnersPensionsPaymentToggle, HomePageChangesBannerToggle, HomePagePersonalisationToggle, PtapActivityTabToggle @@ -207,6 +208,16 @@ case object ClaimMtdFromPtaToggle extends FeatureFlagName { Seq(Environment.Staging, Environment.Qa, Environment.Production) } +case object LowEarnersPensionsPaymentToggle extends FeatureFlagName { + override val name: String = "low-earners-pensions-payment-toggle" + + override val description: Option[String] = Some( + "Enable/disable Low earner's pension payment tile in Taxes and benefits" + ) + + override val defaultState: Boolean = false +} + case object HomePageChangesBannerToggle extends FeatureFlagName { override val name: String = "home-change-banner-toggle" diff --git a/app/services/HomePageServicesProvider.scala b/app/services/HomePageServicesProvider.scala index cc68022eb..2155b16d7 100644 --- a/app/services/HomePageServicesProvider.scala +++ b/app/services/HomePageServicesProvider.scala @@ -34,12 +34,19 @@ class HomePageServicesProvider @Inject() ( configDecorator: ConfigDecorator, featureFlagService: FeatureFlagService, fandFService: FandFService, - taiService: TaiService + taiService: TaiService, + leppService: LeppService )(implicit ec: ExecutionContext) extends CurrentTaxYear { private val MtdItsaEnrolmentKey = "HMRC-MTD-IT" + private def redesignHint(isRedesign: Boolean, hint: String): Option[String] = + Option.when(isRedesign)(hint) + + private def nationalInsuranceHint(isRedesign: Boolean)(implicit messages: Messages): Option[String] = + Option.when(isRedesign)(s"${messages("label.view_national_insurance")} ${messages("label.view_state_pension")}") + def getHomePageServices(isRedesign: Boolean = false)(implicit request: UserRequest[?], hc: HeaderCarrier, @@ -49,11 +56,11 @@ class HomePageServicesProvider @Inject() ( val isTrustedHelperUser = request.trustedHelper.isDefined val nino = request.authNino - def marriageAllowanceF(isRedesign: Boolean): Future[Seq[HomePageService]] = + val marriageAllowanceF: Future[Seq[HomePageService]] = if (isTrustedHelperUser) Future.successful(Seq.empty) else taiService.getTaxComponentsList(nino, current.currentYear).map(buildMarriageAllowanceServices(_, isRedesign)) - def trustedHelperF(isRedesign: Boolean): Future[Seq[HomePageService]] = + val trustedHelperF: Future[Seq[HomePageService]] = if (isTrustedHelperUser) Future.successful(Seq.empty) else fandFService.isAnyFandFRelationships(nino).map(buildTrustedHelperServices(_, isRedesign)) @@ -65,9 +72,10 @@ class HomePageServicesProvider @Inject() ( selfAssessmentOther <- getOtherSelfAssessment(request.saUserType, isTrustedHelperUser) mtdOther <- getMtdOtherService(isTrustedHelperUser) childBenefit <- getChildBenefit(isTrustedHelperUser, isRedesign) + lepp <- getLepp(isTrustedHelperUser) annualTaxSummary <- getAnnualTaxSummaries(isTrustedHelperUser, isRedesign) - marriageAllowance <- marriageAllowanceF(isRedesign) - trustedHelper <- trustedHelperF(isRedesign) + marriageAllowance <- marriageAllowanceF + trustedHelper <- trustedHelperF } yield HomePageServices( Seq( payAsYouEarn, @@ -77,6 +85,7 @@ class HomePageServicesProvider @Inject() ( selfAssessmentOther, mtdOther, childBenefit, + lepp, annualTaxSummary ).flatten ++ marriageAllowance ++ trustedHelper ) @@ -89,7 +98,9 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.mtd_for_itsa"), Some(href), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + Some( + s"${messages("label.view_manage_your_mtd_it")} ${messages("label.online_deadline_tax_returns", (current.currentYear + 1).toString)}" + ), gaAction = Some("Income"), gaLabel = Some("MTD IT & SA"), id = Some("itsa") @@ -125,6 +136,16 @@ class HomePageServicesProvider @Inject() ( hintText = Some(messages("label.mtdit.p1")) ) + private def leppTile(linkUrl: String)(implicit messages: Messages): MyService = + MyService( + messages("label.lepp.title"), + Some(linkUrl), + gaAction = Some("Benefits"), + gaLabel = Some("Low earner's pension payment (LEPP)"), + id = Some("lepp"), + hintText = Some(messages("label.lepp.hint")) + ) + private def getMySelfAssessment( saUserType: SelfAssessmentUserType, enrolments: Set[Enrolment], @@ -163,7 +184,7 @@ class HomePageServicesProvider @Inject() ( Some( mySaTile( href = controllers.routes.SaWrongCredentialsController.landingPage().url, - body = messages("title.signed_in_wrong_account.stop") + body = messages("title.signed_in_wrong_account.h1") ) ) @@ -227,13 +248,22 @@ class HomePageServicesProvider @Inject() ( } } + private def getLepp( + isTrustedHelperUser: Boolean + )(implicit hc: HeaderCarrier, request: UserRequest[?], messages: Messages): Future[Option[MyService]] = + if (isTrustedHelperUser) { + Future.successful(None) + } else { + leppService.getLeppLink.map(_.map(leppTile)) + } + private def getPayAsYouEarn(isRedesign: Boolean)(implicit messages: Messages): Future[Option[MyService]] = Future.successful( Some( MyService( messages("label.pay_as_you_earn_paye"), Some(controllers.routes.RedirectToPayeController.redirectToPaye.url), - Option.when(isRedesign)(messages("label.your_income_from_employers_and_private_pensions_")), + redesignHint(isRedesign, messages("label.your_income_from_employers_and_private_pensions_")), gaAction = Some("Income"), gaLabel = Some("Pay As You Earn (PAYE)"), id = Some("paye") @@ -257,7 +287,8 @@ class HomePageServicesProvider @Inject() ( s"${current.startYear}" ), Some(configDecorator.taxCalcHomePageUrl), - Option.when(isRedesign)( + redesignHint( + isRedesign, messages("label.check_whether_you_paid_too_much_or_too_little_tax_in_a_previous_tax_year") ), gaAction = Some("Income"), @@ -277,9 +308,7 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.new_national_insurance_and_state_pension"), Some(controllers.interstitials.routes.InterstitialController.displayNISP.url), - Option.when(isRedesign)( - s"${messages("label.view_national_insurance")} ${messages("label.view_state_pension")}" - ), + nationalInsuranceHint(isRedesign), gaAction = Some("Income"), gaLabel = Some("National Insurance and State Pension"), id = Some("state-pension") @@ -301,7 +330,7 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Benefits"), gaLabel = Some("Child Benefit"), id = Some("child-benefit"), - hintText = Option.when(isRedesign)(messages("label.get_help_with_the_cost_of_bringing_up_children")) + hintText = redesignHint(isRedesign, messages("label.get_help_with_the_cost_of_bringing_up_children")) ) ) } @@ -321,7 +350,7 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Tax Summaries"), gaLabel = Some("Annual Tax Summary"), id = Some("tax-summary"), - hintText = Option.when(isRedesign)(messages("card.ats.text")) + hintText = redesignHint(isRedesign, messages("card.ats.text")) ) ) } @@ -364,7 +393,7 @@ class HomePageServicesProvider @Inject() ( gaLabel = Some("Marriage Allowance"), id = Some("marriage-allowance"), hintText = - Option.when(isRedesign)(messages("label.transfer_part_of_your_personal_allowance_to_your_partner_")) + redesignHint(isRedesign, messages("label.transfer_part_of_your_personal_allowance_to_your_partner_")) ) ) } @@ -377,7 +406,7 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.trusted_helpers_heading"), Some(configDecorator.manageTrustedHelpersUrl), - Option.when(isRedesign)(messages("label.trusted_helpers_content")), + redesignHint(isRedesign, messages("label.trusted_helpers_content")), gaAction = Some("Account"), gaLabel = Some("Trusted helpers"), id = Some("trusted-helper") @@ -391,7 +420,7 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Account"), gaLabel = Some("Trusted helpers"), id = Some("trusted-helper"), - hintText = Option.when(isRedesign)(messages("label.trusted_helpers_content")) + hintText = redesignHint(isRedesign, messages("label.trusted_helpers_content")) ) ) } diff --git a/app/services/LeppService.scala b/app/services/LeppService.scala new file mode 100644 index 000000000..788f34d44 --- /dev/null +++ b/app/services/LeppService.scala @@ -0,0 +1,54 @@ +/* + * Copyright 2026 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 com.google.inject.Inject +import config.ConfigDecorator +import connectors.LeppConnector +import models.admin.LowEarnersPensionsPaymentToggle +import play.api.Logging +import play.api.mvc.Request +import uk.gov.hmrc.http.HeaderCarrier +import uk.gov.hmrc.mongoFeatureToggles.services.FeatureFlagService + +import scala.concurrent.{ExecutionContext, Future} + +class LeppService @Inject() ( + leppConnector: LeppConnector, + featureFlagService: FeatureFlagService, + configDecorator: ConfigDecorator +)(implicit ec: ExecutionContext) + extends Logging { + + def getLeppLink(implicit hc: HeaderCarrier, request: Request[?]): Future[Option[String]] = + featureFlagService.get(LowEarnersPensionsPaymentToggle).flatMap { toggle => + if (toggle.isEnabled) { + leppConnector.getLeppSummary + .fold(_ => Option.empty[String], response => linkForStatus(response.status)) + .recover { case _ => None } + } else { + Future.successful(None) + } + } + + private def linkForStatus(status: String): Option[String] = + status match { + case "PAYMENTS_AVAILABLE" => Some(configDecorator.leppStartUrl) + case "NO_ACTIONS" => Some(configDecorator.leppPaymentsUrl) + case _ => None + } +} diff --git a/app/util/RateLimiter.scala b/app/util/RateLimiter.scala index 99ccc30c3..9bff4bbae 100644 --- a/app/util/RateLimiter.scala +++ b/app/util/RateLimiter.scala @@ -27,7 +27,11 @@ class Limiters @Inject() (configuration: Configuration) { private lazy val maxTpsForGetClientStatus = configuration .getOptional[Double]("feature.agent-client-relationships.maxTps") .getOrElse(100.0) + private lazy val maxTpsForLeppSummary = configuration + .getOptional[Double]("feature.low-earners-pensions-payment.maxTps") + .getOrElse(100.0) val rateLimiterForGetClientStatus: RateLimiter = RateLimiter.create(maxTpsForGetClientStatus) + val rateLimiterForLeppSummary: RateLimiter = RateLimiter.create(maxTpsForLeppSummary) } case object RateLimitedException extends RuntimeException diff --git a/conf/application.conf b/conf/application.conf index f07d2fbd1..f816132f5 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -105,6 +105,11 @@ microservice { port = 9331 timeoutInMilliseconds = 500 } + low-earners-pensions-payment { + host = localhost + port = 7504 + timeoutInMilliseconds = 500 + } pertax-frontend { host = localhost port = 9232 @@ -283,6 +288,10 @@ external-url { mtd-guidance { url = "https://www.gov.uk/government/publications/extension-of-making-tax-digital-for-income-tax-self-assessment-to-sole-traders-and-landlords/making-tax-digital-for-income-tax-self-assessment-for-sole-traders-and-landlords" } + low-earners-pensions-payment { + start = "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/start" + payments = "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/payments" + } bppSpreadTheCostAdvancePaymentUrl = "https://www.gov.uk/pay-self-assessment-tax-bill/pay-weekly-monthly" } @@ -310,6 +319,9 @@ feature { maxTps = 10 timeoutInSec = 5 } + low-earners-pensions-payment { + maxTps = 100 + } preferences-frontend { timeoutInSec = 5 } diff --git a/conf/messages b/conf/messages index acfd2d941..cc596eaab 100644 --- a/conf/messages +++ b/conf/messages @@ -440,7 +440,7 @@ label.you_owe_hmrc_you_must_pay_by_=You owe HMRC £{0}. You must pay by {1}. label.you_have_no_payments_to_make_to_hmrc=You have no payments to make to HMRC. label.self_assessment=Self Assessment -label.mtd_for_itsa=Self Assessment and Making Tax Digital for Income Tax +label.mtd_for_itsa=Making Tax Digital for Income Tax and Self Assessment label.view_and_manage_your_income_tax_obligations_and_payments=View and manage your Income Tax obligations and payments. label.make_a_payment=Make a payment label.check_if_you_need_to_fill_in_a_tax_return=Check if you need to fill in a tax return @@ -495,11 +495,13 @@ label.your_self_assessment=Your Self Assessment label.online_returns_deadline=The deadline for online returns is 31 January {0}. label.mtd_for_it=Making Tax Digital for Income Tax label.mtd_for_it_sa=Making Tax Digital for Income Tax +label.lepp.title=Low earner''s pension payment (LEPP) +label.lepp.hint=View and accept your low earner''s pension payment. label.send_updates_hmrc_compatible_software=Send updates using HMRC compatible software. label.send_updates_sole_traders=For sole traders and landlords that send quarterly updates using software. label.view_manage_your_mtd_for_it=View and manage Making Tax Digital for Income Tax label.view_manage_your_mtd_itsa=View and manage your Making Tax Digital for Income Tax or access your Self Assessment tax returns. -label.view_manage_your_mtd_it=View and manage Making Tax Digital for Income Tax and access Self Assessment. +label.view_manage_your_mtd_it=View and manage Making Tax Digital for Income Tax, and access Self Assessment. label.itsa_header=Income Tax Self Assessment label.it_header=Income Tax label.view_manage_sa_return=More details about your Self Assessment returns and payments @@ -507,6 +509,7 @@ label.access_your_sa_returns=Access your Self Assessment tax returns label.old_way_sa_returns=The old way for filing your Self Assessment tax returns. label.self_assessment_tax_returns=Self Assessment tax returns label.online_deadline_final_declarations=The deadline for final declarations or online returns is 31 January {0}. +label.online_deadline_tax_returns=The deadline for tax returns is 31 January {0}. label.making_tax_digital=Making Tax Digital label.mtdit.heading=Making Tax Digital for Income Tax @@ -642,7 +645,6 @@ global.page.not.found.error.contact.link=https://www.gov.uk/government/organisat label.access_your_self_assessment=Access your Self Assessment title.signed_in_wrong_account.h1=You are not signed in to the right account -title.signed_in_wrong_account.stop=You are not signed in to the right account. label.signed_in_wrong_account=You used a different account when you first signed-up to view or send your tax returns online. title.sign_in_again.h1=You need to sign back in to Government Gateway using different details diff --git a/conf/messages.cy b/conf/messages.cy index 9621b0141..fd72a9741 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -638,7 +638,6 @@ global.page.not.found.error.contact.link=https://www.gov.uk/government/organisat label.access_your_self_assessment=Cael at eich cyfrif Hunanasesiad title.signed_in_wrong_account.h1=Nid ydych wedi mewngofnodi i’r cyfrif cywir -title.signed_in_wrong_account.stop=Nid ydych wedi mewngofnodi i’r cyfrif cywir. label.signed_in_wrong_account=Defnyddioch gyfrif gwahanol pan wnaethoch gofrestru gyntaf i fwrw golwg dros eich Ffurflenni Treth, neu eu hanfon, ar-lein. title.sign_in_again.h1=Mae angen i chi fewngofnodi eto i Borth y Llywodraeth gan ddefnyddio manylion gwahanol @@ -820,14 +819,17 @@ tax_credits.ended.information.how.continue.li2 = cysylltu â Gwasanaeth Cwsmeria label.mtd_for_it=Troi Treth yn Ddigidol ar gyfer Treth Incwm -label.mtd_for_itsa=Hunanasesiad a Throi Treth yn Ddigidol ar gyfer Treth Incwm -label.view_and_manage_your_income_tax_obligations_and_payments=Bwrw golwg dros eich rhwymedigaethau a’ch taliadau Treth Incwm, a’u rheoli. +label.mtd_for_itsa=Troi Treth yn Ddigidol ar gyfer Treth Incwm a Hunanasesiad +label.view_and_manage_your_income_tax_obligations_and_payments=Bwrw golwg dros a rheoli’ch rhwymedigaethau a’ch taliadau Treth Incwm. label.mtd_for_it_sa=Troi Treth yn Ddigidol ar gyfer Treth Incwm +label.lepp.title=Taliad pensiwn i’r sawl sy’n ennill incwm isel (LEPP) +label.lepp.hint=Bwrw golwg dros a derbyn eich taliad pensiwn i’r sawl sy’n ennill incwm isel. label.send_updates_sole_traders=Ar gyfer unig fasnachwyr a landlordiaid sy’n anfon diweddariadau chwarterol gan ddefnyddio meddalwedd. label.view_manage_your_mtd_for_it=Bwrw golwg dros y cynllun Troi Treth yn Ddigidol ar gyfer Treth Incwm a’i reoli label.view_manage_your_mtd_itsa=Bwrw golwg dros eich cynllun Troi Treth yn Ddigidol ar gyfer Treth Incwm a’i reoli, neu gael at eich Ffurflenni Treth Hunanasesiad. label.view_manage_your_mtd_it=Bwrw golwg dros y cynllun Troi Treth yn Ddigidol ar gyfer Treth Incwm a’i reoli, a chael mynediad at Hunanasesiad. label.it_header=Treth Incwm +label.online_deadline_tax_returns=Y dyddiad cau ar gyfer cyflwyno Ffurflenni Treth yw 31 Ionawr {0}. waiting_for_callback=Os ydych yn aros i gael galwad ffôn yn ôl no_need_to_contact=, does dim angen i chi gysylltu â ni eto. @@ -934,7 +936,7 @@ ptap.support.tab.card.insurance.state.pension.heading=Yswiriant Gwladol a Phensi ptap.support.tab.cards.header=Cymorth ptap.support.tab.card.hmrc.online.link.understanding.account=Deall eich Cyfrif Treth Personol -ptap.support.tab.card.hmrc.online.link.understanding.account.url=/personal-account/support/understanding-your-account +ptap.support.tab.card.hmrc.online.link.understanding.account.url=https://www.gov.uk/mewngofnodi-cofrestru-gwasanaethau-ar-lein-cthef ptap.support.tab.card.hmrc.online.link.extra.support=Cael help gan CThEF os oes angen cymorth ychwanegol arnoch ptap.support.tab.card.hmrc.online.link.extra.support.url=https://www.gov.uk/cael-help-cthem-cymorth-ychwanegol ptap.support.tab.card.hmrc.online.link.technical.support=Cymorth technegol â gwasanaethau ar-lein CThEF diff --git a/test/connectors/CachingLeppConnectorSpec.scala b/test/connectors/CachingLeppConnectorSpec.scala new file mode 100644 index 000000000..1f03b4044 --- /dev/null +++ b/test/connectors/CachingLeppConnectorSpec.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2026 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 connectors + +import cats.data.EitherT +import models.LeppSummaryResponse +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{reset, when} +import play.api.mvc.AnyContentAsEmpty +import play.api.test.FakeRequest +import services.CacheService +import testUtils.{BaseSpec, WireMockHelper} +import uk.gov.hmrc.http.{HeaderCarrier, UpstreamErrorResponse} + +import scala.concurrent.{ExecutionContext, Future} + +class CachingLeppConnectorSpec extends ConnectorSpec with BaseSpec with WireMockHelper { + + private val injectedCacheService: CacheService = app.injector.instanceOf[CacheService] + private val mockLeppConnector: LeppConnector = mock[LeppConnector] + + override implicit val hc: HeaderCarrier = HeaderCarrier() + override implicit lazy val ec: ExecutionContext = scala.concurrent.ExecutionContext.global + + private def connector: CachingLeppConnector = + new CachingLeppConnector(mockLeppConnector, injectedCacheService) + + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + + override def beforeEach(): Unit = { + super.beforeEach() + reset(mockLeppConnector) + } + + "CachingLeppConnector.getLeppSummary" must { + + "fetch from service cache" in { + val response = LeppSummaryResponse("PAYMENTS_AVAILABLE") + when(mockLeppConnector.getLeppSummary(any(), any(), any())) + .thenReturn(EitherT.rightT[Future, UpstreamErrorResponse](response)) + + val result = connector.getLeppSummary.value.futureValue + + result mustBe Right(response) + } + } +} diff --git a/test/connectors/DefaultLeppConnectorSpec.scala b/test/connectors/DefaultLeppConnectorSpec.scala new file mode 100644 index 000000000..29cfed0cc --- /dev/null +++ b/test/connectors/DefaultLeppConnectorSpec.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2026 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 connectors + +import com.github.tomakehurst.wiremock.client.WireMock.{getRequestedFor, matching, urlEqualTo} +import models.LeppSummaryResponse +import org.scalatest.concurrent.IntegrationPatience +import play.api.Application +import play.api.mvc.AnyContentAsEmpty +import play.api.test.FakeRequest +import testUtils.WireMockHelper +import uk.gov.hmrc.http.UpstreamErrorResponse + +class DefaultLeppConnectorSpec extends ConnectorSpec with WireMockHelper with IntegrationPatience { + + override implicit lazy val app: Application = + app( + Map( + "microservice.services.low-earners-pensions-payment.port" -> server.port(), + "microservice.services.low-earners-pensions-payment.timeoutInMilliseconds" -> 1000, + "feature.low-earners-pensions-payment.maxTps" -> 1000 + ) + ) + + private def connector: DefaultLeppConnector = + app.injector.instanceOf[DefaultLeppConnector] + + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + + private val url = "/low-earners-pensions-payment/get-lepp-summary" + + "DefaultLeppConnector.getLeppSummary" must { + + "return the LEPP summary response and send a correlationId header" in { + stubGet(url, OK, Some("""{"status":"PAYMENTS_AVAILABLE","data":{}}""")) + + val result = connector.getLeppSummary.value.futureValue + + result mustBe Right(LeppSummaryResponse("PAYMENTS_AVAILABLE")) + server.verify( + getRequestedFor(urlEqualTo(url)).withHeader( + "correlationId", + matching("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}") + ) + ) + } + + "return Left when the LEPP backend returns an error" in { + stubGet(url, INTERNAL_SERVER_ERROR, None) + + val result = connector.getLeppSummary.value.futureValue + + result mustBe a[Left[UpstreamErrorResponse, _]] + } + } +} diff --git a/test/controllers/HomeControllerSpec.scala b/test/controllers/HomeControllerSpec.scala index 038fd2485..34c15862e 100644 --- a/test/controllers/HomeControllerSpec.scala +++ b/test/controllers/HomeControllerSpec.scala @@ -25,7 +25,7 @@ import models.BreathingSpaceIndicatorResponse.WithinPeriod import models.admin.{GetPersonFromCitizenDetailsToggle, HomePagePersonalisationToggle, PtapActivityTabToggle, ShowPlannedOutageBannerToggle} import models.{BreathingSpaceIndicatorResponse, HomePageServices, MyService, OtherService} import org.jsoup.Jsoup -import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.{any, anyBoolean} import org.mockito.Mockito.{reset, verify, when} import play.api.Application import play.api.i18n.{Lang, Messages, MessagesImpl} @@ -144,7 +144,7 @@ class HomeControllerSpec extends BaseSpec with WireMockHelper with CitizenDetail when(mockConfigDecorator.ptapHomepageNinoRolloutLastNumericDigits) .thenReturn(Seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) - when(mockHomePageServicesProvider.getHomePageServices(any())(any(), any(), any())) + when(mockHomePageServicesProvider.getHomePageServices(anyBoolean())(any(), any(), any())) .thenReturn(Future.successful(HomePageServices(Seq.empty))) when(mockCitizenDetailsService.personDetails(any(), any())(any(), any(), any())) @@ -667,7 +667,7 @@ class HomeControllerSpec extends BaseSpec with WireMockHelper with CitizenDetail hintText = Some("Child Benefit hint") ) - when(mockHomePageServicesProvider.getHomePageServices(any())(any(), any(), any())) + when(mockHomePageServicesProvider.getHomePageServices(anyBoolean())(any(), any(), any())) .thenReturn(Future.successful(HomePageServices(Seq(payeService, childBenefitService)))) val appLocal = appBuilder.build() diff --git a/test/services/HomePageServicesProviderSpec.scala b/test/services/HomePageServicesProviderSpec.scala index 6ca0e9856..48cf3edbd 100644 --- a/test/services/HomePageServicesProviderSpec.scala +++ b/test/services/HomePageServicesProviderSpec.scala @@ -26,7 +26,7 @@ import play.api.i18n.{Lang, Messages, MessagesImpl} import play.api.mvc.AnyContent import play.api.test.FakeRequest import testUtils.BaseSpec -import uk.gov.hmrc.auth.core.{ConfidenceLevel, Enrolment} +import uk.gov.hmrc.auth.core.ConfidenceLevel import uk.gov.hmrc.auth.core.retrieve.Credentials import uk.gov.hmrc.domain.SaUtr import uk.gov.hmrc.mongoFeatureToggles.model.FeatureFlag @@ -42,21 +42,22 @@ class HomePageServicesProviderSpec extends BaseSpec { private val mockFeatureFlagService: FeatureFlagService = mock[FeatureFlagService] private val mockFandFService: FandFService = mock[FandFService] private val mockTaiService: TaiService = mock[TaiService] + private val mockLeppService: LeppService = mock[LeppService] private lazy val service = new HomePageServicesProvider( mockConfigDecorator, mockFeatureFlagService, mockFandFService, - mockTaiService + mockTaiService, + mockLeppService ) implicit lazy val messages: Messages = MessagesImpl(Lang("en"), messagesApi) private def buildRequest( saUserType: SelfAssessmentUserType = NonFilerSelfAssessmentUser, - trustedHelper: Option[TrustedHelper] = None, - hasMtdItsaEnrolment: Boolean = false + trustedHelper: Option[TrustedHelper] = None ): UserRequest[AnyContent] = UserRequest( authNino = generatedNino, @@ -64,7 +65,7 @@ class HomePageServicesProviderSpec extends BaseSpec { credentials = Credentials("credId", "GovernmentGateway"), confidenceLevel = ConfidenceLevel.L200, trustedHelper = trustedHelper, - enrolments = if (hasMtdItsaEnrolment) Set(Enrolment("HMRC-MTD-IT")) else Set.empty, + enrolments = Set.empty, profile = None, breadcrumb = None, request = FakeRequest(), @@ -77,6 +78,7 @@ class HomePageServicesProviderSpec extends BaseSpec { reset(mockFeatureFlagService) reset(mockFandFService) reset(mockTaiService) + reset(mockLeppService) when(mockFeatureFlagService.get(eqTo(ShowTaxCalcTileToggle))) .thenReturn(Future.successful(FeatureFlag(ShowTaxCalcTileToggle, isEnabled = false))) @@ -87,6 +89,9 @@ class HomePageServicesProviderSpec extends BaseSpec { when(mockFandFService.isAnyFandFRelationships(any())(any())) .thenReturn(Future.successful(false)) + when(mockLeppService.getLeppLink(any(), any())) + .thenReturn(Future.successful(None)) + when(mockConfigDecorator.taxCalcHomePageUrl).thenReturn("taxcalc/") when(mockConfigDecorator.taxCalcYearsToShow).thenReturn(4) when(mockConfigDecorator.ssoToActivateSaEnrolmentPinUrl).thenReturn("activate-sa-url") @@ -216,7 +221,7 @@ class HomePageServicesProviderSpec extends BaseSpec { MyService( "Self Assessment", Some(controllers.routes.SaWrongCredentialsController.landingPage().url), - Some(messages("title.signed_in_wrong_account.stop")), + Some(messages("title.signed_in_wrong_account.h1")), Map(), Some("Income"), Some("Self Assessment"), @@ -237,119 +242,6 @@ class HomePageServicesProviderSpec extends BaseSpec { ) } - "return combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT when redesign is true" in { - implicit val request: UserRequest[AnyContent] = - buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) - - val result = service.getHomePageServices().futureValue - - result.myServices must contain( - MyService( - messages("label.mtd_for_itsa"), - Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - Map(), - Some("Income"), - Some("MTD IT & SA"), - id = Some("itsa") - ) - ) - result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) - result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") - result.otherServices.map(_.title) must not contain messages("label.self_assessment") - result.otherServices.map(_.link) must not contain - controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url - } - - "return combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT when redesign is false" in { - implicit val request: UserRequest[AnyContent] = - buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) - - val result = service.getHomePageServices().futureValue - - result.myServices must contain( - MyService( - messages("label.mtd_for_itsa"), - Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - Map(), - Some("Income"), - Some("MTD IT & SA"), - id = Some("itsa") - ) - ) - result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) - result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") - result.otherServices.map(_.title) must not contain messages("label.self_assessment") - result.otherServices.map(_.link) must not contain - controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url - } - - "return combined MTD/SA tile in myServices for wrong credentials user with HMRC-MTD-IT when redesign is true" in { - implicit val request: UserRequest[AnyContent] = - buildRequest(WrongCredentialsSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) - - val result = service.getHomePageServices().futureValue - - result.myServices must contain( - MyService( - messages("label.mtd_for_itsa"), - Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - Map(), - Some("Income"), - Some("MTD IT & SA"), - id = Some("itsa") - ) - ) - result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) - result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") - result.otherServices.map(_.title) must not contain messages("label.self_assessment") - result.otherServices.map(_.link) must not contain - controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url - } - - "return combined MTD/SA tile in myServices for wrong credentials user with HMRC-MTD-IT when redesign is false" in { - implicit val request: UserRequest[AnyContent] = - buildRequest(WrongCredentialsSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) - - val result = service.getHomePageServices().futureValue - - result.myServices must contain( - MyService( - messages("label.mtd_for_itsa"), - Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - Map(), - Some("Income"), - Some("MTD IT & SA"), - id = Some("itsa") - ) - ) - result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) - result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") - result.otherServices.map(_.title) must not contain messages("label.self_assessment") - result.otherServices.map(_.link) must not contain - controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url - } - - "return Welsh content for combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT" in { - implicit val welshMessages: Messages = MessagesImpl(Lang("cy"), messagesApi) - implicit val request: UserRequest[AnyContent] = - buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) - - val result = service.getHomePageServices().futureValue - - val itsaService = result.myServices.find(_.id.contains("itsa")) - itsaService.map(_.title) mustBe Some(welshMessages("label.mtd_for_itsa")) - itsaService.flatMap(_.hintText) mustBe Some( - welshMessages("label.view_and_manage_your_income_tax_obligations_and_payments") - ) - result.myServices.map(_.title) must contain(welshMessages("label.mtd_for_itsa")) - result.otherServices.map(_.title) must not contain welshMessages("label.mtd_for_itsa") - result.otherServices.map(_.title) must not contain welshMessages("label.self_assessment") - } - "return self assessment and MTD in otherServices for not yet activated SA user" in { implicit val request: UserRequest[AnyContent] = buildRequest(NotYetActivatedOnlineFilerSelfAssessmentUser(SaUtr("11"))) @@ -443,7 +335,7 @@ class HomePageServicesProviderSpec extends BaseSpec { verify(mockFeatureFlagService, times(0)).get(eqTo(ShowTaxCalcTileToggle)) } - "return no self assessment, child benefit, annual tax summary, marriage allowance or trusted helpers when trusted helper is active" in { + "return no self assessment, child benefit, LEPP, annual tax summary, marriage allowance or trusted helpers when trusted helper is active" in { implicit val request: UserRequest[AnyContent] = buildRequest( ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), @@ -478,6 +370,43 @@ class HomePageServicesProviderSpec extends BaseSpec { verify(mockTaiService, times(0)).getTaxComponentsList(any(), any())(any(), any()) verify(mockFandFService, times(0)).isAnyFandFRelationships(any())(any()) + verify(mockLeppService, times(0)).getLeppLink(any(), any()) + } + + "return LEPP in myServices when the LEPP service returns a link" in { + implicit val request: UserRequest[AnyContent] = + buildRequest() + val leppStartUrl = + "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/start" + + when(mockLeppService.getLeppLink(any(), any())) + .thenReturn(Future.successful(Some(leppStartUrl))) + + val result = service.getHomePageServices().futureValue + + result.myServices must contain( + MyService( + "Low earner's pension payment (LEPP)", + Some(leppStartUrl), + Some("View and accept your low earner's pension payment."), + Map(), + gaAction = Some("Benefits"), + gaLabel = Some("Low earner's pension payment (LEPP)"), + id = Some("lepp") + ) + ) + + result.otherServices.map(_.id) must not contain Some("lepp") + } + + "return no LEPP service when the LEPP service returns no link" in { + implicit val request: UserRequest[AnyContent] = + buildRequest() + + val result = service.getHomePageServices().futureValue + + result.myServices.map(_.id) must not contain Some("lepp") + result.otherServices.map(_.id) must not contain Some("lepp") } "return no tax calculation service when feature flag is disabled" in { diff --git a/test/services/LeppServiceSpec.scala b/test/services/LeppServiceSpec.scala new file mode 100644 index 000000000..eed7fff0c --- /dev/null +++ b/test/services/LeppServiceSpec.scala @@ -0,0 +1,126 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services + +import cats.data.EitherT +import config.ConfigDecorator +import connectors.LeppConnector +import models.LeppSummaryResponse +import models.admin.LowEarnersPensionsPaymentToggle +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{reset, times, verify, when} +import play.api.http.Status.INTERNAL_SERVER_ERROR +import play.api.mvc.AnyContentAsEmpty +import play.api.test.FakeRequest +import testUtils.BaseSpec +import uk.gov.hmrc.http.UpstreamErrorResponse +import uk.gov.hmrc.mongoFeatureToggles.model.FeatureFlag + +import scala.concurrent.Future + +class LeppServiceSpec extends BaseSpec { + + private val mockLeppConnector: LeppConnector = mock[LeppConnector] + private val mockConfigDecorator: ConfigDecorator = mock[ConfigDecorator] + private val startUrl = + "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/start" + private val paymentsUrl = + "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/payments" + private val sut: LeppService = + new LeppService(mockLeppConnector, mockFeatureFlagService, mockConfigDecorator) + + implicit val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + + override def beforeEach(): Unit = { + super.beforeEach() + reset(mockLeppConnector) + reset(mockConfigDecorator) + + when(mockConfigDecorator.leppStartUrl).thenReturn(startUrl) + when(mockConfigDecorator.leppPaymentsUrl).thenReturn(paymentsUrl) + when(mockFeatureFlagService.get(ArgumentMatchers.eq(LowEarnersPensionsPaymentToggle))) + .thenReturn(Future.successful(FeatureFlag(LowEarnersPensionsPaymentToggle, isEnabled = true))) + } + + private def stubSummary(status: String): Unit = + when(mockLeppConnector.getLeppSummary(any(), any(), any())) + .thenReturn(EitherT.rightT[Future, UpstreamErrorResponse](LeppSummaryResponse(status))) + + "getLeppLink" must { + + "return None and not call LEPP when the toggle is disabled" in { + when(mockFeatureFlagService.get(ArgumentMatchers.eq(LowEarnersPensionsPaymentToggle))) + .thenReturn(Future.successful(FeatureFlag(LowEarnersPensionsPaymentToggle, isEnabled = false))) + + sut.getLeppLink.futureValue mustBe None + verify(mockLeppConnector, times(0)).getLeppSummary(any(), any(), any()) + } + + "return the start URL when payments are available" in { + stubSummary("PAYMENTS_AVAILABLE") + + sut.getLeppLink.futureValue mustBe Some(startUrl) + } + + "return the payments URL when no actions are available" in { + stubSummary("NO_ACTIONS") + + sut.getLeppLink.futureValue mustBe Some(paymentsUrl) + } + + "return None when the user is not eligible" in { + stubSummary("NOT_ELIGIBLE") + + sut.getLeppLink.futureValue mustBe None + } + + "return None for CHECK because the ticket does not define a tile action for it" in { + stubSummary("CHECK") + + sut.getLeppLink.futureValue mustBe None + } + + "return None for an unknown status" in { + stubSummary("UNKNOWN") + + sut.getLeppLink.futureValue mustBe None + } + + "return None when the LEPP backend returns an error" in { + when(mockLeppConnector.getLeppSummary(any(), any(), any())) + .thenReturn( + EitherT.leftT[Future, LeppSummaryResponse]( + UpstreamErrorResponse("server error", INTERNAL_SERVER_ERROR) + ) + ) + + sut.getLeppLink.futureValue mustBe None + } + + "return None when the LEPP backend does not respond" in { + when(mockLeppConnector.getLeppSummary(any(), any(), any())) + .thenReturn( + EitherT[Future, UpstreamErrorResponse, LeppSummaryResponse]( + Future.failed(new RuntimeException("No response")) + ) + ) + + sut.getLeppLink.futureValue mustBe None + } + } +} diff --git a/test/views/html/HomeViewSpec.scala b/test/views/html/HomeViewSpec.scala index 94975ae9e..b98c7a4c5 100644 --- a/test/views/html/HomeViewSpec.scala +++ b/test/views/html/HomeViewSpec.scala @@ -19,7 +19,6 @@ package views.html import config.ConfigDecorator import controllers.auth.requests.UserRequest import controllers.bindable.Origin -import models.{MyService, OtherService} import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.mockito.ArgumentMatchers.any @@ -138,40 +137,6 @@ class HomeViewSpec extends ViewSpec { view must include(utr) } - "render combined Self Assessment and MTD supporting text in the current design" in { - implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) - val combinedService = MyService( - messages("label.mtd_for_itsa"), - Some("/itsa"), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - id = Some("itsa") - ) - - val document = asDocument(home(homeViewModel.copy(myServices = Seq(combinedService))).toString) - - document.select("ul#my_taxes li#itsa a").text() mustBe messages("label.mtd_for_itsa") - document.select("ul#my_taxes li#itsa p.govuk-body").text() mustBe messages( - "label.view_and_manage_your_income_tax_obligations_and_payments" - ) - } - - "render Self Assessment other service supporting text in the current design" in { - implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) - val selfAssessmentService = OtherService( - messages("label.self_assessment"), - "/self-assessment", - id = Some("self-assessment"), - hintText = Some(messages("label.activate_your_self_assessment")) - ) - - val document = asDocument(home(homeViewModel.copy(otherServices = Seq(selfAssessmentService))).toString) - - document.select("ul#other_taxes li#self-assessment a").text() mustBe messages("label.self_assessment") - document.select("ul#other_taxes li#self-assessment p.govuk-body").text() mustBe messages( - "label.activate_your_self_assessment" - ) - } - "show the alert banner if there is some alert content" in { implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) diff --git a/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala b/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala index c0b8cc646..6d2121d5d 100644 --- a/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala +++ b/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala @@ -179,22 +179,6 @@ class TaxesAndBenefitsViewSpec extends ViewSpec { document.select("div.hmrc-card p.govuk-hint").text() mustBe "PAYE hint text" } - "render combined Self Assessment and MTD supporting text in the tabular redesign" in { - val combinedService = MyService( - messages("label.mtd_for_itsa"), - Some("/itsa"), - Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), - id = Some("itsa") - ) - - val document = asDocument(page(Seq(combinedService), Seq.empty).toString) - - document.select("div.hmrc-card h4.hmrc-card__heading").text() mustBe messages("label.mtd_for_itsa") - document.select("div.hmrc-card p.govuk-hint").text() mustBe messages( - "label.view_and_manage_your_income_tax_obligations_and_payments" - ) - } - "render otherService hints when present" in { val childBenefitWithHint = childBenefitService.copy(hintText = Some("Child Benefit hint text")) val document = asDocument(page(Seq.empty, Seq(childBenefitWithHint)).toString) From 4e70e189015c979ed98c783d0c440b5303ad4fd7 Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Wed, 5 Aug 2026 16:59:36 +0100 Subject: [PATCH 2/8] PTAD-239 fix: add low-earners-pensions-payment service config to IntegrationSpec RLSInterruptPageSpec and other integration tests were aborting because DefaultLeppConnector injection was failing due to missing low-earners-pensions-payment service port configuration in the test application builder. --- it/test/testUtils/IntegrationSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/it/test/testUtils/IntegrationSpec.scala b/it/test/testUtils/IntegrationSpec.scala index 542153ea2..e26fba84e 100644 --- a/it/test/testUtils/IntegrationSpec.scala +++ b/it/test/testUtils/IntegrationSpec.scala @@ -369,6 +369,7 @@ trait IntegrationSpec "microservice.services.breathing-space-if-proxy.port" -> server.port(), "microservice.services.taxcalc-frontend.port" -> server.port(), "microservice.services.fandf.port" -> server.port(), + "microservice.services.low-earners-pensions-payment.port" -> server.port(), "sca-wrapper.services.single-customer-account-wrapper-data.url" -> s"http://localhost:${server.port()}" ) From f47702281b7e2e00ee2ab7394997d6455e9d393e Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Wed, 5 Aug 2026 16:59:45 +0100 Subject: [PATCH 3/8] PTAD-239 fix: add LeppConnector binding to RLSInterruptPageSpec overrides RLSInterruptPageSpec disables HmrcModule but needs to provide an explicit binding for LeppConnector (now added by HmrcModule) to avoid injection conflicts. --- it/test/address/RLSInterruptPageSpec.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/it/test/address/RLSInterruptPageSpec.scala b/it/test/address/RLSInterruptPageSpec.scala index 44da90dfd..5f17b94a4 100644 --- a/it/test/address/RLSInterruptPageSpec.scala +++ b/it/test/address/RLSInterruptPageSpec.scala @@ -20,6 +20,7 @@ import cats.data.EitherT import com.github.tomakehurst.wiremock.client.WireMock.{get, ok, status as _, urlEqualTo} import config.{ApplicationStartUp, CryptoProvider} import connectors._ +import connectors.{DefaultLeppConnector, LeppConnector} import models.admin.* import org.mockito.ArgumentMatchers import org.mockito.Mockito.when @@ -50,7 +51,8 @@ class RLSInterruptPageSpec extends IntegrationSpec { inject.bind[AgentClientAuthorisationConnector].to[DefaultAgentClientAuthorisationConnector], inject.bind[EnrolmentsConnector].to[DefaultEnrolmentsConnector], inject.bind[Encrypter with Decrypter].toProvider[CryptoProvider], - inject.bind[TaiConnector].to[DefaultTaiConnector] + inject.bind[TaiConnector].to[DefaultTaiConnector], + inject.bind[LeppConnector].to[DefaultLeppConnector] ) .build() From f19db42282d77ba3796647e79283842dc6be534b Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Fri, 7 Aug 2026 17:51:39 +0100 Subject: [PATCH 4/8] PTAD-239 fix: configure LEPP URLs as localhost service host like other external services LEPP external URLs should use absolute localhost:7504 format (matching taxcalc-frontend, tax-summaries-frontend pattern) for local integration testing, not relative URLs. --- conf/application.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/application.conf b/conf/application.conf index f816132f5..2964f0f26 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -289,8 +289,8 @@ external-url { url = "https://www.gov.uk/government/publications/extension-of-making-tax-digital-for-income-tax-self-assessment-to-sole-traders-and-landlords/making-tax-digital-for-income-tax-self-assessment-for-sole-traders-and-landlords" } low-earners-pensions-payment { - start = "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/start" - payments = "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/payments" + start = "http://localhost:7504/accept-your-low-earners-pension-payment/start" + payments = "http://localhost:7504/accept-your-low-earners-pension-payment/payments" } bppSpreadTheCostAdvancePaymentUrl = "https://www.gov.uk/pay-self-assessment-tax-bill/pay-weekly-monthly" } From cc1080cba1f9425357c5b2eae48ff64aec42a606 Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Thu, 13 Aug 2026 21:05:18 +0100 Subject: [PATCH 5/8] PTAD-239 fix: refactor LEPP config to standard pattern and remove unrelated changes --- app/config/ConfigDecorator.scala | 11 +-- app/services/HomePageServicesProvider.scala | 77 ++++++++++----------- conf/application.conf | 8 +-- conf/messages | 5 +- conf/messages.cy | 7 +- 5 files changed, 50 insertions(+), 58 deletions(-) diff --git a/app/config/ConfigDecorator.scala b/app/config/ConfigDecorator.scala index 9c24392e0..948032160 100644 --- a/app/config/ConfigDecorator.scala +++ b/app/config/ConfigDecorator.scala @@ -53,9 +53,10 @@ class ConfigDecorator @Inject() ( def seissUrl: String = servicesConfig.baseUrl("self-employed-income-support") - private lazy val formFrontendService = servicesConfig.baseUrl("dfs-digital-forms-frontend") - private lazy val taxCalcFrontendService = servicesConfig.baseUrl("taxcalc-frontend") - private lazy val taxCalcFrontendExternal = getExternalUrl("taxcalc-frontend.host").getOrElse("") + private lazy val formFrontendService = servicesConfig.baseUrl("dfs-digital-forms-frontend") + private lazy val taxCalcFrontendService = servicesConfig.baseUrl("taxcalc-frontend") + private lazy val taxCalcFrontendExternal = getExternalUrl("taxcalc-frontend.host").getOrElse("") + private lazy val leppFrontendService = servicesConfig.baseUrl("low-earners-pensions-payment-frontend") lazy val businessTaxAccountService: String = servicesConfig.baseUrl("business-tax-account") @@ -304,10 +305,10 @@ class ConfigDecorator @Inject() ( lazy val mtdGuidanceUrl: String = runModeConfiguration.get[String]("external-url.mtd-guidance.url") lazy val leppStartUrl: String = - runModeConfiguration.get[String]("external-url.low-earners-pensions-payment.start") + s"$leppFrontendService/accept-your-low-earners-pension-payment/start" lazy val leppPaymentsUrl: String = - runModeConfiguration.get[String]("external-url.low-earners-pensions-payment.payments") + s"$leppFrontendService/accept-your-low-earners-pension-payment/payments" lazy val addressChangeBannerTextEn: String = runModeConfiguration.get[String]("feature.address-change-error.banner.paragraph.en") diff --git a/app/services/HomePageServicesProvider.scala b/app/services/HomePageServicesProvider.scala index 2155b16d7..fd87eb721 100644 --- a/app/services/HomePageServicesProvider.scala +++ b/app/services/HomePageServicesProvider.scala @@ -41,12 +41,6 @@ class HomePageServicesProvider @Inject() ( private val MtdItsaEnrolmentKey = "HMRC-MTD-IT" - private def redesignHint(isRedesign: Boolean, hint: String): Option[String] = - Option.when(isRedesign)(hint) - - private def nationalInsuranceHint(isRedesign: Boolean)(implicit messages: Messages): Option[String] = - Option.when(isRedesign)(s"${messages("label.view_national_insurance")} ${messages("label.view_state_pension")}") - def getHomePageServices(isRedesign: Boolean = false)(implicit request: UserRequest[?], hc: HeaderCarrier, @@ -56,11 +50,11 @@ class HomePageServicesProvider @Inject() ( val isTrustedHelperUser = request.trustedHelper.isDefined val nino = request.authNino - val marriageAllowanceF: Future[Seq[HomePageService]] = + def marriageAllowanceF(isRedesign: Boolean): Future[Seq[HomePageService]] = if (isTrustedHelperUser) Future.successful(Seq.empty) else taiService.getTaxComponentsList(nino, current.currentYear).map(buildMarriageAllowanceServices(_, isRedesign)) - val trustedHelperF: Future[Seq[HomePageService]] = + def trustedHelperF(isRedesign: Boolean): Future[Seq[HomePageService]] = if (isTrustedHelperUser) Future.successful(Seq.empty) else fandFService.isAnyFandFRelationships(nino).map(buildTrustedHelperServices(_, isRedesign)) @@ -74,8 +68,8 @@ class HomePageServicesProvider @Inject() ( childBenefit <- getChildBenefit(isTrustedHelperUser, isRedesign) lepp <- getLepp(isTrustedHelperUser) annualTaxSummary <- getAnnualTaxSummaries(isTrustedHelperUser, isRedesign) - marriageAllowance <- marriageAllowanceF - trustedHelper <- trustedHelperF + marriageAllowance <- marriageAllowanceF(isRedesign) + trustedHelper <- trustedHelperF(isRedesign) } yield HomePageServices( Seq( payAsYouEarn, @@ -98,9 +92,7 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.mtd_for_itsa"), Some(href), - Some( - s"${messages("label.view_manage_your_mtd_it")} ${messages("label.online_deadline_tax_returns", (current.currentYear + 1).toString)}" - ), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), gaAction = Some("Income"), gaLabel = Some("MTD IT & SA"), id = Some("itsa") @@ -136,16 +128,6 @@ class HomePageServicesProvider @Inject() ( hintText = Some(messages("label.mtdit.p1")) ) - private def leppTile(linkUrl: String)(implicit messages: Messages): MyService = - MyService( - messages("label.lepp.title"), - Some(linkUrl), - gaAction = Some("Benefits"), - gaLabel = Some("Low earner's pension payment (LEPP)"), - id = Some("lepp"), - hintText = Some(messages("label.lepp.hint")) - ) - private def getMySelfAssessment( saUserType: SelfAssessmentUserType, enrolments: Set[Enrolment], @@ -184,7 +166,7 @@ class HomePageServicesProvider @Inject() ( Some( mySaTile( href = controllers.routes.SaWrongCredentialsController.landingPage().url, - body = messages("title.signed_in_wrong_account.h1") + body = messages("title.signed_in_wrong_account.stop") ) ) @@ -248,22 +230,13 @@ class HomePageServicesProvider @Inject() ( } } - private def getLepp( - isTrustedHelperUser: Boolean - )(implicit hc: HeaderCarrier, request: UserRequest[?], messages: Messages): Future[Option[MyService]] = - if (isTrustedHelperUser) { - Future.successful(None) - } else { - leppService.getLeppLink.map(_.map(leppTile)) - } - private def getPayAsYouEarn(isRedesign: Boolean)(implicit messages: Messages): Future[Option[MyService]] = Future.successful( Some( MyService( messages("label.pay_as_you_earn_paye"), Some(controllers.routes.RedirectToPayeController.redirectToPaye.url), - redesignHint(isRedesign, messages("label.your_income_from_employers_and_private_pensions_")), + Option.when(isRedesign)(messages("label.your_income_from_employers_and_private_pensions_")), gaAction = Some("Income"), gaLabel = Some("Pay As You Earn (PAYE)"), id = Some("paye") @@ -287,8 +260,7 @@ class HomePageServicesProvider @Inject() ( s"${current.startYear}" ), Some(configDecorator.taxCalcHomePageUrl), - redesignHint( - isRedesign, + Option.when(isRedesign)( messages("label.check_whether_you_paid_too_much_or_too_little_tax_in_a_previous_tax_year") ), gaAction = Some("Income"), @@ -308,7 +280,9 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.new_national_insurance_and_state_pension"), Some(controllers.interstitials.routes.InterstitialController.displayNISP.url), - nationalInsuranceHint(isRedesign), + Option.when(isRedesign)( + s"${messages("label.view_national_insurance")} ${messages("label.view_state_pension")}" + ), gaAction = Some("Income"), gaLabel = Some("National Insurance and State Pension"), id = Some("state-pension") @@ -330,12 +304,31 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Benefits"), gaLabel = Some("Child Benefit"), id = Some("child-benefit"), - hintText = redesignHint(isRedesign, messages("label.get_help_with_the_cost_of_bringing_up_children")) + hintText = Option.when(isRedesign)(messages("label.get_help_with_the_cost_of_bringing_up_children")) ) ) } } + private def leppTile(linkUrl: String)(implicit messages: Messages): MyService = + MyService( + messages("label.lepp.title"), + Some(linkUrl), + gaAction = Some("Benefits"), + gaLabel = Some("Low earner's pension payment (LEPP)"), + id = Some("lepp"), + hintText = Some(messages("label.lepp.hint")) + ) + + private def getLepp( + isTrustedHelperUser: Boolean + )(implicit hc: HeaderCarrier, request: UserRequest[?], messages: Messages): Future[Option[MyService]] = + if (isTrustedHelperUser) { + Future.successful(None) + } else { + leppService.getLeppLink.map(_.map(leppTile)) + } + private def getAnnualTaxSummaries(isTrustedHelperUser: Boolean, isRedesign: Boolean)(implicit messages: Messages ): Future[Option[OtherService]] = @@ -350,7 +343,7 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Tax Summaries"), gaLabel = Some("Annual Tax Summary"), id = Some("tax-summary"), - hintText = redesignHint(isRedesign, messages("card.ats.text")) + hintText = Option.when(isRedesign)(messages("card.ats.text")) ) ) } @@ -393,7 +386,7 @@ class HomePageServicesProvider @Inject() ( gaLabel = Some("Marriage Allowance"), id = Some("marriage-allowance"), hintText = - redesignHint(isRedesign, messages("label.transfer_part_of_your_personal_allowance_to_your_partner_")) + Option.when(isRedesign)(messages("label.transfer_part_of_your_personal_allowance_to_your_partner_")) ) ) } @@ -406,7 +399,7 @@ class HomePageServicesProvider @Inject() ( MyService( messages("label.trusted_helpers_heading"), Some(configDecorator.manageTrustedHelpersUrl), - redesignHint(isRedesign, messages("label.trusted_helpers_content")), + Option.when(isRedesign)(messages("label.trusted_helpers_content")), gaAction = Some("Account"), gaLabel = Some("Trusted helpers"), id = Some("trusted-helper") @@ -420,7 +413,7 @@ class HomePageServicesProvider @Inject() ( gaAction = Some("Account"), gaLabel = Some("Trusted helpers"), id = Some("trusted-helper"), - hintText = redesignHint(isRedesign, messages("label.trusted_helpers_content")) + hintText = Option.when(isRedesign)(messages("label.trusted_helpers_content")) ) ) } diff --git a/conf/application.conf b/conf/application.conf index 2964f0f26..ce2cc7c5e 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -288,9 +288,8 @@ external-url { mtd-guidance { url = "https://www.gov.uk/government/publications/extension-of-making-tax-digital-for-income-tax-self-assessment-to-sole-traders-and-landlords/making-tax-digital-for-income-tax-self-assessment-for-sole-traders-and-landlords" } - low-earners-pensions-payment { - start = "http://localhost:7504/accept-your-low-earners-pension-payment/start" - payments = "http://localhost:7504/accept-your-low-earners-pension-payment/payments" + low-earners-pensions-payment-frontend { + host = "http://localhost:7503" } bppSpreadTheCostAdvancePaymentUrl = "https://www.gov.uk/pay-self-assessment-tax-bill/pay-weekly-monthly" } @@ -319,9 +318,6 @@ feature { maxTps = 10 timeoutInSec = 5 } - low-earners-pensions-payment { - maxTps = 100 - } preferences-frontend { timeoutInSec = 5 } diff --git a/conf/messages b/conf/messages index cc596eaab..27e55fe6e 100644 --- a/conf/messages +++ b/conf/messages @@ -440,7 +440,7 @@ label.you_owe_hmrc_you_must_pay_by_=You owe HMRC £{0}. You must pay by {1}. label.you_have_no_payments_to_make_to_hmrc=You have no payments to make to HMRC. label.self_assessment=Self Assessment -label.mtd_for_itsa=Making Tax Digital for Income Tax and Self Assessment +label.mtd_for_itsa=Self Assessment and Making Tax Digital for Income Tax label.view_and_manage_your_income_tax_obligations_and_payments=View and manage your Income Tax obligations and payments. label.make_a_payment=Make a payment label.check_if_you_need_to_fill_in_a_tax_return=Check if you need to fill in a tax return @@ -501,7 +501,7 @@ label.send_updates_hmrc_compatible_software=Send updates using HMRC compatible s label.send_updates_sole_traders=For sole traders and landlords that send quarterly updates using software. label.view_manage_your_mtd_for_it=View and manage Making Tax Digital for Income Tax label.view_manage_your_mtd_itsa=View and manage your Making Tax Digital for Income Tax or access your Self Assessment tax returns. -label.view_manage_your_mtd_it=View and manage Making Tax Digital for Income Tax, and access Self Assessment. +label.view_manage_your_mtd_it=View and manage Making Tax Digital for Income Tax and access Self Assessment. label.itsa_header=Income Tax Self Assessment label.it_header=Income Tax label.view_manage_sa_return=More details about your Self Assessment returns and payments @@ -645,6 +645,7 @@ global.page.not.found.error.contact.link=https://www.gov.uk/government/organisat label.access_your_self_assessment=Access your Self Assessment title.signed_in_wrong_account.h1=You are not signed in to the right account +title.signed_in_wrong_account.stop=You are not signed in to the right account. label.signed_in_wrong_account=You used a different account when you first signed-up to view or send your tax returns online. title.sign_in_again.h1=You need to sign back in to Government Gateway using different details diff --git a/conf/messages.cy b/conf/messages.cy index fd72a9741..9f914afee 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -638,6 +638,7 @@ global.page.not.found.error.contact.link=https://www.gov.uk/government/organisat label.access_your_self_assessment=Cael at eich cyfrif Hunanasesiad title.signed_in_wrong_account.h1=Nid ydych wedi mewngofnodi i’r cyfrif cywir +title.signed_in_wrong_account.stop=Nid ydych wedi mewngofnodi i’r cyfrif cywir. label.signed_in_wrong_account=Defnyddioch gyfrif gwahanol pan wnaethoch gofrestru gyntaf i fwrw golwg dros eich Ffurflenni Treth, neu eu hanfon, ar-lein. title.sign_in_again.h1=Mae angen i chi fewngofnodi eto i Borth y Llywodraeth gan ddefnyddio manylion gwahanol @@ -819,8 +820,8 @@ tax_credits.ended.information.how.continue.li2 = cysylltu â Gwasanaeth Cwsmeria label.mtd_for_it=Troi Treth yn Ddigidol ar gyfer Treth Incwm -label.mtd_for_itsa=Troi Treth yn Ddigidol ar gyfer Treth Incwm a Hunanasesiad -label.view_and_manage_your_income_tax_obligations_and_payments=Bwrw golwg dros a rheoli’ch rhwymedigaethau a’ch taliadau Treth Incwm. +label.mtd_for_itsa=Hunanasesiad a Throi Treth yn Ddigidol ar gyfer Treth Incwm +label.view_and_manage_your_income_tax_obligations_and_payments=Bwrw golwg dros eich rhwymedigaethau a’ch taliadau Treth Incwm, a’u rheoli. label.mtd_for_it_sa=Troi Treth yn Ddigidol ar gyfer Treth Incwm label.lepp.title=Taliad pensiwn i’r sawl sy’n ennill incwm isel (LEPP) label.lepp.hint=Bwrw golwg dros a derbyn eich taliad pensiwn i’r sawl sy’n ennill incwm isel. @@ -936,7 +937,7 @@ ptap.support.tab.card.insurance.state.pension.heading=Yswiriant Gwladol a Phensi ptap.support.tab.cards.header=Cymorth ptap.support.tab.card.hmrc.online.link.understanding.account=Deall eich Cyfrif Treth Personol -ptap.support.tab.card.hmrc.online.link.understanding.account.url=https://www.gov.uk/mewngofnodi-cofrestru-gwasanaethau-ar-lein-cthef +ptap.support.tab.card.hmrc.online.link.understanding.account.url=/personal-account/support/understanding-your-account ptap.support.tab.card.hmrc.online.link.extra.support=Cael help gan CThEF os oes angen cymorth ychwanegol arnoch ptap.support.tab.card.hmrc.online.link.extra.support.url=https://www.gov.uk/cael-help-cthem-cymorth-ychwanegol ptap.support.tab.card.hmrc.online.link.technical.support=Cymorth technegol â gwasanaethau ar-lein CThEF From 62e8a138541ceb21e31c3353da260c74f462a0f2 Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Thu, 13 Aug 2026 21:23:26 +0100 Subject: [PATCH 6/8] PTAD-239 fix: refactor LEPP config to standard pattern and remove unrelated changes --- app/config/ConfigDecorator.scala | 8 +- conf/application.conf | 7 +- .../HomePageServicesProviderSpec.scala | 161 +++++++++++++----- 3 files changed, 127 insertions(+), 49 deletions(-) diff --git a/app/config/ConfigDecorator.scala b/app/config/ConfigDecorator.scala index 948032160..46ce33550 100644 --- a/app/config/ConfigDecorator.scala +++ b/app/config/ConfigDecorator.scala @@ -53,10 +53,10 @@ class ConfigDecorator @Inject() ( def seissUrl: String = servicesConfig.baseUrl("self-employed-income-support") - private lazy val formFrontendService = servicesConfig.baseUrl("dfs-digital-forms-frontend") - private lazy val taxCalcFrontendService = servicesConfig.baseUrl("taxcalc-frontend") - private lazy val taxCalcFrontendExternal = getExternalUrl("taxcalc-frontend.host").getOrElse("") - private lazy val leppFrontendService = servicesConfig.baseUrl("low-earners-pensions-payment-frontend") + private lazy val formFrontendService = servicesConfig.baseUrl("dfs-digital-forms-frontend") + private lazy val taxCalcFrontendService = servicesConfig.baseUrl("taxcalc-frontend") + private lazy val taxCalcFrontendExternal = getExternalUrl("taxcalc-frontend.host").getOrElse("") + private lazy val leppFrontendService = servicesConfig.baseUrl("low-earners-pensions-payment-frontend") lazy val businessTaxAccountService: String = servicesConfig.baseUrl("business-tax-account") diff --git a/conf/application.conf b/conf/application.conf index ce2cc7c5e..dfdeb9332 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -105,6 +105,10 @@ microservice { port = 9331 timeoutInMilliseconds = 500 } + low-earners-pensions-payment-frontend { + host = localhost + port = 7503 + } low-earners-pensions-payment { host = localhost port = 7504 @@ -288,9 +292,6 @@ external-url { mtd-guidance { url = "https://www.gov.uk/government/publications/extension-of-making-tax-digital-for-income-tax-self-assessment-to-sole-traders-and-landlords/making-tax-digital-for-income-tax-self-assessment-for-sole-traders-and-landlords" } - low-earners-pensions-payment-frontend { - host = "http://localhost:7503" - } bppSpreadTheCostAdvancePaymentUrl = "https://www.gov.uk/pay-self-assessment-tax-bill/pay-weekly-monthly" } diff --git a/test/services/HomePageServicesProviderSpec.scala b/test/services/HomePageServicesProviderSpec.scala index 48cf3edbd..b20060b5f 100644 --- a/test/services/HomePageServicesProviderSpec.scala +++ b/test/services/HomePageServicesProviderSpec.scala @@ -26,7 +26,7 @@ import play.api.i18n.{Lang, Messages, MessagesImpl} import play.api.mvc.AnyContent import play.api.test.FakeRequest import testUtils.BaseSpec -import uk.gov.hmrc.auth.core.ConfidenceLevel +import uk.gov.hmrc.auth.core.{ConfidenceLevel, Enrolment} import uk.gov.hmrc.auth.core.retrieve.Credentials import uk.gov.hmrc.domain.SaUtr import uk.gov.hmrc.mongoFeatureToggles.model.FeatureFlag @@ -57,7 +57,8 @@ class HomePageServicesProviderSpec extends BaseSpec { private def buildRequest( saUserType: SelfAssessmentUserType = NonFilerSelfAssessmentUser, - trustedHelper: Option[TrustedHelper] = None + trustedHelper: Option[TrustedHelper] = None, + hasMtdItsaEnrolment: Boolean = false ): UserRequest[AnyContent] = UserRequest( authNino = generatedNino, @@ -65,7 +66,7 @@ class HomePageServicesProviderSpec extends BaseSpec { credentials = Credentials("credId", "GovernmentGateway"), confidenceLevel = ConfidenceLevel.L200, trustedHelper = trustedHelper, - enrolments = Set.empty, + enrolments = if (hasMtdItsaEnrolment) Set(Enrolment("HMRC-MTD-IT")) else Set.empty, profile = None, breadcrumb = None, request = FakeRequest(), @@ -221,7 +222,7 @@ class HomePageServicesProviderSpec extends BaseSpec { MyService( "Self Assessment", Some(controllers.routes.SaWrongCredentialsController.landingPage().url), - Some(messages("title.signed_in_wrong_account.h1")), + Some(messages("title.signed_in_wrong_account.stop")), Map(), Some("Income"), Some("Self Assessment"), @@ -242,6 +243,119 @@ class HomePageServicesProviderSpec extends BaseSpec { ) } + "return combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT when redesign is true" in { + implicit val request: UserRequest[AnyContent] = + buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) + + val result = service.getHomePageServices().futureValue + + result.myServices must contain( + MyService( + messages("label.mtd_for_itsa"), + Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + Map(), + Some("Income"), + Some("MTD IT & SA"), + id = Some("itsa") + ) + ) + result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) + result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") + result.otherServices.map(_.title) must not contain messages("label.self_assessment") + result.otherServices.map(_.link) must not contain + controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url + } + + "return combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT when redesign is false" in { + implicit val request: UserRequest[AnyContent] = + buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) + + val result = service.getHomePageServices().futureValue + + result.myServices must contain( + MyService( + messages("label.mtd_for_itsa"), + Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + Map(), + Some("Income"), + Some("MTD IT & SA"), + id = Some("itsa") + ) + ) + result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) + result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") + result.otherServices.map(_.title) must not contain messages("label.self_assessment") + result.otherServices.map(_.link) must not contain + controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url + } + + "return combined MTD/SA tile in myServices for wrong credentials user with HMRC-MTD-IT when redesign is true" in { + implicit val request: UserRequest[AnyContent] = + buildRequest(WrongCredentialsSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) + + val result = service.getHomePageServices().futureValue + + result.myServices must contain( + MyService( + messages("label.mtd_for_itsa"), + Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + Map(), + Some("Income"), + Some("MTD IT & SA"), + id = Some("itsa") + ) + ) + result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) + result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") + result.otherServices.map(_.title) must not contain messages("label.self_assessment") + result.otherServices.map(_.link) must not contain + controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url + } + + "return combined MTD/SA tile in myServices for wrong credentials user with HMRC-MTD-IT when redesign is false" in { + implicit val request: UserRequest[AnyContent] = + buildRequest(WrongCredentialsSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) + + val result = service.getHomePageServices().futureValue + + result.myServices must contain( + MyService( + messages("label.mtd_for_itsa"), + Some(controllers.interstitials.routes.InterstitialController.displayItsaMergePage.url), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + Map(), + Some("Income"), + Some("MTD IT & SA"), + id = Some("itsa") + ) + ) + result.myServices.map(_.title) must contain(messages("label.mtd_for_itsa")) + result.otherServices.map(_.title) must not contain messages("label.mtd_for_itsa") + result.otherServices.map(_.title) must not contain messages("label.self_assessment") + result.otherServices.map(_.link) must not contain + controllers.interstitials.routes.MtdAdvertInterstitialController.displayMTDITPage.url + } + + "return Welsh content for combined MTD/SA tile in myServices for activated online filer with HMRC-MTD-IT" in { + implicit val welshMessages: Messages = MessagesImpl(Lang("cy"), messagesApi) + implicit val request: UserRequest[AnyContent] = + buildRequest(ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), hasMtdItsaEnrolment = true) + + val result = service.getHomePageServices().futureValue + + val itsaService = result.myServices.find(_.id.contains("itsa")) + itsaService.map(_.title) mustBe Some(welshMessages("label.mtd_for_itsa")) + itsaService.flatMap(_.hintText) mustBe Some( + welshMessages("label.view_and_manage_your_income_tax_obligations_and_payments") + ) + result.myServices.map(_.title) must contain(welshMessages("label.mtd_for_itsa")) + result.otherServices.map(_.title) must not contain welshMessages("label.mtd_for_itsa") + result.otherServices.map(_.title) must not contain welshMessages("label.self_assessment") + } + "return self assessment and MTD in otherServices for not yet activated SA user" in { implicit val request: UserRequest[AnyContent] = buildRequest(NotYetActivatedOnlineFilerSelfAssessmentUser(SaUtr("11"))) @@ -335,7 +449,7 @@ class HomePageServicesProviderSpec extends BaseSpec { verify(mockFeatureFlagService, times(0)).get(eqTo(ShowTaxCalcTileToggle)) } - "return no self assessment, child benefit, LEPP, annual tax summary, marriage allowance or trusted helpers when trusted helper is active" in { + "return no self assessment, child benefit, annual tax summary, marriage allowance or trusted helpers when trusted helper is active" in { implicit val request: UserRequest[AnyContent] = buildRequest( ActivatedOnlineFilerSelfAssessmentUser(SaUtr("11")), @@ -370,43 +484,6 @@ class HomePageServicesProviderSpec extends BaseSpec { verify(mockTaiService, times(0)).getTaxComponentsList(any(), any())(any(), any()) verify(mockFandFService, times(0)).isAnyFandFRelationships(any())(any()) - verify(mockLeppService, times(0)).getLeppLink(any(), any()) - } - - "return LEPP in myServices when the LEPP service returns a link" in { - implicit val request: UserRequest[AnyContent] = - buildRequest() - val leppStartUrl = - "https://www.tax.service.gov.uk/accept-your-low-earners-pension-payment/start" - - when(mockLeppService.getLeppLink(any(), any())) - .thenReturn(Future.successful(Some(leppStartUrl))) - - val result = service.getHomePageServices().futureValue - - result.myServices must contain( - MyService( - "Low earner's pension payment (LEPP)", - Some(leppStartUrl), - Some("View and accept your low earner's pension payment."), - Map(), - gaAction = Some("Benefits"), - gaLabel = Some("Low earner's pension payment (LEPP)"), - id = Some("lepp") - ) - ) - - result.otherServices.map(_.id) must not contain Some("lepp") - } - - "return no LEPP service when the LEPP service returns no link" in { - implicit val request: UserRequest[AnyContent] = - buildRequest() - - val result = service.getHomePageServices().futureValue - - result.myServices.map(_.id) must not contain Some("lepp") - result.otherServices.map(_.id) must not contain Some("lepp") } "return no tax calculation service when feature flag is disabled" in { From 3c4b222809acf4d56e0c8d03ddfee95044cd4f38 Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Thu, 13 Aug 2026 21:29:27 +0100 Subject: [PATCH 7/8] PTAD-239 cleanup: remove unused label.online_deadline_tax_returns --- conf/messages | 1 - conf/messages.cy | 1 - 2 files changed, 2 deletions(-) diff --git a/conf/messages b/conf/messages index 27e55fe6e..fef224d0c 100644 --- a/conf/messages +++ b/conf/messages @@ -509,7 +509,6 @@ label.access_your_sa_returns=Access your Self Assessment tax returns label.old_way_sa_returns=The old way for filing your Self Assessment tax returns. label.self_assessment_tax_returns=Self Assessment tax returns label.online_deadline_final_declarations=The deadline for final declarations or online returns is 31 January {0}. -label.online_deadline_tax_returns=The deadline for tax returns is 31 January {0}. label.making_tax_digital=Making Tax Digital label.mtdit.heading=Making Tax Digital for Income Tax diff --git a/conf/messages.cy b/conf/messages.cy index 9f914afee..d0fce82e9 100644 --- a/conf/messages.cy +++ b/conf/messages.cy @@ -830,7 +830,6 @@ label.view_manage_your_mtd_for_it=Bwrw golwg dros y cynllun Troi Treth yn Ddigid label.view_manage_your_mtd_itsa=Bwrw golwg dros eich cynllun Troi Treth yn Ddigidol ar gyfer Treth Incwm a’i reoli, neu gael at eich Ffurflenni Treth Hunanasesiad. label.view_manage_your_mtd_it=Bwrw golwg dros y cynllun Troi Treth yn Ddigidol ar gyfer Treth Incwm a’i reoli, a chael mynediad at Hunanasesiad. label.it_header=Treth Incwm -label.online_deadline_tax_returns=Y dyddiad cau ar gyfer cyflwyno Ffurflenni Treth yw 31 Ionawr {0}. waiting_for_callback=Os ydych yn aros i gael galwad ffôn yn ôl no_need_to_contact=, does dim angen i chi gysylltu â ni eto. From 22c6993447ce17732c0af8c4225719981fe7adab Mon Sep 17 00:00:00 2001 From: Olakunle Kuye Date: Thu, 13 Aug 2026 21:32:17 +0100 Subject: [PATCH 8/8] PTAD-239 fix: restore unrelated view spec tests --- test/views/html/HomeViewSpec.scala | 35 +++++++++++++++++++ .../options/TaxesAndBenefitsViewSpec.scala | 16 +++++++++ 2 files changed, 51 insertions(+) diff --git a/test/views/html/HomeViewSpec.scala b/test/views/html/HomeViewSpec.scala index b98c7a4c5..94975ae9e 100644 --- a/test/views/html/HomeViewSpec.scala +++ b/test/views/html/HomeViewSpec.scala @@ -19,6 +19,7 @@ package views.html import config.ConfigDecorator import controllers.auth.requests.UserRequest import controllers.bindable.Origin +import models.{MyService, OtherService} import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.mockito.ArgumentMatchers.any @@ -137,6 +138,40 @@ class HomeViewSpec extends ViewSpec { view must include(utr) } + "render combined Self Assessment and MTD supporting text in the current design" in { + implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) + val combinedService = MyService( + messages("label.mtd_for_itsa"), + Some("/itsa"), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + id = Some("itsa") + ) + + val document = asDocument(home(homeViewModel.copy(myServices = Seq(combinedService))).toString) + + document.select("ul#my_taxes li#itsa a").text() mustBe messages("label.mtd_for_itsa") + document.select("ul#my_taxes li#itsa p.govuk-body").text() mustBe messages( + "label.view_and_manage_your_income_tax_obligations_and_payments" + ) + } + + "render Self Assessment other service supporting text in the current design" in { + implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) + val selfAssessmentService = OtherService( + messages("label.self_assessment"), + "/self-assessment", + id = Some("self-assessment"), + hintText = Some(messages("label.activate_your_self_assessment")) + ) + + val document = asDocument(home(homeViewModel.copy(otherServices = Seq(selfAssessmentService))).toString) + + document.select("ul#other_taxes li#self-assessment a").text() mustBe messages("label.self_assessment") + document.select("ul#other_taxes li#self-assessment p.govuk-body").text() mustBe messages( + "label.activate_your_self_assessment" + ) + } + "show the alert banner if there is some alert content" in { implicit val userRequest: UserRequest[AnyContentAsEmpty.type] = buildUserRequest(request = FakeRequest()) diff --git a/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala b/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala index 6d2121d5d..c0b8cc646 100644 --- a/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala +++ b/test/views/html/home/options/TaxesAndBenefitsViewSpec.scala @@ -179,6 +179,22 @@ class TaxesAndBenefitsViewSpec extends ViewSpec { document.select("div.hmrc-card p.govuk-hint").text() mustBe "PAYE hint text" } + "render combined Self Assessment and MTD supporting text in the tabular redesign" in { + val combinedService = MyService( + messages("label.mtd_for_itsa"), + Some("/itsa"), + Some(messages("label.view_and_manage_your_income_tax_obligations_and_payments")), + id = Some("itsa") + ) + + val document = asDocument(page(Seq(combinedService), Seq.empty).toString) + + document.select("div.hmrc-card h4.hmrc-card__heading").text() mustBe messages("label.mtd_for_itsa") + document.select("div.hmrc-card p.govuk-hint").text() mustBe messages( + "label.view_and_manage_your_income_tax_obligations_and_payments" + ) + } + "render otherService hints when present" in { val childBenefitWithHint = childBenefitService.copy(hintText = Some("Child Benefit hint text")) val document = asDocument(page(Seq.empty, Seq(childBenefitWithHint)).toString)