Skip to content
9 changes: 9 additions & 0 deletions app/config/ConfigDecorator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class ConfigDecorator @Inject() (
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")

Expand Down Expand Up @@ -267,6 +268,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 +304,12 @@ class ConfigDecorator @Inject() (

lazy val mtdGuidanceUrl: String = runModeConfiguration.get[String]("external-url.mtd-guidance.url")

lazy val leppStartUrl: String =
s"$leppFrontendService/accept-your-low-earners-pension-payment/start"

lazy val leppPaymentsUrl: String =
s"$leppFrontendService/accept-your-low-earners-pension-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
24 changes: 23 additions & 1 deletion app/services/HomePageServicesProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class HomePageServicesProvider @Inject() (
configDecorator: ConfigDecorator,
featureFlagService: FeatureFlagService,
fandFService: FandFService,
taiService: TaiService
taiService: TaiService,
leppService: LeppService
)(implicit ec: ExecutionContext)
extends CurrentTaxYear {

Expand Down Expand Up @@ -65,6 +66,7 @@ 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)
Expand All @@ -77,6 +79,7 @@ class HomePageServicesProvider @Inject() (
selfAssessmentOther,
mtdOther,
childBenefit,
lepp,
annualTaxSummary
).flatten ++ marriageAllowance ++ trustedHelper
)
Expand Down Expand Up @@ -307,6 +310,25 @@ class HomePageServicesProvider @Inject() (
}
}

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]] =
Expand Down
54 changes: 54 additions & 0 deletions app/services/LeppService.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 4 additions & 0 deletions app/util/RateLimiter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ microservice {
port = 9331
timeoutInMilliseconds = 500
}
low-earners-pensions-payment-frontend {
host = localhost
port = 7503
}
low-earners-pensions-payment {

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.

Also define a low-earners-pensions-payment-frontend, with port 7503

host = localhost
port = 7504
timeoutInMilliseconds = 500
}
pertax-frontend {
host = localhost
port = 9232
Expand Down
2 changes: 2 additions & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ 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
Expand Down
2 changes: 2 additions & 0 deletions conf/messages.cy
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,8 @@ 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_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.
Expand Down
4 changes: 3 additions & 1 deletion it/test/address/RLSInterruptPageSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions it/test/testUtils/IntegrationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()}"
)

Expand Down
Loading