Skip to content

Commit

Permalink
ITSASU-3400 - Implement pre-pop API connection, transforming json and…
Browse files Browse the repository at this point in the history
… cleansing data based on rules
  • Loading branch information
AlexRimmerHMRC committed Nov 7, 2024
1 parent b6fa9dc commit 447a773
Show file tree
Hide file tree
Showing 13 changed files with 836 additions and 8 deletions.
11 changes: 10 additions & 1 deletion app/config/MicroserviceAppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ trait AppConfig {

val signUpServiceAuthorisationToken: String
val signUpServiceEnvironment: String

val prePopURL: String
val prePopAuthorisationToken: String
val prePopEnvironment: String
}


Expand Down Expand Up @@ -79,6 +83,12 @@ class MicroserviceAppConfig @Inject()(servicesConfig: ServicesConfig, val config
override lazy val signUpServiceAuthorisationToken: String = s"Bearer ${loadConfig(s"$signUpServiceBase.authorization-token")}"
override lazy val signUpServiceEnvironment: String = loadConfig(s"$signUpServiceBase.environment")

override lazy val prePopURL: String = servicesConfig.baseUrl("pre-pop")

private val prePopBase = "microservice.services.pre-pop"
override lazy val prePopAuthorisationToken: String = s"Bearer ${loadConfig(s"$prePopBase.authorization-token")}"
override lazy val prePopEnvironment: String = loadConfig(s"$prePopBase.environment")

private def desBase =
if (FeatureSwitching.isEnabled(featureswitch.StubDESFeature, configuration)) "microservice.services.stub-des"
else "microservice.services.des"
Expand All @@ -98,5 +108,4 @@ class MicroserviceAppConfig @Inject()(servicesConfig: ServicesConfig, val config
lazy val timeToLiveSeconds: Int = loadConfig("mongodb.timeToLiveSeconds").toInt
lazy val sessionTimeToLiveSeconds: Int = loadConfig("mongodb.sessionTimeToLiveSeconds").toInt
lazy val throttleTimeToLiveSeconds: Int = loadConfig("mongodb.throttleTimeToLiveSeconds").toInt

}
45 changes: 45 additions & 0 deletions app/connectors/PrePopConnector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2023 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 config.AppConfig
import parsers.PrePopParser.{GetPrePopResponse, GetPrePopResponseHttpReads}
import uk.gov.hmrc.http.{Authorization, HeaderCarrier, HeaderNames, HttpClient, HttpReads}

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class PrePopConnector @Inject()(http: HttpClient,
appConfig: AppConfig)(implicit ec: ExecutionContext) {

def prepopUrl(nino: String): String = s"${appConfig.prePopURL}/income-tax/pre-pop/$nino"

def getPrePopData(nino: String)(implicit hc: HeaderCarrier): Future[GetPrePopResponse] = {

val headerCarrier: HeaderCarrier = hc
.copy(authorization = Some(Authorization(appConfig.prePopAuthorisationToken)))
.withExtraHeaders("Environment" -> appConfig.prePopEnvironment)

val headers: Seq[(String, String)] = Seq(
HeaderNames.authorisation -> appConfig.prePopAuthorisationToken,
"Environment" -> appConfig.prePopEnvironment
)

http.GET[GetPrePopResponse](prepopUrl(nino), headers = headers)(implicitly[HttpReads[GetPrePopResponse]], headerCarrier, implicitly)
}
}
45 changes: 45 additions & 0 deletions app/controllers/PrePopController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2024 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 controllers

import connectors.PrePopConnector
import play.api.Logging
import play.api.libs.json.Json
import play.api.mvc.{Action, AnyContent, ControllerComponents}
import services.AuthService
import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController

import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext

@Singleton
class PrePopController @Inject()(authService: AuthService,
prePopConnector: PrePopConnector,
cc: ControllerComponents)(implicit ec: ExecutionContext) extends BackendController(cc) with Logging {

def prePop(nino: String): Action[AnyContent] = Action.async { implicit request =>
authService.authorised() {
prePopConnector.getPrePopData(nino) map {
case Right(value) => Ok(Json.toJson(value))
case Left(error) =>
logger.error(s"[PrePopController][prePop] - Error when fetching pre-pop data. Status: ${error.status}, Reason: ${error.reason}")
InternalServerError
}
}
}

}
107 changes: 107 additions & 0 deletions app/models/PrePopData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2024 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 models.subscription.Address
import models.subscription.business.{AccountingMethod, Accruals, Cash}
import play.api.libs.functional.syntax.toFunctionalBuilderOps
import play.api.libs.json.{Json, OWrites, Reads, __}
import uk.gov.hmrc.http.InternalServerException

import scala.util.matching.Regex


case class PrePopData(selfEmployment: Option[Seq[PrePopSelfEmployment]],
ukPropertyAccountingMethod: Option[AccountingMethod],
foreignPropertyAccountingMethod: Option[AccountingMethod])

object PrePopData {

private val toAccountingMethod: String => AccountingMethod = {
case "C" => Cash
case "A" => Accruals
case method => throw new InternalServerException(s"[PrePopData] - Could not parse accounting method from api. Received: $method")
}

implicit val reads: Reads[PrePopData] = (
(__ \ "selfEmployment").readNullable[Seq[PrePopSelfEmployment]] and
(__ \ "ukProperty" \ "accountingMethod").readNullable[String].map(_.map(toAccountingMethod)) and
(__ \ "foreignProperty" \ 0 \ "accountingMethod").readNullable[String].map(_.map(toAccountingMethod))
)(PrePopData.apply _)

implicit val writes: OWrites[PrePopData] = Json.writes[PrePopData]

}

case class PrePopSelfEmployment(name: String,
trade: Option[String],
address: Option[Address],
startDate: Option[DateModel],
accountingMethod: AccountingMethod)

object PrePopSelfEmployment {

private val dateRegex: Regex = "^([0-9]{4})-([0-9]{2})-([0-9]{2})$".r

private val tradeMaxLength: Int = 35
private val tradeMinLetters: Int = 2

private def fromApi(name: String,
trade: String,
addressFirstLine: Option[String],
addressPostcode: Option[String],
startDate: Option[String],
accountingMethod: String): PrePopSelfEmployment = {

// Any characters not defined in this list will be matched on and replaced by single spaces
val notAllowedCharactersRegex: String = """[^ A-Za-z0-9&'/\\.,-]"""
val adjustedName = name.replaceAll(notAllowedCharactersRegex, " ").trim
val adjustedTrade = trade.replaceAll(notAllowedCharactersRegex, " ").trim

PrePopSelfEmployment(
name = adjustedName,
trade = adjustedTrade match {
case value if value.length <= tradeMaxLength && value.count(_.isLetter) >= tradeMinLetters => Some(value)
case _ => None
},
address = addressFirstLine match {
case Some(firstLine) => Some(Address(Seq(firstLine), addressPostcode))
case _ => None
},
startDate = startDate map {
case dateRegex(year, month, day) => DateModel(day = day, month = month, year = year)
},
accountingMethod = accountingMethod match {
case "A" => Accruals
case "C" => Cash
case method => throw new InternalServerException(s"[PrePopSelfEmployment] - Could not parse accounting method from api. Received: $method")
}
)
}

implicit val reads: Reads[PrePopSelfEmployment] = (
(__ \ "businessName").read[String] and
(__ \ "businessDescription").read[String] and
(__ \ "businessAddressFirstLine").readNullable[String] and
(__ \ "businessAddressPostcode").readNullable[String] and
(__ \ "dateBusinessStarted").readNullable[String] and
(__ \ "accountingMethod").read[String]
)(PrePopSelfEmployment.fromApi _)

implicit val writes: OWrites[PrePopSelfEmployment] = Json.writes[PrePopSelfEmployment]

}
46 changes: 46 additions & 0 deletions app/parsers/PrePopParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2023 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 parsers

import models.{ErrorModel, PrePopData}
import play.api.Logging
import play.api.http.Status.{NOT_FOUND, OK}
import play.api.libs.json.{JsError, JsSuccess}
import uk.gov.hmrc.http.{HttpReads, HttpResponse}

object PrePopParser extends Logging {

type GetPrePopResponse = Either[ErrorModel, PrePopData]

implicit object GetPrePopResponseHttpReads extends HttpReads[GetPrePopResponse] {
override def read(method: String, url: String, response: HttpResponse): GetPrePopResponse = {
response.status match {
case OK =>
response.json.validate[PrePopData] match {
case JsSuccess(value, _) => Right(value)
case JsError(_) => Left(ErrorModel(OK, s"Failure parsing json response from prepop api"))
}
case NOT_FOUND =>
Right(PrePopData(None, None, None))
case status =>
logger.error(s"[PrePopParser] - Unexpected status from pre-pop API. Status: $status")
Left(ErrorModel(status, "Unexpected status returned from pre-pop api"))
}
}
}

}
7 changes: 7 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ microservice {
authorization-token = "dev"
}

pre-pop {
host = localhost
port = 9562
environment = "dev"
authorization-token = "dev"
}

des {
url = "http://localhost:9562"
environment = "dev"
Expand Down
2 changes: 2 additions & 0 deletions conf/subscription.routes
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ POST /mis/sign-up/:nino/:taxYear controllers.SignUpContro

POST /mis/create/:mtdbsaRef controllers.BusinessIncomeSourcesController.createIncomeSource(mtdbsaRef: String)

GET /pre-pop/:nino controllers.PrePopController.prePop(nino: String)

POST /throttled controllers.throttle.ThrottlingController.throttled(throttleId: String)

POST /itsa-status controllers.MandationStatusController.mandationStatus
Loading

0 comments on commit 447a773

Please sign in to comment.