Skip to content
8 changes: 8 additions & 0 deletions app/config/ConfigDecorator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions app/config/HmrcModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
97 changes: 97 additions & 0 deletions app/connectors/LeppConnector.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
25 changes: 25 additions & 0 deletions app/models/LeppSummaryResponse.scala
Original file line number Diff line number Diff line change
@@ -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]
}
11 changes: 11 additions & 0 deletions app/models/admin/FeatureFlags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ object AllFeatureFlags {
MTDUserStatusToggle,
GetMatchingFromCitizenDetailsToggle,
ClaimMtdFromPtaToggle,
LowEarnersPensionsPaymentToggle,
HomePageChangesBannerToggle,
HomePagePersonalisationToggle,
PtapActivityTabToggle
Expand Down Expand Up @@ -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"

Expand Down
63 changes: 46 additions & 17 deletions app/services/HomePageServicesProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))

Expand All @@ -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,
Expand All @@ -77,6 +85,7 @@ class HomePageServicesProvider @Inject() (
selfAssessmentOther,
mtdOther,
childBenefit,
lepp,
annualTaxSummary
).flatten ++ marriageAllowance ++ trustedHelper
)
Expand All @@ -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")
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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")
)
)

Expand Down Expand Up @@ -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")
Expand All @@ -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"),
Expand All @@ -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")
Expand All @@ -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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of unrelated changes

)
)
}
Expand All @@ -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"))
)
)
}
Expand Down Expand Up @@ -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_"))
)
)
}
Expand All @@ -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")
Expand All @@ -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"))
)
)
}
Expand Down
Loading