diff --git a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingController.scala b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingController.scala index 92af25b..f058fc1 100644 --- a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingController.scala +++ b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingController.scala @@ -48,6 +48,39 @@ class GamblingController @Inject() (authorise: AuthAction, service: GamblingServ } } } + def getBusinessName(mgdRegNumber: String): Action[AnyContent] = authorise.async { implicit request => + + service.getBusinessName(mgdRegNumber).map { + case Right(summary) => Ok(Json.toJson(summary)) + case Left(error) => + val logMessage = s"[GamblingController][getBusinessName] code=${error.code} mgdRegNumber=$mgdRegNumber" + error match { + case InvalidMgdRegNumber => + logger.warn(logMessage) + BadRequest(errorResponse(error)) + case UnexpectedError => + logger.error(logMessage) + InternalServerError(errorResponse(error)) + } + } + } + + def getBusinessDetails(mgdRegNumber: String): Action[AnyContent] = authorise.async { implicit request => + + service.getBusinessDetails(mgdRegNumber).map { + case Right(summary) => Ok(Json.toJson(summary)) + case Left(error) => + val logMessage = s"[GamblingController][getBusinessDetails] code=${error.code} mgdRegNumber=$mgdRegNumber" + error match { + case InvalidMgdRegNumber => + logger.warn(logMessage) + BadRequest(errorResponse(error)) + case UnexpectedError => + logger.error(logMessage) + InternalServerError(errorResponse(error)) + } + } + } def getMgdCertificate(mgdRegNumber: String): Action[AnyContent] = authorise.async { implicit request => @@ -71,29 +104,6 @@ class GamblingController @Inject() (authorise: AuthAction, service: GamblingServ } } - def getBusinessDetails(mgdRegNumber: String): Action[AnyContent] = - authorise.async { implicit request => - - service.getBusinessDetails(mgdRegNumber).map { - case Right(details) => - Ok(Json.toJson(details)) - - case Left(error) => - val logMessage = - s"[GamblingController][getBusinessDetails] code=${error.code} mgdRegNumber=$mgdRegNumber" - - error match { - case InvalidMgdRegNumber => - logger.warn(logMessage) - BadRequest(errorResponse(error)) - - case UnexpectedError => - logger.error(logMessage) - InternalServerError(errorResponse(error)) - } - } - } - def getOperatorDetails(mgdRegNumber: String): Action[AnyContent] = authorise.async { implicit request => diff --git a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessDetails.scala b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessDetails.scala index 37064a2..be8d38b 100644 --- a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessDetails.scala +++ b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessDetails.scala @@ -17,6 +17,7 @@ package uk.gov.hmrc.rdsdatacacheproxy.gambling.models import play.api.libs.json.{Json, OFormat} + import java.time.LocalDate final case class BusinessDetails( diff --git a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessName.scala b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessName.scala new file mode 100644 index 0000000..820de90 --- /dev/null +++ b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/BusinessName.scala @@ -0,0 +1,35 @@ +/* + * 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 uk.gov.hmrc.rdsdatacacheproxy.gambling.models +import play.api.libs.json.{Json, OFormat} +import java.time.LocalDate + +final case class BusinessName( + mgdRegNumber: String, + solePropTitle: Option[String], + solePropFirstName: Option[String], + solePropMidName: Option[String], + solePropLastName: Option[String], + businessName: Option[String], + businessType: Option[BusinessType], + tradingName: Option[String], + systemDate: Option[LocalDate] +) + +object BusinessName { + implicit val format: OFormat[BusinessName] = Json.format[BusinessName] +} diff --git a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepository.scala b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepository.scala index b2a99b2..5f3e89d 100644 --- a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepository.scala +++ b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepository.scala @@ -25,9 +25,10 @@ import scala.concurrent.{ExecutionContext, Future, blocking} trait GamblingDataSource { def getReturnSummary(mgdRegNumber: String): Future[ReturnSummary] + def getBusinessName(mgdRegNumber: String): Future[BusinessName] + def getBusinessDetails(mgdRegNumber: String): Future[BusinessDetails] def getMgdCertificate(mgdRegNumber: String): Future[MgdCertificate] def getOperatorDetails(mgdRegNumber: String): Future[OperatorDetails] - def getBusinessDetails(mgdRegNumber: String): Future[BusinessDetails] } @Singleton @@ -349,6 +350,66 @@ class GamblingDataCacheRepository @Inject() ( } })(ec) } + override def getBusinessName(mgdRegNumber: String): Future[BusinessName] = { + + logger.info(s"[GamblingDataCacheRepository][getBusinessName] mgdRegNumber=$mgdRegNumber") + + Future { + db.withConnection { conn => + + val cs = conn.prepareCall("{ call MGD_DC_VARIATION_PK.GET_BUSINESS_NAME(?, ?) }") + + try { + cs.setString(1, mgdRegNumber) + cs.registerOutParameter(2, oracle.jdbc.OracleTypes.CURSOR) + cs.execute() + + val rs = cs.getObject(2).asInstanceOf[java.sql.ResultSet] + + if (rs == null) { + val msg = s"Null cursor returned for mgdRegNumber=$mgdRegNumber" + logger.error(s"[GamblingDataCacheRepository] $msg") + throw new RuntimeException(msg) + } + + try { + if (rs.next()) { + + def optInt(col: String): Option[Int] = + Option(rs.getObject(col)).map { + case bd: java.math.BigDecimal => bd.intValue() + case n: java.lang.Number => n.intValue() + case other => other.toString.toInt + } + + val businessType: Option[BusinessType] = + optInt("business_type").flatMap(BusinessType.fromCode) + + BusinessName( + mgdRegNumber = rs.getString("MGD_REG_NUMBER"), + solePropTitle = Option(rs.getString("SOLE_PROP_TITLE")), + solePropFirstName = Option(rs.getString("SOLE_PROP_FIRST_NAME")), + solePropMidName = Option(rs.getString("SOLE_PROP_MIDDLE_NAME")), + solePropLastName = Option(rs.getString("SOLE_PROP_LAST_NAME")), + businessName = Option(rs.getString("BUSINESS_NAME")), + businessType = businessType, + tradingName = Option(rs.getString("TRADING_NAME")), + systemDate = Option(rs.getDate("SYSTEM_DATE")).map(_.toLocalDate) + ) + } else { + val msg = s"Empty result set for mgdRegNumber=$mgdRegNumber" + logger.error(s"[GamblingDataCacheRepository] $msg") + throw new RuntimeException(msg) + } + } finally { + rs.close() + } + } finally { + cs.close() + } + } + }(ec) + } override def getReturnSummary(mgdRegNumber: String): Future[ReturnSummary] = { diff --git a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingService.scala b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingService.scala index e153786..d5a8d8e 100644 --- a/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingService.scala +++ b/app/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingService.scala @@ -50,6 +50,25 @@ class GamblingService @Inject() ( } } + def getBusinessName(rawMgdRegNumber: String)(implicit hc: HeaderCarrier): Future[Either[GamblingError, BusinessName]] = { + + val mgdRegNumber = rawMgdRegNumber.trim.toUpperCase + + if (!regNumberPattern.matcher(mgdRegNumber).matches()) { + logger.warn(s"[GamblingService][getBusinessName] Invalid pattern for mgdRegNumber=$mgdRegNumber") + Future.successful(Left(InvalidMgdRegNumber)) + } else { + + repository + .getBusinessName(mgdRegNumber) + .map(summary => Right(summary)) + .recover { case ex: Exception => + logger.error(s"[GamblingService][getBusinessName] Unexpected error mgdRegNumber=$mgdRegNumber", ex) + Left(UnexpectedError) + } + } + } + def getMgdCertificate(rawMgdRegNumber: String)(implicit hc: HeaderCarrier): Future[Either[GamblingError, MgdCertificate]] = { val mgdRegNumber = rawMgdRegNumber.trim.toUpperCase diff --git a/conf/app.routes b/conf/app.routes index 97a5fe1..ecb5eca 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -39,6 +39,7 @@ GET /gambling/return-summary/:mgdRegNumber uk.gov.hmrc.rdsdataca GET /gambling/mgd-certificate/:mgdRegNumber uk.gov.hmrc.rdsdatacacheproxy.gambling.controllers.GamblingController.getMgdCertificate(mgdRegNumber: String) GET /gambling/operator-details/:mgdRegNumber uk.gov.hmrc.rdsdatacacheproxy.gambling.controllers.GamblingController.getOperatorDetails(mgdRegNumber: String) GET /gambling/business-details/:mgdRegNumber uk.gov.hmrc.rdsdatacacheproxy.gambling.controllers.GamblingController.getBusinessDetails(mgdRegNumber: String) +GET /gambling/business-name/:mgdRegNumber uk.gov.hmrc.rdsdatacacheproxy.gambling.controllers.GamblingController.getBusinessName(mgdRegNumber: String) #Gambling Returns routes GET /gambling/returns-submitted/:regime/:regNumber uk.gov.hmrc.rdsdatacacheproxy.gambling.controllers.GamblingReturnsController.getReturnsSubmitted(regime: String, regNumber: String, pageNo: Int ?=1, pageSize: Int ?=10) diff --git a/conf/application.conf b/conf/application.conf index bc947ed..c633984 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -120,8 +120,8 @@ oracle.gambling { db.host = "localhost" db.port = "1521" dbName = "xe" - username = "GTR_DATA" - password = "GTR_DATA" + username = "mgd_data" + password = "mgd_data" connectionTimeout = 50s validationTimeout = 3s idleTimeout = 10m diff --git a/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerISpec.scala b/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerISpec.scala index a135c8d..4528e87 100644 --- a/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerISpec.scala +++ b/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerISpec.scala @@ -23,11 +23,15 @@ import play.api.Application import play.api.http.Status.* import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder -import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.{BusinessDetails, GamblingStubData, MgdCertificate, OperatorDetails} +import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.{BusinessDetails, GamblingStubData, OperatorDetails, MgdCertificate} +import play.api.libs.json.Reads + +import scala.concurrent.ExecutionContext.Implicits.global import uk.gov.hmrc.rdsdatacacheproxy.gambling.repositories.GamblingDataSource import uk.gov.hmrc.rdsdatacacheproxy.itutil.{ApplicationWithWiremock, AuthStub} import scala.concurrent.ExecutionContext.Implicits.global +import java.time.LocalDate import scala.concurrent.Future class GamblingControllerISpec extends AnyWordSpec with Matchers with ScalaFutures with IntegrationPatience with ApplicationWithWiremock { @@ -77,6 +81,11 @@ class GamblingControllerISpec extends AnyWordSpec with Matchers with ScalaFuture GamblingStubData.getReturnSummary(mgdRegNumber) } + override def getBusinessName(mgdRegNumber: String) = + Future { + GamblingStubData.getBusinessName(mgdRegNumber) + } + override def getMgdCertificate(mgdRegNumber: String): Future[MgdCertificate] = Future.successful( MgdCertificate( @@ -122,6 +131,13 @@ class GamblingControllerISpec extends AnyWordSpec with Matchers with ScalaFuture private val endpoint = "/gambling/return-summary" + implicit val localDateReads: Reads[LocalDate] = + Reads.localDateReads("yyyy-MM-dd") + + implicit val optLocalDateReads: Reads[Option[LocalDate]] = + Reads.optionWithNull[LocalDate] + + "GET /gambling/return-summary (stubbed repo, no DB)" should { "return 200 with correct summary (0,0)" in { diff --git a/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepositoryISpec.scala b/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepositoryISpec.scala index 015f105..88d6eae 100644 --- a/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepositoryISpec.scala +++ b/it/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/repositories/GamblingDataCacheRepositoryISpec.scala @@ -24,7 +24,9 @@ import play.api.Application import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.* +import uk.gov.hmrc.rdsdatacacheproxy.gambling.repositories.GamblingDataSource +import java.time.LocalDate import scala.concurrent.Future class GamblingDataCacheRepositoryISpec extends AnyWordSpec with Matchers with ScalaFutures with IntegrationPatience with GuiceOneAppPerSuite { @@ -40,6 +42,9 @@ class GamblingDataCacheRepositoryISpec extends AnyWordSpec with Matchers with Sc override def getOperatorDetails(mgdRegNumber: String): Future[OperatorDetails] = Future.successful(GamblingStubData.getOperatorDetails(mgdRegNumber)) + override def getBusinessName(mgdRegNumber: String): Future[BusinessName] = + Future.successful(GamblingStubData.getBusinessName(mgdRegNumber)) + override def getBusinessDetails(mgdRegNumber: String): Future[BusinessDetails] = Future.successful(GamblingStubData.getBusinessDetails(mgdRegNumber)) } @@ -114,7 +119,7 @@ class GamblingDataCacheRepositoryISpec extends AnyWordSpec with Matchers with Sc val result = repository.getBusinessDetails("XYZ00000000001").futureValue result.businessType mustBe None - result.businessPartnerNumber mustBe Some("BPN123456") + result.businessPartnerNumber mustBe Some("bar") } "return consistent results across multiple calls" in { @@ -241,4 +246,205 @@ class GamblingDataCacheRepositoryISpec extends AnyWordSpec with Matchers with Sc result.returnsOverdue must be >= 0 } } + "getBusinessName (stubbed repository)" should { + + "return John Doe as Sole Proprietor" in { + val result = repository.getBusinessName("XYZ00000000000").futureValue + + result mustBe BusinessName( + mgdRegNumber = "XYZ00000000000", + solePropTitle = Some("Mr"), + solePropFirstName = Some("John"), + solePropMidName = Some("C"), + solePropLastName = Some("Doe"), + businessName = Some("John Doe Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("DoeDoe"), + systemDate = Some(LocalDate.of(2026, 4, 20)) + ) + } + + "return Marge Simpson as Sole Proprietor" in { + val result = repository.getBusinessName("XYZ00000000010").futureValue + + result mustBe BusinessName( + mgdRegNumber = "XYZ00000000010", + solePropTitle = Some("Mrs"), + solePropFirstName = Some("Marge"), + solePropMidName = Some("Jacqueline"), + solePropLastName = Some("Simpson"), + businessName = Some("Pretzel Wagon"), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Marge Simpson"), + systemDate = Some(LocalDate.of(2026, 4, 20)) + ) + } + + "return last name and business name correctly" in { + val result = repository.getBusinessName("XYZ00000000001").futureValue + + result.solePropLastName mustBe Some("Doe") + result.businessName mustBe Some("Jane Doe Co.") + } + + "return correct middle name and system date" in { + val result = repository.getBusinessName("XYZ00000000010").futureValue + + result.solePropMidName mustBe Some("Jacqueline") + result.systemDate mustBe Some(LocalDate.of(2026, 4, 20)) + } + + "return correct title and trading name" in { + val result = repository.getBusinessName("XYZ00000000012").futureValue + + result.solePropTitle mustBe Some("Miss") + result.tradingName mustBe Some("Miss Havisham") + } + + "return correct business type and first name" in { + val result = repository.getBusinessName("XYZ00000000021").futureValue + + result.solePropFirstName mustBe Some("Eugine") + result.businessType mustBe Some(BusinessType.SoleProprietor) + } + + "return default values for unknown mgdRegNumber" in { + val result = repository.getBusinessName("XYZ99999999999").futureValue + + result mustBe BusinessName( + mgdRegNumber = "XYZ99999999999", + solePropTitle = Some("Mr"), + solePropFirstName = Some("Foo"), + solePropMidName = Some("B"), + solePropLastName = Some("Bar"), + businessName = Some("FooBar Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Foobar"), + systemDate = Some(LocalDate.of(2026, 4, 20)) + ) + } + + "return consistent results across multiple calls" in { + val result1 = repository.getBusinessName("XYZ00000000012").futureValue + val result2 = repository.getBusinessName("XYZ00000000012").futureValue + + result1 mustBe result2 + } + + "handle different valid mgdRegNumbers independently" in { + val result1 = repository.getBusinessName("XYZ00000000010").futureValue + val result2 = repository.getBusinessName("XYZ00000000001").futureValue + + result1 must not be result2 + } + + "propagate downstream failure from stub" in { + val exception = intercept[RuntimeException] { + repository.getBusinessName("ERR00000000000").futureValue + } + + exception.getMessage must include("Simulated downstream failure") + } + + "handle special characters in mgdRegNumber" in { + val result = repository.getBusinessName("XYZ-123/ABC").futureValue + + result.mgdRegNumber mustBe "XYZ-123/ABC" + } + + "handle whitespace mgdRegNumber" in { + val result = repository.getBusinessName(" ").futureValue + + result.mgdRegNumber mustBe (" ") + } + + "return populated fields for all required responses" in { + val result = repository.getBusinessName("XYZ00000000012").futureValue + + result.mgdRegNumber must not be empty + result.solePropTitle must not be empty + result.solePropFirstName must not be empty + result.solePropLastName must not be empty + result.businessName must not be empty + result.tradingName must not be empty + } + } + + "getBusinessDetails (stubbed repository)" should { + + "return values correctly" in { + val result = repository.getBusinessDetails("XYZ00000000000").futureValue + + result mustBe BusinessDetails( + mgdRegNumber = "XYZ00000000000", + businessType = Some(BusinessType.CorporateBody), + currentlyRegistered = 2, + groupReg = false, + dateOfRegistration = Some(LocalDate.of(2024, 4, 21)), businessPartnerNumber = Some("bar"), systemDate = LocalDate.of(2024, 4, 21) + ) + } + + "return business type correctly" in { + val result = repository.getBusinessDetails("XYZ00000000001").futureValue + + result.businessType mustBe None + result.currentlyRegistered mustBe 1 + result.groupReg mustBe false + result.dateOfRegistration mustBe Some(LocalDate.of(2024, 4, 21)) + result.businessPartnerNumber mustBe Some("bar") + result.systemDate mustBe LocalDate.of(2024, 4, 21) + } + + "return group reg correctly" in { + val result = repository.getBusinessDetails("XYZ00000000010").futureValue + + result.businessType mustBe Some(BusinessType.UnincorporatedBody) + result.currentlyRegistered mustBe 2 + result.groupReg mustBe true + result.dateOfRegistration mustBe Some(LocalDate.of(2024, 4, 21)) + result.businessPartnerNumber mustBe Some("bar") + result.systemDate mustBe LocalDate.of(2024, 4, 21) + } + + "return both date values correctly" in { + val result = repository.getBusinessDetails("XYZ00000000012").futureValue + + result mustBe BusinessDetails("XYZ00000000012", Some(BusinessType.SoleProprietor), 2, true, Some(LocalDate.of(2023, 4, 21)), Some("barfoo"), LocalDate.of(2023, 4, 21)) + } + + "handle multiple values" in { + val result = repository.getBusinessDetails("XYZ00000000021").futureValue + + result.businessType mustBe Some(BusinessType.Partnership) + result.currentlyRegistered mustBe 2 + result.groupReg mustBe false + result.dateOfRegistration mustBe Some(LocalDate.of(2024, 1, 21)) + result.businessPartnerNumber mustBe Some("barbar") + result.systemDate mustBe LocalDate.of(2024, 1, 21) + } + + + + "handle different valid mgdRegNumbers independently" in { + val result1 = repository.getBusinessDetails("XYZ00000000010").futureValue + val result2 = repository.getBusinessDetails("XYZ00000000001").futureValue + + result1 must not be result2 + } + + "propagate downstream failure from stub" in { + val exception = intercept[RuntimeException] { + repository.getBusinessDetails("ERR00000000000").futureValue + } + + exception.getMessage must include("Simulated downstream failure") + } + + + "handle whitespace mgdRegNumber" in { + val result = repository.getBusinessDetails(" ").futureValue + + result mustBe BusinessDetails(" ", Some(BusinessType.Partnership), 0, false, Some(LocalDate.of(2026, 4, 22)), Some("unknown"), LocalDate.of(2026, 4, 22)) + } + } } diff --git a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerSpec.scala b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerSpec.scala index 7f93b8e..06ca943 100644 --- a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerSpec.scala +++ b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/controllers/GamblingControllerSpec.scala @@ -38,14 +38,14 @@ import org.mockito.Mockito.* import org.mockito.ArgumentMatchers.{any, eq as eqTo} import org.scalatest.matchers.should.Matchers.{should, shouldBe} import play.api.libs.json.{JsValue, Json} - import play.api.test.FakeRequest import play.api.test.Helpers.* import uk.gov.hmrc.rdsdatacacheproxy.base.SpecBase import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.GamblingError.{InvalidMgdRegNumber, UnexpectedError} -import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.ReturnSummary +import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.{BusinessDetails, BusinessName, BusinessType, ReturnSummary} import uk.gov.hmrc.rdsdatacacheproxy.gambling.services.GamblingService +import java.time.LocalDate import scala.concurrent.Future class GamblingControllerSpec extends SpecBase with MockitoSugar { @@ -87,14 +87,98 @@ class GamblingControllerSpec extends SpecBase with MockitoSugar { verify(mockService).getReturnSummary(eqTo("XWM00000001770"))(any()) } + "returns 400 when InvalidMgdRegNumber" in new Setup { + when(mockService.getReturnSummary(any())(any())) + .thenReturn(Future.successful(Left(InvalidMgdRegNumber))) + + val req = FakeRequest(GET, "/gambling/return-summary/jhrfdshgksdhg") + val res = controller.getReturnSummary(" ")(req) + + status(res) mustBe BAD_REQUEST + contentAsJson(res) mustBe Json.obj( + "code" -> "INVALID_MGD_REG_NUMBER", + "message" -> "mgdRegNumber does not exist" + ) + + verify(mockService).getReturnSummary(eqTo(" "))(any()) + } + + "returns 500 when UnexpectedError" in new Setup { + when(mockService.getReturnSummary(any())(any())) + .thenReturn(Future.successful(Left(UnexpectedError))) + + val req = FakeRequest(GET, "/gambling/return-summary/ERR00001770") + val res = controller.getReturnSummary("ERR00001770")(req) + + status(res) mustBe INTERNAL_SERVER_ERROR + contentAsJson(res) mustBe Json.obj( + "code" -> "UNEXPECTED_ERROR", + "message" -> "Unexpected error occurred" + ) + + verify(mockService).getReturnSummary(eqTo("ERR00001770"))(any()) + } } + "GamblingController#getBusinessName" - { + + "returns 200 when service succeeds" in new Setup { + val dateTime: Some[LocalDate] = Some(LocalDate.of(2026, 4, 20)) + val name = BusinessName("XWM00000001770", + Some("fooBar"), + Some("foobar"), + Some("fooBar"), + Some("fooBar"), + Some("fooBar"), + Some(BusinessType.Partnership), + Some("fooBar"), + dateTime + ) + + when(mockService.getBusinessName(eqTo("XWM00000001770"))(any())) + .thenReturn(Future.successful(Right(name))) + + val req = FakeRequest(GET, "/gambling/business-name/XWM00000001770") + val res = controller.getBusinessName("XWM00000001770")(req) + + status(res) mustBe OK + contentType(res) mustBe Some(JSON) + contentAsJson(res) mustBe Json.toJson(name) + + verify(mockService).getBusinessName(eqTo("XWM00000001770"))(any()) + verifyNoMoreInteractions(mockService) + } + + "allows request through AuthAction" in new Setup { + val dateTime: Some[LocalDate] = Some(LocalDate.of(2026, 4, 20)) + val name = BusinessName("XWM00000001770", + Some("fooBar"), + Some("foobar"), + Some("fooBar"), + Some("fooBar"), + Some("fooBar"), + Some(BusinessType.Partnership), + Some("fooBar"), + dateTime + ) + + when(mockService.getBusinessName(any())(any())) + .thenReturn(Future.successful(Right(name))) + + val req = FakeRequest(GET, "/gambling/business-name/XWM00000001770") + val res = controller.getBusinessName("XWM00000001770")(req) + + status(res) mustBe OK + + verify(mockService).getBusinessName(eqTo("XWM00000001770"))(any()) + } + } "returns 400 when InvalidMgdRegNumber" in new Setup { - when(mockService.getReturnSummary(any())(any())) + when(mockService.getBusinessName(any())(any())) .thenReturn(Future.successful(Left(InvalidMgdRegNumber))) - val req = FakeRequest(GET, "/gambling/return-summary/jhrfdshgksdhg") - val res = controller.getReturnSummary(" ")(req) + val req = FakeRequest(GET, "/gambling/business-name/jhrfdshgksdhg") + val res = controller.getBusinessName(" ")(req) status(res) mustBe BAD_REQUEST contentAsJson(res) mustBe Json.obj( @@ -102,15 +186,96 @@ class GamblingControllerSpec extends SpecBase with MockitoSugar { "message" -> "mgdRegNumber does not exist" ) - verify(mockService).getReturnSummary(eqTo(" "))(any()) + verify(mockService).getBusinessName(eqTo(" "))(any()) } "returns 500 when UnexpectedError" in new Setup { - when(mockService.getReturnSummary(any())(any())) + when(mockService.getBusinessName(any())(any())) + .thenReturn(Future.successful(Left(UnexpectedError))) + + val req = FakeRequest(GET, "/gambling/business-name/ERR00001770") + val res = controller.getBusinessName("ERR00001770")(req) + + status(res) mustBe INTERNAL_SERVER_ERROR + contentAsJson(res) mustBe Json.obj( + "code" -> "UNEXPECTED_ERROR", + "message" -> "Unexpected error occurred" + ) + + verify(mockService).getBusinessName(eqTo("ERR00001770"))(any()) + } + + "GamblingController#getBusinessDetails" - { + + "returns 200 when service succeeds for BusinessDetails" in new Setup { + val summary = BusinessDetails("XWM00000001770", + Some(BusinessType.SoleProprietor), + 1, + true, + Some(LocalDate.of(2024, 4, 21)), + Some("bar"), + LocalDate.of(2024, 4, 21) + ) + + when(mockService.getBusinessDetails(eqTo("XWM00000001770"))(any())) + .thenReturn(Future.successful(Right(summary))) + + val req = FakeRequest(GET, "/gambling/business-details/XWM00000001770") + val res = controller.getBusinessDetails("XWM00000001770")(req) + + status(res) mustBe OK + contentType(res) mustBe Some(JSON) + contentAsJson(res) mustBe Json.toJson(summary) + + verify(mockService).getBusinessDetails(eqTo("XWM00000001770"))(any()) + verifyNoMoreInteractions(mockService) + } + + "allows request through AuthAction for BusinessDetails" in new Setup { + val summary = + BusinessDetails("XWM00000001770", + Some(BusinessType.SoleProprietor), + 1, + true, + Some(LocalDate.of(2024, 4, 21)), + Some("bar"), + LocalDate.of(2024, 4, 21) + ) + + when(mockService.getBusinessDetails(any())(any())) + .thenReturn(Future.successful(Right(summary))) + + val req = FakeRequest(GET, "/gambling/business-details/XWM00000001770") + val res = controller.getBusinessDetails("XWM00000001770")(req) + + status(res) mustBe OK + + verify(mockService).getBusinessDetails(eqTo("XWM00000001770"))(any()) + } + } + + "returns 400 when InvalidMgdRegNumber for BusinessDetails" in new Setup { + when(mockService.getBusinessDetails(any())(any())) + .thenReturn(Future.successful(Left(InvalidMgdRegNumber))) + + val req = FakeRequest(GET, "/gambling/business-details/jhrfdshgksdhg") + val res = controller.getBusinessDetails(" ")(req) + + status(res) mustBe BAD_REQUEST + contentAsJson(res) mustBe Json.obj( + "code" -> "INVALID_MGD_REG_NUMBER", + "message" -> "mgdRegNumber does not exist" + ) + + verify(mockService).getBusinessDetails(eqTo(" "))(any()) + } + + "returns 500 when UnexpectedError for BusinessDetails" in new Setup { + when(mockService.getBusinessDetails(any())(any())) .thenReturn(Future.successful(Left(UnexpectedError))) - val req = FakeRequest(GET, "/gambling/return-summary/ERR00001770") - val res = controller.getReturnSummary("ERR00001770")(req) + val req = FakeRequest(GET, "/gambling/business-details/ERR00001770") + val res = controller.getBusinessDetails("ERR00001770")(req) status(res) mustBe INTERNAL_SERVER_ERROR contentAsJson(res) mustBe Json.obj( @@ -118,7 +283,7 @@ class GamblingControllerSpec extends SpecBase with MockitoSugar { "message" -> "Unexpected error occurred" ) - verify(mockService).getReturnSummary(eqTo("ERR00001770"))(any()) + verify(mockService).getBusinessDetails(eqTo("ERR00001770"))(any()) } } diff --git a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingModelSpec.scala b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingModelSpec.scala new file mode 100644 index 0000000..c2b4cd4 --- /dev/null +++ b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingModelSpec.scala @@ -0,0 +1,117 @@ +/* + * 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. + */ + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import play.api.libs.json.Json +import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.* +import java.time.LocalDate + +class GamblingModelSpec extends AnyWordSpec with Matchers { + + "ReturnSummaryModel" should { + "read and write a full-populated object" in { + val jsonAsString: String = + s""" + |{ + |"mgdRegNumber": "XYZ00000000000", + |"returnsDue": 2, + |"returnsOverdue": 1 + |} + """.stripMargin + + val json = Json.parse(jsonAsString) + val model = json.as[ReturnSummary] + + model mustBe ReturnSummary( + mgdRegNumber = "XYZ00000000000", + returnsDue = 2, + returnsOverdue = 1 + ) + + Json.toJson(model) mustBe json + } + } + "BusinessNameModel" should { + val dateBusinessName: LocalDate = LocalDate.of(1991, 1, 1) + "read and write a full-populated object" in { + val jsonAsString: String = + s""" + |{ + |"mgdRegNumber": "XYZ00000000000", + |"solePropTitle": "Mr", + |"solePropFirstName": "John", + |"solePropMidName": "C", + |"solePropLastName": "Doe", + |"businessName": "John Doe Co.", + |"businessType": 1, + |"tradingName": "DoeDoe", + |"systemDate": "$dateBusinessName" + |} + """.stripMargin + + val json = Json.parse(jsonAsString) + val model = json.as[BusinessName] + + model mustBe BusinessName( + mgdRegNumber = "XYZ00000000000", + solePropTitle = Some("Mr"), + solePropFirstName = Some("John"), + solePropMidName = Some("C"), + solePropLastName = Some("Doe"), + businessName = Some("John Doe Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("DoeDoe"), + systemDate = Some(dateBusinessName) + ) + + Json.toJson(model) mustBe json + } + } + + "BusinessDetailsModel" should { + val date: LocalDate = LocalDate.of(2000, 1, 1) + "read and write a full-populated object" in { + val jsonAsString: String = + s""" + |{ + |"mgdRegNumber": "XYZ00000000000", + |"businessType": 1, + |"currentlyRegistered": 1, + |"groupReg": true, + |"dateOfRegistration": "$date", + |"businessPartnerNumber": "bar", + |"systemDate": "$date" + |} + """.stripMargin + + val json = Json.parse(jsonAsString) + val model = json.as[BusinessDetails] + + model mustBe BusinessDetails( + mgdRegNumber = "XYZ00000000000", + businessType = Some(BusinessType.SoleProprietor), + currentlyRegistered = 1, + groupReg = true, + dateOfRegistration = Some(LocalDate.of(2000, 1, 1)), + businessPartnerNumber = Some("bar"), + systemDate = LocalDate.of(2000, 1, 1) + ) + + Json.toJson(model) mustBe json + } + } +} diff --git a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingStubData.scala b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingStubData.scala index acc5615..7122d7e 100644 --- a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingStubData.scala +++ b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/models/GamblingStubData.scala @@ -83,24 +83,165 @@ object GamblingStubData { // ------------------------- def getBusinessDetails(mgdRegNumber: String): BusinessDetails = mgdRegNumber match { - case "ERR00000000000" => throw new RuntimeException("Simulated downstream failure") case "EMPTY000000000" => throw new RuntimeException("No business details found") - case _ => + case "NULL000000000" => + throw new RuntimeException("Null cursor") + + case "XYZ00000000000" => + BusinessDetails( + mgdRegNumber = mgdRegNumber, + businessType = Some(BusinessType.CorporateBody), + currentlyRegistered = 2, + groupReg = false, + dateOfRegistration = Some(LocalDate.of(2024, 4, 21)), + businessPartnerNumber = Some("bar"), + systemDate = LocalDate.of(2024, 4, 21) + ) + case "XYZ00000000001" => BusinessDetails( mgdRegNumber = mgdRegNumber, businessType = None, currentlyRegistered = 1, groupReg = false, - dateOfRegistration = Some(LocalDate.now().minusYears(1)), - businessPartnerNumber = Some("BPN123456"), - systemDate = LocalDate.now() + dateOfRegistration = Some(LocalDate.of(2024, 4, 21)), + businessPartnerNumber = Some("bar"), + systemDate = LocalDate.of(2024, 4, 21) + ) + case "XYZ00000000010" => + BusinessDetails( + mgdRegNumber = mgdRegNumber, + businessType = Some(BusinessType.UnincorporatedBody), + currentlyRegistered = 2, + groupReg = true, + dateOfRegistration = Some(LocalDate.of(2024, 4, 21)), + businessPartnerNumber = Some("bar"), + systemDate = LocalDate.of(2024, 4, 21) + ) + case "XYZ00000000012" => + BusinessDetails( + mgdRegNumber = mgdRegNumber, + businessType = Some(BusinessType.SoleProprietor), + currentlyRegistered = 2, + groupReg = true, + dateOfRegistration = Some(LocalDate.of(2023, 4, 21)), + businessPartnerNumber = Some("barfoo"), + systemDate = LocalDate.of(2023, 4, 21) + ) + case "XYZ00000000021" => + BusinessDetails( + mgdRegNumber = mgdRegNumber, + businessType = Some(BusinessType.Partnership), + currentlyRegistered = 2, + groupReg = false, + dateOfRegistration = Some(LocalDate.of(2024, 1, 21)), + businessPartnerNumber = Some("barbar"), + systemDate = LocalDate.of(2024, 1, 21) + ) + case _ => + BusinessDetails( + mgdRegNumber = mgdRegNumber, + businessType = Some(BusinessType.Partnership), + currentlyRegistered = 0, + groupReg = false, + dateOfRegistration = Some(LocalDate.of(2026, 4, 22)), + businessPartnerNumber = Some("unknown"), + systemDate = LocalDate.of(2026, 4, 22) + ) + } + + def getBusinessName(mgdRegNumber: String): BusinessName = { + val dateTimeOne: Some[LocalDate] = Some(LocalDate.of(2026, 4, 20)) + val dateTimeTwo: Some[LocalDate] = Some(LocalDate.of(2026, 1, 1)) + val dateTimeThree: Some[LocalDate] = Some(LocalDate.of(1991, 1, 1)) + mgdRegNumber match { + case "ERR00000000000" => + throw new RuntimeException("Simulated downstream failure") + + case "EMPTY000000000" => + throw new RuntimeException("No business name found") + + case "NULL000000000" => + throw new RuntimeException("Null cursor") + + case "XYZ00000000000" => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Mr"), + solePropFirstName = Some("John"), + solePropMidName = Some("C"), + solePropLastName = Some("Doe"), + businessName = Some("John Doe Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("DoeDoe"), + systemDate = dateTimeOne + ) + case "XYZ00000000001" => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Mrs"), + solePropFirstName = Some("Jane"), + solePropMidName = Some("C"), + solePropLastName = Some("Doe"), + businessName = Some("Jane Doe Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("DoeDoe"), + systemDate = dateTimeTwo + ) + case "XYZ00000000010" => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Mrs"), + solePropFirstName = Some("Marge"), + solePropMidName = Some("Jacqueline"), + solePropLastName = Some("Simpson"), + businessName = Some("Pretzel Wagon"), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Marge Simpson"), + systemDate = dateTimeOne + ) + case "XYZ00000000012" => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Miss"), + solePropFirstName = Some("Catherine"), + solePropMidName = None, + solePropLastName = Some("Havisham"), + businessName = Some("Failed Expectations"), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Miss Havisham"), + systemDate = dateTimeThree + ) + case "XYZ00000000021" => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Mr"), + solePropFirstName = Some("Eugine"), + solePropMidName = Some("H"), + solePropLastName = Some("Krabs"), + businessName = Some("Krusty Krab"), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Mr Krabs"), + systemDate = dateTimeThree + ) + case _ => + BusinessName( + mgdRegNumber, + solePropTitle = Some("Mr"), + solePropFirstName = Some("Foo"), + solePropMidName = Some("B"), + solePropLastName = Some("Bar"), + businessName = Some("FooBar Co."), + businessType = Some(BusinessType.SoleProprietor), + tradingName = Some("Foobar"), + systemDate = dateTimeOne ) } + } // ------------------------- // MgdCertificate diff --git a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingServiceSpec.scala b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingServiceSpec.scala index 5396a12..f5eccc8 100644 --- a/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingServiceSpec.scala +++ b/test/uk/gov/hmrc/rdsdatacacheproxy/gambling/services/GamblingServiceSpec.scala @@ -20,10 +20,11 @@ import org.mockito.ArgumentMatchers.eq as eqTo import org.mockito.Mockito.{reset, verify, verifyNoMoreInteractions, when} import org.scalatest.matchers.must.Matchers.mustBe import uk.gov.hmrc.rdsdatacacheproxy.base.SpecBase -import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.GamblingError.{InvalidMgdRegNumber, UnexpectedError} import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.* +import uk.gov.hmrc.rdsdatacacheproxy.gambling.models.GamblingError.{InvalidMgdRegNumber, UnexpectedError} import uk.gov.hmrc.rdsdatacacheproxy.gambling.repositories.GamblingDataSource +import java.time.LocalDate import scala.concurrent.Future final class GamblingServiceSpec extends SpecBase { @@ -91,6 +92,75 @@ final class GamblingServiceSpec extends SpecBase { verifyNoMoreInteractions(repository) } } + "GamblingService#getBusinessName" - { + + "return Right(summary) when repository succeeds" in { + + val summary = BusinessName( + mgdRegNumber = validMgdRegNumber, + solePropTitle = Some("Mr"), + solePropFirstName = Some("Foo"), + solePropMidName = Some("B"), + solePropLastName = Some("Bar"), + businessName = Some("FooBar Co."), + businessType = Some(BusinessType.Partnership), + tradingName = Some("Foobar"), + systemDate = Some(LocalDate.of(1991, 1, 1)) + ) + + when(repository.getBusinessName(eqTo(validMgdRegNumber))) + .thenReturn(Future.successful(summary)) + + val result = service.getBusinessName(validMgdRegNumber).futureValue + + result mustBe Right(summary) + verify(repository).getBusinessName(eqTo(validMgdRegNumber)) + verifyNoMoreInteractions(repository) + } + + "normalise input (trim + uppercase) before calling repository" in { + + val rawInput = " xwm12345678901 " + + val summary = BusinessName( + mgdRegNumber = normalisedMgdRegNumber, + solePropTitle = Some("Mr"), + solePropFirstName = Some("John"), + solePropMidName = Some("C"), + solePropLastName = Some("Doe"), + businessName = Some("John Doe Co."), + businessType = Some(BusinessType.Partnership), + tradingName = Some("DoeDoe"), + systemDate = Some(LocalDate.of(1991, 1, 1)) + ) + + when(repository.getBusinessName(eqTo(normalisedMgdRegNumber))) + .thenReturn(Future.successful(summary)) + + val result = service.getBusinessName(rawInput).futureValue + result mustBe Right(summary) + verify(repository).getBusinessName(eqTo(normalisedMgdRegNumber)) + verifyNoMoreInteractions(repository) + } + + "return InvalidMgdRegNumber and not call repository when input is invalid" in { + + val invalidInput = "xwm12345678" + val result = service.getBusinessName(invalidInput).futureValue + result mustBe Left(InvalidMgdRegNumber) + verifyNoMoreInteractions(repository) + } + + "return UnexpectedError when repository throws exception" in { + + when(repository.getReturnSummary(eqTo(validMgdRegNumber))) + .thenReturn(Future.failed(new RuntimeException("DB failure when calling repo"))) + val result = service.getReturnSummary(validMgdRegNumber).futureValue + result mustBe Left(UnexpectedError) + verify(repository).getReturnSummary(eqTo(validMgdRegNumber)) + verifyNoMoreInteractions(repository) + } + } "GamblingService#getMgdCertificate" - {