diff --git a/app/config/MicroserviceAppConfig.scala b/app/config/MicroserviceAppConfig.scala index 38e6a7a..1bb9fee 100644 --- a/app/config/MicroserviceAppConfig.scala +++ b/app/config/MicroserviceAppConfig.scala @@ -52,6 +52,10 @@ trait AppConfig { val signUpServiceAuthorisationToken: String val signUpServiceEnvironment: String + + val prePopURL: String + val prePopAuthorisationToken: String + val prePopEnvironment: String } @@ -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" @@ -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 - } diff --git a/app/connectors/PrePopConnector.scala b/app/connectors/PrePopConnector.scala new file mode 100644 index 0000000..c386870 --- /dev/null +++ b/app/connectors/PrePopConnector.scala @@ -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) + } +} diff --git a/app/controllers/PrePopController.scala b/app/controllers/PrePopController.scala new file mode 100644 index 0000000..4f581b3 --- /dev/null +++ b/app/controllers/PrePopController.scala @@ -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 + } + } + } + +} \ No newline at end of file diff --git a/app/models/PrePopData.scala b/app/models/PrePopData.scala new file mode 100644 index 0000000..a26ead0 --- /dev/null +++ b/app/models/PrePopData.scala @@ -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] + +} \ No newline at end of file diff --git a/app/parsers/PrePopParser.scala b/app/parsers/PrePopParser.scala new file mode 100644 index 0000000..2a1f099 --- /dev/null +++ b/app/parsers/PrePopParser.scala @@ -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")) + } + } + } + +} diff --git a/conf/application.conf b/conf/application.conf index 43076f9..db5a8ec 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -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" diff --git a/conf/subscription.routes b/conf/subscription.routes index 40a3f95..248ad1b 100644 --- a/conf/subscription.routes +++ b/conf/subscription.routes @@ -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 diff --git a/it/test/connectors/PrePopConnectorISpec.scala b/it/test/connectors/PrePopConnectorISpec.scala new file mode 100644 index 0000000..fb6ec03 --- /dev/null +++ b/it/test/connectors/PrePopConnectorISpec.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2020 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.MicroserviceAppConfig +import helpers.ComponentSpecBase +import helpers.IntegrationTestConstants._ +import helpers.servicemocks.PrePopStub +import models.subscription.business.Cash +import models.{ErrorModel, PrePopData, PrePopSelfEmployment} +import play.api.http.Status._ +import play.api.libs.json.Json +import play.api.mvc.Request +import play.api.test.FakeRequest + +class PrePopConnectorISpec extends ComponentSpecBase { + + private lazy val prePopConnector: PrePopConnector = app.injector.instanceOf[PrePopConnector] + private lazy val appConfig: MicroserviceAppConfig = app.injector.instanceOf[MicroserviceAppConfig] + implicit val request: Request[_] = FakeRequest() + + "the pre pop connector" when { + "receiving a OK (200) response" should { + "return pre pop data" in { + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = OK, + body = Json.obj( + "selfEmployment" -> Json.arr( + Json.obj( + "businessName" -> "ABC", + "businessDescription" -> "Plumbing", + "accountingMethod" -> "C" + ) + ) + ) + ) + + val result = prePopConnector.getPrePopData(testNino) + + result.futureValue shouldBe Right(PrePopData( + selfEmployment = Some(Seq( + PrePopSelfEmployment(name = "ABC", trade = Some("Plumbing"), address = None, startDate = None, accountingMethod = Cash) + )), + ukPropertyAccountingMethod = None, + foreignPropertyAccountingMethod = None + )) + } + "return a json parse failure when the received json could not be parsed" in { + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = OK, + body = Json.obj( + "selfEmployment" -> Json.arr( + Json.obj() + ) + ) + ) + + val result = prePopConnector.getPrePopData(testNino) + + result.futureValue shouldBe Left(ErrorModel(OK, "Failure parsing json response from prepop api")) + } + } + "receiving a NOT_FOUND (404) response" should { + "return an empty pre-pop data" in { + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = NOT_FOUND, + body = Json.obj() + ) + + val result = prePopConnector.getPrePopData(testNino) + + result.futureValue shouldBe Right(PrePopData( + selfEmployment = None, ukPropertyAccountingMethod = None, foreignPropertyAccountingMethod = None + )) + } + } + "receiving a non handled status response" should { + "return an unexpected status error" in { + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = INTERNAL_SERVER_ERROR, + body = Json.obj() + ) + + val result = prePopConnector.getPrePopData(testNino) + + result.futureValue shouldBe Left(ErrorModel(INTERNAL_SERVER_ERROR, "Unexpected status returned from pre-pop api")) + } + } + } +} + diff --git a/it/test/controllers/PrePopControllerISpec.scala b/it/test/controllers/PrePopControllerISpec.scala new file mode 100644 index 0000000..63aaf7c --- /dev/null +++ b/it/test/controllers/PrePopControllerISpec.scala @@ -0,0 +1,105 @@ +/* + * 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 controllers + +import config.MicroserviceAppConfig +import config.featureswitch.FeatureSwitching +import helpers.ComponentSpecBase +import helpers.IntegrationTestConstants._ +import helpers.servicemocks.{AuthStub, PrePopStub, SignUpTaxYearStub} +import models.PrePopData +import models.SignUpResponse.SignUpSuccess +import play.api.http.Status._ +import play.api.libs.json.{JsObject, Json} + +class PrePopControllerISpec extends ComponentSpecBase with FeatureSwitching { + + val appConfig: MicroserviceAppConfig = app.injector.instanceOf[MicroserviceAppConfig] + + val readJson: JsObject = Json.obj( + "selfEmployments" -> Json.arr( + Json.obj( + "businessName" -> "ABC", + "businessDescription" -> "Plumbing", + "accountingMethod" -> "A" + ) + ) + ) + + val writeJson: JsObject = Json.toJsObject(Json.fromJson[PrePopData](readJson).get) + + "PrePopController" should { + "return a OK response with pre-pop data" in { + AuthStub.stubAuth(OK) + + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = OK, + body = readJson + ) + + val res = IncomeTaxSubscription.getPrePop(testNino) + + res should have( + httpStatus(OK), + jsonBodyOf(writeJson) + ) + } + "return an INTERNAL_SERVER_ERROR" when { + "there was a problem with the pre-pop json from the API" in { + AuthStub.stubAuth(OK) + + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = OK, + body = Json.obj( + "selfEmployment" -> Json.arr( + Json.obj() + ) + ) + ) + + val res = IncomeTaxSubscription.getPrePop(testNino) + + res should have( + httpStatus(INTERNAL_SERVER_ERROR) + ) + } + "there was an error returned from the pre-pop API" in { + AuthStub.stubAuth(OK) + + PrePopStub.stubPrePop(testNino)( + appConfig.prePopAuthorisationToken, + appConfig.prePopEnvironment + )( + status = INTERNAL_SERVER_ERROR, + body = Json.obj() + ) + + val res = IncomeTaxSubscription.getPrePop(testNino) + + res should have( + httpStatus(INTERNAL_SERVER_ERROR) + ) + } + } + } +} \ No newline at end of file diff --git a/it/test/helpers/ComponentSpecBase.scala b/it/test/helpers/ComponentSpecBase.scala index 61dee5d..77c2dbb 100644 --- a/it/test/helpers/ComponentSpecBase.scala +++ b/it/test/helpers/ComponentSpecBase.scala @@ -60,15 +60,17 @@ trait ComponentSpecBase extends AnyWordSpecLike "microservice.services.gg-authentication.host" -> mockHost, "microservice.services.gg-authentication.port" -> mockPort, "microservice.services.throttle-threshold" -> "2", - "throttle.testThrottle.max"-> "10", - "throttle.zeroTestThrottle.max"-> "0", + "throttle.testThrottle.max" -> "10", + "throttle.zeroTestThrottle.max" -> "0", "throttle.oneTestThrottle.max" -> "1", "microservice.services.status-determination-service.host" -> mockHost, "microservice.services.status-determination-service.port" -> mockPort, "microservice.services.signup-tax-year-service.host" -> mockHost, "microservice.services.signup-tax-year-service.port" -> mockPort, "microservice.services.get-business-details.host" -> mockHost, - "microservice.services.get-business-details.port" -> mockPort + "microservice.services.get-business-details.port" -> mockPort, + "microservice.services.pre-pop.host" -> mockHost, + "microservice.services.pre-pop.port" -> mockPort ) ++ overriddenConfig() def overriddenConfig(): Map[String, String] = Map.empty @@ -153,18 +155,21 @@ trait ComponentSpecBase extends AnyWordSpecLike def getAllSessionData: WSResponse = authorisedClient(s"/session-data/all").get().futureValue - def retrieveSessionData(id:String): WSResponse = + def retrieveSessionData(id: String): WSResponse = authorisedClient(s"/session-data/id/$id").get().futureValue - def deleteSessionData(id:String): WSResponse = + def deleteSessionData(id: String): WSResponse = authorisedClient(s"/session-data/id/$id").delete().futureValue def deleteAllSessionData(): WSResponse = authorisedClient(s"/session-data/id").delete().futureValue - def insertSessionData(id:String, body: JsObject): WSResponse = + def insertSessionData(id: String, body: JsObject): WSResponse = authorisedClient(s"/session-data/id/$id", "Content-Type" -> "application/json").post(body.toString()).futureValue + def getPrePop(nino: String): WSResponse = + authorisedClient(s"/pre-pop/$nino").get().futureValue + def post[T](uri: String, body: T)(implicit writes: Writes[T]): WSResponse = buildClient(uri) .withHttpHeaders( @@ -182,6 +187,6 @@ trait ComponentSpecBase extends AnyWordSpecLike val authorisation = "Authorization" -> "Bearer 123" val headers = extraHeaders.toList :+ sessionId :+ authorisation buildClient(path) - .withHttpHeaders(headers:_*) + .withHttpHeaders(headers: _*) } } diff --git a/it/test/helpers/servicemocks/PrePopStub.scala b/it/test/helpers/servicemocks/PrePopStub.scala new file mode 100644 index 0000000..0e7580b --- /dev/null +++ b/it/test/helpers/servicemocks/PrePopStub.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2020 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 helpers.servicemocks + +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import play.api.libs.json.JsValue + +object PrePopStub extends WireMockMethods { + + private def prePopUri(nino: String): String = s"/income-tax/pre-pop/$nino" + + def stubPrePop(nino: String) + (authorizationHeader: String, environmentHeader: String) + (status: Int, body: JsValue): StubMapping = { + when( + method = GET, + uri = prePopUri(nino), + headers = Map[String, String]( + "Authorization" -> authorizationHeader, + "Environment" -> environmentHeader + ) + ).thenReturn(status, body) + + } +} diff --git a/test/models/PrePopDataSpec.scala b/test/models/PrePopDataSpec.scala new file mode 100644 index 0000000..35d135c --- /dev/null +++ b/test/models/PrePopDataSpec.scala @@ -0,0 +1,223 @@ +/* + * 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.{Accruals, Cash} +import org.scalatest.matchers.must.Matchers +import org.scalatestplus.play.PlaySpec +import play.api.libs.json._ + +class PrePopDataSpec extends PlaySpec with Matchers { + + "PrePopSelfEmployment" should { + "successfully read from json" when { + "all data is present" in { + Json.fromJson[PrePopSelfEmployment](selfEmploymentJsonFull) mustBe JsSuccess(selfEmploymentModelFull) + } + "all optional data is missing" in { + Json.fromJson[PrePopSelfEmployment](selfEmploymentJsonMinimal) mustBe JsSuccess(selfEmploymentModelMinimal) + } + "trade is longer than 35 characters" in { + Json.fromJson[PrePopSelfEmployment](Json.obj( + "businessName" -> "AB 123", + "businessDescription" -> ("A" * 36), + "accountingMethod" -> "A" + )) mustBe JsSuccess(PrePopSelfEmployment( + name = "AB 123", + trade = None, + address = None, + startDate = None, + accountingMethod = Accruals + )) + } + "trade does not contain a minimum of 2 letters" in { + Json.fromJson[PrePopSelfEmployment](Json.obj( + "businessName" -> "AB 123", + "businessDescription" -> "P 383", + "accountingMethod" -> "A" + )) mustBe JsSuccess(PrePopSelfEmployment( + name = "AB 123", + trade = None, + address = None, + startDate = None, + accountingMethod = Accruals + )) + } + "trade contains characters which are not allowed" in { + Json.fromJson[PrePopSelfEmployment](Json.obj( + "businessName" -> "AB 123", + "businessDescription" -> """!@£$%^*()_+}{":?><~`#§± AZaz09&'/\.,-""", + "accountingMethod" -> "A" + )) mustBe JsSuccess(PrePopSelfEmployment( + name = "AB 123", + trade = Some("""AZaz09&'/\.,-"""), + address = None, + startDate = None, + accountingMethod = Accruals + )) + } + "name contains characters which are not allowed" in { + Json.fromJson[PrePopSelfEmployment](Json.obj( + "businessName" -> """!@£$%^*()_+}{":?><~`#§± AZaz09&'/\.,-""", + "businessDescription" -> "Plumbing", + "accountingMethod" -> "A" + )) mustBe JsSuccess(PrePopSelfEmployment( + name = """AZaz09&'/\.,-""", + trade = Some("Plumbing"), + address = None, + startDate = None, + accountingMethod = Accruals + )) + } + } + "fail to read from json" when { + "business name is missing" in { + Json.fromJson[PrePopSelfEmployment](selfEmploymentJsonFull - "businessName") mustBe JsError(__ \ "businessName", "error.path.missing") + } + "business description is missing" in { + Json.fromJson[PrePopSelfEmployment](selfEmploymentJsonFull - "businessDescription") mustBe JsError(__ \ "businessDescription", "error.path.missing") + } + "accounting method is missing" in { + Json.fromJson[PrePopSelfEmployment](selfEmploymentJsonFull - "accountingMethod") mustBe JsError(__ \ "accountingMethod", "error.path.missing") + } + } + + "successfully write to json" when { + "all optional data items are present" in { + Json.toJson(selfEmploymentModelFull) mustBe selfEmploymentJsonWriteFull + } + "all optional data items are not present" in { + Json.toJson(selfEmploymentModelMinimal) mustBe selfEmploymentJsonWriteMinimal + } + } + } + + lazy val selfEmploymentJsonFull: JsObject = Json.obj( + "businessName" -> "AB 123", + "businessDescription" -> "EL 987", + "businessAddressFirstLine" -> "1 long road", + "businessAddressPostcode" -> "ZZ1 1ZZ", + "dateBusinessStarted" -> "1900-01-01", + "accountingMethod" -> "A" + ) + lazy val selfEmploymentJsonWriteFull: JsObject = Json.obj( + "name" -> "AB 123", + "trade" -> "EL 987", + "address" -> Json.obj( + "lines" -> Json.arr( + "1 long road" + ), + "postcode" -> "ZZ1 1ZZ" + ), + "startDate" -> Json.obj( + "day" -> "01", + "month" -> "01", + "year" -> "1900" + ), + "accountingMethod" -> Accruals.stringValue + ) + lazy val selfEmploymentModelFull: PrePopSelfEmployment = PrePopSelfEmployment( + name = "AB 123", + trade = Some("EL 987"), + address = Some(Address( + lines = Seq("1 long road"), + postcode = Some("ZZ1 1ZZ") + )), + startDate = Some(DateModel("01", "01", "1900")), + accountingMethod = Accruals + ) + lazy val selfEmploymentJsonMinimal: JsObject = Json.obj( + "businessName" -> "AB 123", + "businessDescription" -> "PL 567", + "accountingMethod" -> "C" + ) + lazy val selfEmploymentJsonWriteMinimal: JsObject = Json.obj( + "name" -> "AB 123", + "trade" -> "PL 567", + "accountingMethod" -> Cash.stringValue + ) + lazy val selfEmploymentModelMinimal: PrePopSelfEmployment = PrePopSelfEmployment( + name = "AB 123", + trade = Some("PL 567"), + address = None, + startDate = None, + accountingMethod = Cash + ) + + "PrePopData" should { + "successfully read from json" when { + "all data is present" in { + Json.fromJson[PrePopData](prePopDataJsonFull) mustBe JsSuccess(prePopDataModelFull) + } + "all optional data is missing" in { + Json.fromJson[PrePopData](prePopDataJsonMinimal) mustBe JsSuccess(prePopDataModelMinimal) + } + "uk property and foreign property are present but their inner accounting method is missing" in { + Json.fromJson[PrePopData](Json.obj( + "ukProperty" -> Json.obj(), + "foreignProperty" -> Json.arr() + )) mustBe JsSuccess(prePopDataModelMinimal) + } + } + "successfully write to json" when { + "all optional data values are present" in { + Json.toJson(prePopDataModelFull) mustBe Json.obj( + "selfEmployment" -> Json.arr( + selfEmploymentJsonWriteFull, + selfEmploymentJsonWriteMinimal + ), + "ukPropertyAccountingMethod" -> Accruals.stringValue, + "foreignPropertyAccountingMethod" -> Cash.stringValue + ) + } + "all optional data values are missing" in { + Json.toJson(prePopDataModelMinimal) mustBe Json.obj() + } + } + } + + lazy val prePopDataJsonFull: JsObject = Json.obj( + "selfEmployment" -> Json.arr( + selfEmploymentJsonFull, + selfEmploymentJsonMinimal + ), + "ukProperty" -> Json.obj( + "accountingMethod" -> "A" + ), + "foreignProperty" -> Json.arr( + Json.obj( + "accountingMethod" -> "C" + ), + Json.obj( + "accountingMethod" -> "A" + ) + ) + ) + lazy val prePopDataModelFull: PrePopData = PrePopData( + selfEmployment = Some(Seq( + selfEmploymentModelFull, + selfEmploymentModelMinimal + )), + ukPropertyAccountingMethod = Some(Accruals), + foreignPropertyAccountingMethod = Some(Cash) + ) + + lazy val prePopDataJsonMinimal: JsObject = Json.obj() + lazy val prePopDataModelMinimal: PrePopData = PrePopData(None, None, None) + +} diff --git a/test/parsers/PrePopParserSpec.scala b/test/parsers/PrePopParserSpec.scala new file mode 100644 index 0000000..b5e9409 --- /dev/null +++ b/test/parsers/PrePopParserSpec.scala @@ -0,0 +1,78 @@ +/* + * 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 common.CommonSpec +import models.subscription.Address +import models.subscription.business.Cash +import models.{DateModel, ErrorModel, PrePopData, PrePopSelfEmployment} +import parsers.PrePopParser.GetPrePopResponse +import play.api.http.Status.{INTERNAL_SERVER_ERROR, NOT_FOUND, OK} +import play.api.libs.json.{JsObject, Json} +import uk.gov.hmrc.http.HttpResponse + +class PrePopParserSpec extends CommonSpec { + + "PrePopParser" when { + "an OK (200) status is returned" should { + "parse pre-pop data successfully" when { + "valid json is returned" in { + read(OK, Json.obj("selfEmployment" -> Json.arr(selfEmploymentJson))) shouldBe Right(PrePopData( + Some(Seq(PrePopSelfEmployment( + "ABC", + Some("Plumbing"), + Some(Address(Seq("123 Street"), Some("ZZ1 1ZZ"))), + Some(DateModel("01", "01", "1900")), + Cash + ))), + None, + None + )) + } + } + "fail to parse the pre-pop data" when { + "invalid json is returned" in { + read(OK, Json.obj("selfEmployment" -> Json.arr(Json.obj()))) shouldBe Left(ErrorModel(OK, "Failure parsing json response from prepop api")) + } + } + } + "a NOT_FOUND (404) status is returned" should { + "return an empty prepop data set" in { + read(NOT_FOUND) shouldBe Right(PrePopData(None, None, None)) + } + } + "a different status is returned" should { + "return an unexpected status error model" in { + read(INTERNAL_SERVER_ERROR) shouldBe Left(ErrorModel(INTERNAL_SERVER_ERROR, "Unexpected status returned from pre-pop api")) + } + } + } + + def read(status: Int, json: JsObject = Json.obj()): GetPrePopResponse = { + PrePopParser.GetPrePopResponseHttpReads.read("", "", HttpResponse(status, json.toString())) + } + + lazy val selfEmploymentJson: JsObject = Json.obj( + "businessName" -> "ABC", + "businessDescription" -> "Plumbing", + "businessAddressFirstLine" -> "123 Street", + "businessAddressPostcode" -> "ZZ1 1ZZ", + "dateBusinessStarted" -> "1900-01-01", + "accountingMethod" -> "C" + ) + +}