diff --git a/app/config/Module.scala b/app/config/Module.scala
index 67d9d1b5..79a1f21d 100755
--- a/app/config/Module.scala
+++ b/app/config/Module.scala
@@ -36,6 +36,7 @@ class Module extends AbstractModule {
bind(classOf[JourneyAnswersRepository]).to(classOf[MongoJourneyAnswersRepository])
bind(classOf[SelfEmploymentConnector]).to(classOf[SelfEmploymentConnectorImpl])
bind(classOf[JourneyStatusService]).to(classOf[JourneyStatusServiceImpl])
+ bind(classOf[NICsAnswersService]).to(classOf[NICsAnswersServiceImpl])
()
}
diff --git a/app/connectors/SelfEmploymentConnectorImpl.scala b/app/connectors/SelfEmploymentConnectorImpl.scala
index 550e13ac..fbb38da2 100644
--- a/app/connectors/SelfEmploymentConnectorImpl.scala
+++ b/app/connectors/SelfEmploymentConnectorImpl.scala
@@ -16,6 +16,7 @@
package connectors
+import cats.data.EitherT
import com.typesafe.config.ConfigFactory
import config.AppConfig
import connectors.SelfEmploymentConnector._
@@ -23,10 +24,13 @@ import models.common.TaxYear.{asTys, endDate, startDate}
import models.common._
import models.connector.IntegrationContext.IFSHeaderCarrier
import models.connector._
+import models.connector.api_1638.RequestSchemaAPI1638
+import models.connector.api_1639.SuccessResponseAPI1639
import models.connector.api_1802.request.{CreateAmendSEAnnualSubmissionRequestBody, CreateAmendSEAnnualSubmissionRequestData}
import models.connector.api_1894.request.{CreateSEPeriodSummaryRequestBody, CreateSEPeriodSummaryRequestData}
import models.connector.api_1895.request.{AmendSEPeriodSummaryRequestBody, AmendSEPeriodSummaryRequestData}
-import uk.gov.hmrc.http.{HeaderCarrier, HttpClient}
+import models.domain.ApiResultT
+import uk.gov.hmrc.http.{HeaderCarrier, HttpClient, HttpReads}
import utils.Logging
import javax.inject.{Inject, Singleton}
@@ -43,6 +47,13 @@ trait SelfEmploymentConnector {
def createSEPeriodSummary(data: CreateSEPeriodSummaryRequestData)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1894Response]
def amendSEPeriodSummary(data: AmendSEPeriodSummaryRequestData)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1895Response]
def listSEPeriodSummary(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1965Response]
+
+ def getDisclosuresSubmission(
+ ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Option[SuccessResponseAPI1639]]
+ def upsertDisclosuresSubmission(ctx: JourneyContextWithNino, data: RequestSchemaAPI1638)(implicit
+ hc: HeaderCarrier,
+ ec: ExecutionContext): ApiResultT[Unit]
+ def deleteDisclosuresSubmission(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Unit]
}
object SelfEmploymentConnector {
@@ -53,6 +64,8 @@ object SelfEmploymentConnector {
type Api1894Response = ApiResponse[api_1894.response.CreateSEPeriodSummaryResponse]
type Api1895Response = ApiResponse[api_1895.response.AmendSEPeriodSummaryResponse]
type Api1965Response = ApiResponse[api_1965.ListSEPeriodSummariesResponse]
+ type Api1638Response = ApiResponse[Unit]
+ type Api1639Response = ApiResponseOption[SuccessResponseAPI1639]
}
@Singleton
@@ -75,6 +88,9 @@ class SelfEmploymentConnectorImpl @Inject() (http: HttpClient, appConfig: AppCon
private def periodicSummaryDetailUrl(nino: Nino, incomeSourceId: BusinessId, taxYear: TaxYear) =
s"${baseUrl(nino, incomeSourceId, taxYear)}/periodic-summary-detail?from=${startDate(taxYear)}&to=${endDate(taxYear)}"
+ private def disclosuresSubmissionUrl(nino: Nino, taxYear: TaxYear) =
+ s"${appConfig.ifsBaseUrl}/income-tax/disclosures/$nino/${taxYear.toYYYY_YY}"
+
// TODO Move to GetBusinessDetailsConnector
def getBusinesses(idType: IdType, idNumber: String)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1171Response] = {
val context = mkIFSMetadata(IFSApiName.Api1171, api1171BusinessDetailsUrl(idType, idNumber))
@@ -117,4 +133,32 @@ class SelfEmploymentConnectorImpl @Inject() (http: HttpClient, appConfig: AppCon
val context = mkIFSMetadata(IFSApiName.Api1803, url)
get[Api1803Response](http, context)
}
+
+ def getDisclosuresSubmission(
+ ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Option[SuccessResponseAPI1639]] = {
+ val url = disclosuresSubmissionUrl(ctx.nino, ctx.taxYear)
+ val context = mkIFSMetadata(IFSApiName.Api1639, url)
+ implicit val reads: HttpReads[ApiResponse[Option[SuccessResponseAPI1639]]] = commonGetReads[SuccessResponseAPI1639]
+
+ EitherT(get[Api1639Response](http, context))
+ }
+
+ def upsertDisclosuresSubmission(ctx: JourneyContextWithNino, data: RequestSchemaAPI1638)(implicit
+ hc: HeaderCarrier,
+ ec: ExecutionContext): ApiResultT[Unit] = {
+ val url = disclosuresSubmissionUrl(ctx.nino, ctx.taxYear)
+ val context = mkIFSMetadata(IFSApiName.Api1638, url)
+ implicit val reads: HttpReads[ApiResponse[Unit]] = commonNoBodyResponse
+
+ EitherT(put[RequestSchemaAPI1638, Api1638Response](http, context, data))
+ }
+
+ def deleteDisclosuresSubmission(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Unit] = {
+ val url = disclosuresSubmissionUrl(ctx.nino, ctx.taxYear)
+ val context = mkIFSMetadata(IFSApiName.Api1640, url)
+ implicit val reads: HttpReads[ApiResponse[Unit]] = commonDeleteReads
+
+ EitherT(delete(http, context))
+ }
+
}
diff --git a/app/connectors/package.scala b/app/connectors/package.scala
index 70fe7834..7f1e11a5 100644
--- a/app/connectors/package.scala
+++ b/app/connectors/package.scala
@@ -52,4 +52,13 @@ package object connectors {
ConnectorRequestInfo("PUT", context.url, context.api).logRequestWithBody(logger, body)
http.PUT[Req, Resp](context.url, body)(writes, reads, context.enrichedHeaderCarrier, ec)
}
+
+ def delete[Resp: HttpReads](http: HttpClient, context: IntegrationContext)(implicit
+ hc: HeaderCarrier,
+ ec: ExecutionContext,
+ logger: Logger): Future[Resp] = {
+ val reads = implicitly[HttpReads[Resp]]
+ ConnectorRequestInfo("DELETE", context.url, context.api).logRequest(logger)
+ http.DELETE[Resp](context.url)(reads, context.enrichedHeaderCarrier, ec)
+ }
}
diff --git a/app/controllers/JourneyAnswersController.scala b/app/controllers/JourneyAnswersController.scala
index 88e5fdd0..4aa4557b 100644
--- a/app/controllers/JourneyAnswersController.scala
+++ b/app/controllers/JourneyAnswersController.scala
@@ -54,6 +54,7 @@ import models.frontend.expenses.tailoring.ExpensesTailoring.TotalAmount
import models.frontend.expenses.tailoring.ExpensesTailoringAnswers._
import models.frontend.expenses.workplaceRunningCosts.WorkplaceRunningCostsAnswers
import models.frontend.income.IncomeJourneyAnswers
+import models.frontend.nics.NICsAnswers
import play.api.libs.json.Format.GenericFormat
import play.api.libs.json.{Reads, Writes}
import play.api.mvc.{Action, AnyContent, ControllerComponents, Result}
@@ -72,7 +73,8 @@ class JourneyAnswersController @Inject() (auth: AuthorisedAction,
abroadAnswersService: AbroadAnswersService,
incomeService: IncomeAnswersService,
expensesService: ExpensesAnswersService,
- capitalAllowancesService: CapitalAllowancesAnswersService)(implicit ec: ExecutionContext)
+ capitalAllowancesService: CapitalAllowancesAnswersService,
+ nicsAnswersService: NICsAnswersService)(implicit ec: ExecutionContext)
extends BackendController(cc)
with Logging {
@@ -422,4 +424,14 @@ class JourneyAnswersController @Inject() (auth: AuthorisedAction,
handleOptionalApiResult(capitalAllowancesService.getStructuresBuildings(JourneyContextWithNino(taxYear, businessId, user.getMtditid, nino)))
}
+ def saveNationalInsuranceContributions(taxYear: TaxYear, businessId: BusinessId, nino: Nino): Action[AnyContent] = auth.async { implicit user =>
+ getBodyWithCtx[NICsAnswers](taxYear, businessId, nino) { (ctx, answers) =>
+ nicsAnswersService.saveAnswers(ctx, answers).map(_ => NoContent)
+ }
+ }
+
+ def getNationalInsuranceContributions(taxYear: TaxYear, businessId: BusinessId, nino: Nino): Action[AnyContent] = auth.async { implicit user =>
+ handleOptionalApiResult(nicsAnswersService.getAnswers(JourneyContextWithNino(taxYear, businessId, user.getMtditid, nino)))
+ }
+
}
diff --git a/app/models/common/TaxYear.scala b/app/models/common/TaxYear.scala
index 8fa46564..53909baa 100644
--- a/app/models/common/TaxYear.scala
+++ b/app/models/common/TaxYear.scala
@@ -23,6 +23,8 @@ import java.time.LocalDate
final case class TaxYear(endYear: Int) extends AnyVal {
override def toString: String = endYear.toString
+
+ def toYYYY_YY: String = s"${endYear - 1}-${endYear.toString.takeRight(2)}"
}
object TaxYear {
diff --git a/app/models/connector/IFSApiName.scala b/app/models/connector/IFSApiName.scala
index 2620ccef..1570762a 100644
--- a/app/models/connector/IFSApiName.scala
+++ b/app/models/connector/IFSApiName.scala
@@ -26,6 +26,9 @@ object IFSApiName extends Enum[IFSApiName] {
val values = IndexedSeq[IFSApiName]()
case object Api1171 extends IFSApiName("1171")
+ case object Api1638 extends IFSApiName("1638")
+ case object Api1639 extends IFSApiName("1639")
+ case object Api1640 extends IFSApiName("1640")
case object Api1786 extends IFSApiName("1786")
case object Api1802 extends IFSApiName("1802")
case object Api1803 extends IFSApiName("1803")
diff --git a/app/models/connector/api_1638/RequestSchemaAPI1638.scala b/app/models/connector/api_1638/RequestSchemaAPI1638.scala
new file mode 100644
index 00000000..dfc6e562
--- /dev/null
+++ b/app/models/connector/api_1638/RequestSchemaAPI1638.scala
@@ -0,0 +1,66 @@
+/*
+ * 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.connector.api_1638
+
+import models.connector.api_1639.SuccessResponseAPI1639
+import models.frontend.nics.NICsAnswers
+import play.api.libs.json._
+
+/** Represents the Swagger definition for requestSchemaAPI1638.
+ */
+case class RequestSchemaAPI1638(
+ taxAvoidance: Option[List[RequestSchemaAPI1638TaxAvoidanceInner]],
+ class2Nics: Option[RequestSchemaAPI1638Class2Nics]
+)
+
+object RequestSchemaAPI1638 {
+ implicit lazy val requestSchemaAPI1638JsonFormat: Format[RequestSchemaAPI1638] = Json.format[RequestSchemaAPI1638]
+
+ /** Some(false) is not possible to set on class2VoluntaryContributions. We cannot send an empty object therefore if there are no other fields we
+ * have to call DELETE.
+ *
+ * @return
+ * None if the object needs to be DELETED or Some() if it needs to be updated via PUT
+ */
+ def mkRequestBody(answers: NICsAnswers, maybeExistingDisclosures: Option[SuccessResponseAPI1639]): Option[RequestSchemaAPI1638] = {
+ val existingDisclosures = maybeExistingDisclosures.getOrElse(SuccessResponseAPI1639.empty)
+
+ val bodyForPut = RequestSchemaAPI1638(
+ taxAvoidance = existingDisclosures.taxAvoidance.map { taxAvoidance =>
+ taxAvoidance.map { taxAvoidanceInner =>
+ RequestSchemaAPI1638TaxAvoidanceInner(
+ srn = taxAvoidanceInner.srn,
+ taxYear = taxAvoidanceInner.taxYear
+ )
+ }
+ },
+ class2Nics = existingDisclosures.class2Nics.map { class2Nics =>
+ RequestSchemaAPI1638Class2Nics(
+ class2VoluntaryContributions = class2Nics.class2VoluntaryContributions
+ )
+ }
+ )
+
+ if (answers.class2NICs) {
+ Some(bodyForPut.copy(class2Nics = Some(RequestSchemaAPI1638Class2Nics(Some(true)))))
+ } else if (bodyForPut.taxAvoidance.isDefined) {
+ Some(bodyForPut.copy(class2Nics = None))
+ } else {
+ None
+ }
+ }
+}
diff --git a/app/models/connector/api_1638/RequestSchemaAPI1638Class2Nics.scala b/app/models/connector/api_1638/RequestSchemaAPI1638Class2Nics.scala
new file mode 100644
index 00000000..38b8e2ab
--- /dev/null
+++ b/app/models/connector/api_1638/RequestSchemaAPI1638Class2Nics.scala
@@ -0,0 +1,29 @@
+/*
+ * 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.connector.api_1638
+
+import play.api.libs.json._
+
+/** Voluntary contributions.
+ */
+case class RequestSchemaAPI1638Class2Nics(
+ class2VoluntaryContributions: Option[Boolean]
+)
+
+object RequestSchemaAPI1638Class2Nics {
+ implicit lazy val requestSchemaAPI1638Class2NicsJsonFormat: Format[RequestSchemaAPI1638Class2Nics] = Json.format[RequestSchemaAPI1638Class2Nics]
+}
diff --git a/app/models/connector/api_1638/RequestSchemaAPI1638TaxAvoidanceInner.scala b/app/models/connector/api_1638/RequestSchemaAPI1638TaxAvoidanceInner.scala
new file mode 100644
index 00000000..c7e251ba
--- /dev/null
+++ b/app/models/connector/api_1638/RequestSchemaAPI1638TaxAvoidanceInner.scala
@@ -0,0 +1,31 @@
+/*
+ * 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.connector.api_1638
+
+import play.api.libs.json._
+
+/** Represents the Swagger definition for requestSchemaAPI1638_taxAvoidance_inner.
+ */
+case class RequestSchemaAPI1638TaxAvoidanceInner(
+ srn: String,
+ taxYear: String
+)
+
+object RequestSchemaAPI1638TaxAvoidanceInner {
+ implicit lazy val requestSchemaAPI1638TaxAvoidanceInnerJsonFormat: Format[RequestSchemaAPI1638TaxAvoidanceInner] =
+ Json.format[RequestSchemaAPI1638TaxAvoidanceInner]
+}
diff --git a/app/models/connector/api_1639/SuccessResponseAPI1639.scala b/app/models/connector/api_1639/SuccessResponseAPI1639.scala
new file mode 100644
index 00000000..a4255708
--- /dev/null
+++ b/app/models/connector/api_1639/SuccessResponseAPI1639.scala
@@ -0,0 +1,32 @@
+/*
+ * 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.connector.api_1639
+
+import play.api.libs.json._
+
+/** Represents the Swagger definition for successResponseAPI1639.
+ */
+case class SuccessResponseAPI1639(
+ taxAvoidance: Option[List[SuccessResponseAPI1639TaxAvoidanceInner]],
+ class2Nics: Option[SuccessResponseAPI1639Class2Nics]
+)
+
+object SuccessResponseAPI1639 {
+ implicit lazy val successResponseAPI1639JsonFormat: Format[SuccessResponseAPI1639] = Json.format[SuccessResponseAPI1639]
+
+ def empty: SuccessResponseAPI1639 = SuccessResponseAPI1639(None, None)
+}
diff --git a/app/models/connector/api_1639/SuccessResponseAPI1639Class2Nics.scala b/app/models/connector/api_1639/SuccessResponseAPI1639Class2Nics.scala
new file mode 100644
index 00000000..3fd9efc4
--- /dev/null
+++ b/app/models/connector/api_1639/SuccessResponseAPI1639Class2Nics.scala
@@ -0,0 +1,30 @@
+/*
+ * 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.connector.api_1639
+
+import play.api.libs.json._
+
+/** Voluntary contributions.
+ */
+case class SuccessResponseAPI1639Class2Nics(
+ class2VoluntaryContributions: Option[Boolean]
+)
+
+object SuccessResponseAPI1639Class2Nics {
+ implicit lazy val successResponseAPI1639Class2NicsJsonFormat: Format[SuccessResponseAPI1639Class2Nics] =
+ Json.format[SuccessResponseAPI1639Class2Nics]
+}
diff --git a/app/models/connector/api_1639/SuccessResponseAPI1639TaxAvoidanceInner.scala b/app/models/connector/api_1639/SuccessResponseAPI1639TaxAvoidanceInner.scala
new file mode 100644
index 00000000..aa798609
--- /dev/null
+++ b/app/models/connector/api_1639/SuccessResponseAPI1639TaxAvoidanceInner.scala
@@ -0,0 +1,31 @@
+/*
+ * 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.connector.api_1639
+
+import play.api.libs.json._
+
+/** Represents the Swagger definition for successResponseAPI1639_taxAvoidance_inner.
+ */
+case class SuccessResponseAPI1639TaxAvoidanceInner(
+ srn: String,
+ taxYear: String
+)
+
+object SuccessResponseAPI1639TaxAvoidanceInner {
+ implicit lazy val successResponseAPI1639TaxAvoidanceInnerJsonFormat: Format[SuccessResponseAPI1639TaxAvoidanceInner] =
+ Json.format[SuccessResponseAPI1639TaxAvoidanceInner]
+}
diff --git a/app/models/connector/package.scala b/app/models/connector/package.scala
index 48e81788..12935825 100644
--- a/app/models/connector/package.scala
+++ b/app/models/connector/package.scala
@@ -21,33 +21,72 @@ import connectors.DownstreamParser.CommonDownstreamParser
import models.error.DownstreamError
import models.logging.ConnectorResponseInfo
import play.api.Logger
-import play.api.http.Status.{CREATED, OK}
+import play.api.http.Status._
import play.api.libs.json._
import uk.gov.hmrc.http.{HttpReads, HttpResponse}
import java.time.format.DateTimeFormatter
package object connector {
- type ApiResponse[A] = Either[DownstreamError, A]
+ type ApiResponse[A] = Either[DownstreamError, A]
+ type ApiResponseOption[A] = Either[DownstreamError, Option[A]]
val dateFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE
- implicit def httpReads[A: Reads](implicit logger: Logger): HttpReads[ApiResponse[A]] = (method: String, url: String, response: HttpResponse) => {
+ /** It treats any non OK / CREATED / ACCEPTED as an error
+ */
+ implicit def commonReads[A: Reads](implicit logger: Logger): HttpReads[ApiResponse[A]] = (method: String, url: String, response: HttpResponse) => {
ConnectorResponseInfo(method, url, response).logResponseWarnOn4xx(logger)
response.status match {
- case OK | CREATED =>
- response.json
- .validate[A]
- .fold[Either[DownstreamError, A]](
- errors => Left(createCommonErrorParser(method, url, response).reportInvalidJsonError(errors.toList)),
- parsedModel => Right(parsedModel)
- )
-
- case _ => Left(createCommonErrorParser(method, url, response).pagerDutyError(response))
+ case OK | CREATED | ACCEPTED => toA(response, method, url)
+ case _ => Left(createCommonErrorParser(method, url, response).pagerDutyError(response))
}
}
+ /** It treats any non OK / CREATED / NO_CONTENT / ACCEPTED as an error, and return Unit otherwise
+ */
+ def commonNoBodyResponse(implicit logger: Logger): HttpReads[ApiResponse[Unit]] = (method: String, url: String, response: HttpResponse) => {
+ ConnectorResponseInfo(method, url, response).logResponseWarnOn4xx(logger)
+
+ response.status match {
+ case OK | NO_CONTENT | CREATED | ACCEPTED => Right(())
+ case _ => Left(createCommonErrorParser(method, url, response).pagerDutyError(response))
+ }
+ }
+
+ /** It treats NOT_FOUND / OK / CREATED / ACCEPTED as correct response and returns None
+ */
+ def commonGetReads[A: Reads](implicit logger: Logger): HttpReads[ApiResponse[Option[A]]] =
+ (method: String, url: String, response: HttpResponse) => {
+ ConnectorResponseInfo(method, url, response).logResponseWarnOn4xx(logger)
+
+ response.status match {
+ case OK | CREATED | ACCEPTED => toA(response, method, url).map(Some(_))
+ case NOT_FOUND | NO_CONTENT => Right(None)
+ case _ => Left(createCommonErrorParser(method, url, response).pagerDutyError(response))
+ }
+ }
+
+ /** It treats NOT_FOUND / NO_CONTENT / OK / ACCEPTED as correct response and returns None
+ */
+ def commonDeleteReads(implicit logger: Logger): HttpReads[ApiResponse[Unit]] = (method: String, url: String, response: HttpResponse) => {
+ ConnectorResponseInfo(method, url, response).logResponseWarnOn4xx(logger)
+
+ response.status match {
+ case NOT_FOUND | NO_CONTENT | OK | ACCEPTED => Right(())
+ case _ => Left(createCommonErrorParser(method, url, response).pagerDutyError(response))
+ }
+ }
+
+ private def toA[A: Reads](response: HttpResponse, method: String, url: String): Either[DownstreamError, A] =
+ response.json
+ .validate[A]
+ .fold[Either[DownstreamError, A]](
+ errors => Left(createCommonErrorParser(method, url, response).reportInvalidJsonError(errors.toList)),
+ parsedModel => Right(parsedModel)
+ )
+
private def createCommonErrorParser(method: String, url: String, response: HttpResponse): DownstreamParser =
CommonDownstreamParser(method, url, response)
}
diff --git a/app/models/database/nics/NICsStorageAnswers.scala b/app/models/database/nics/NICsStorageAnswers.scala
new file mode 100644
index 00000000..58ce9aae
--- /dev/null
+++ b/app/models/database/nics/NICsStorageAnswers.scala
@@ -0,0 +1,34 @@
+/*
+ * 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.database.nics
+
+import models.frontend.nics.NICsAnswers
+import play.api.libs.json.{Json, OFormat}
+
+/** The API does not support false. We store only false here.
+ */
+final case class NICsStorageAnswers(class2NICs: Option[Boolean])
+
+object NICsStorageAnswers {
+ implicit val format: OFormat[NICsStorageAnswers] = Json.format[NICsStorageAnswers]
+
+ /** We send class2=true to API, so we don't store it. We don't send false, so we must store it based on our rule to store everything not sent
+ * downstream
+ */
+ def fromJourneyAnswers(answers: NICsAnswers): NICsStorageAnswers =
+ if (answers.class2NICs) NICsStorageAnswers(None) else NICsStorageAnswers(Some(false))
+}
diff --git a/app/models/frontend/nics/NICsAnswers.scala b/app/models/frontend/nics/NICsAnswers.scala
new file mode 100644
index 00000000..65145c57
--- /dev/null
+++ b/app/models/frontend/nics/NICsAnswers.scala
@@ -0,0 +1,39 @@
+/*
+ * 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.frontend.nics
+
+import models.connector.api_1639.SuccessResponseAPI1639
+import models.database.nics.NICsStorageAnswers
+import play.api.libs.json.{Format, Json}
+
+case class NICsAnswers(class2NICs: Boolean)
+
+object NICsAnswers {
+ implicit val formats: Format[NICsAnswers] = Json.format[NICsAnswers]
+
+ def mkPriorData(maybeApiAnswers: Option[SuccessResponseAPI1639], maybeDbAnswers: Option[NICsStorageAnswers]): Option[NICsAnswers] = {
+ val existingClass2Nics = for {
+ answers <- maybeApiAnswers
+ nicsAnswers <- answers.class2Nics
+ class2Nics <- nicsAnswers.class2VoluntaryContributions
+ } yield class2Nics
+
+ val maybeNics = existingClass2Nics.map(NICsAnswers(_))
+ maybeNics.orElse(maybeDbAnswers.flatMap(_.class2NICs.map(NICsAnswers(_))))
+ }
+
+}
diff --git a/app/repositories/MongoJourneyAnswersRepository.scala b/app/repositories/MongoJourneyAnswersRepository.scala
index 9da80bb6..d0a3af54 100644
--- a/app/repositories/MongoJourneyAnswersRepository.scala
+++ b/app/repositories/MongoJourneyAnswersRepository.scala
@@ -31,7 +31,8 @@ import org.mongodb.scala.model.Projections.exclude
import org.mongodb.scala.model._
import org.mongodb.scala.result.UpdateResult
import play.api.Logger
-import play.api.libs.json.{JsValue, Json}
+import play.api.libs.json.{JsValue, Json, Reads}
+import services.journeyAnswers.getPersistedAnswers
import uk.gov.hmrc.mongo.MongoComponent
import uk.gov.hmrc.mongo.play.json.PlayMongoRepository
import utils.Logging
@@ -40,9 +41,11 @@ import java.time.{Clock, Instant, ZoneOffset}
import java.util.concurrent.TimeUnit
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
+import scala.reflect.ClassTag
trait JourneyAnswersRepository {
def get(ctx: JourneyContext): ApiResultT[Option[JourneyAnswers]]
+ def getAnswers[A: Reads](ctx: JourneyContext)(implicit ct: ClassTag[A]): ApiResultT[Option[A]]
def getAll(taxYear: TaxYear, mtditid: Mtditid, businesses: List[Business]): ApiResultT[TaskList]
def upsertAnswers(ctx: JourneyContext, newData: JsValue): ApiResultT[Unit]
def setStatus(ctx: JourneyContext, status: JourneyStatus): ApiResultT[Unit]
@@ -100,6 +103,12 @@ class MongoJourneyAnswersRepository @Inject() (mongo: MongoComponent, appConfig:
.headOption())
}
+ def getAnswers[A: Reads](ctx: JourneyContext)(implicit ct: ClassTag[A]): ApiResultT[Option[A]] =
+ for {
+ row <- get(ctx)
+ maybeDbAnswers <- getPersistedAnswers[A](row)
+ } yield maybeDbAnswers
+
def getAll(taxYear: TaxYear, mtditid: Mtditid, businesses: List[Business]): ApiResultT[TaskList] = {
val filter = filterAllJourneys(taxYear, mtditid)
val projection = exclude("data")
diff --git a/app/services/journeyAnswers/NICsAnswersService.scala b/app/services/journeyAnswers/NICsAnswersService.scala
new file mode 100644
index 00000000..a23a1afb
--- /dev/null
+++ b/app/services/journeyAnswers/NICsAnswersService.scala
@@ -0,0 +1,64 @@
+/*
+ * 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 services.journeyAnswers
+
+import cats.implicits.toFunctorOps
+import connectors.SelfEmploymentConnector
+import models.common._
+import models.connector.api_1638.RequestSchemaAPI1638
+import models.database.nics.NICsStorageAnswers
+import models.domain.ApiResultT
+import models.frontend.nics.NICsAnswers
+import play.api.libs.json.Json
+import repositories.JourneyAnswersRepository
+import uk.gov.hmrc.http.HeaderCarrier
+
+import javax.inject.{Inject, Singleton}
+import scala.concurrent.ExecutionContext
+
+trait NICsAnswersService {
+ def saveAnswers(ctx: JourneyContextWithNino, answers: NICsAnswers)(implicit hc: HeaderCarrier): ApiResultT[Unit]
+ def getAnswers(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier): ApiResultT[Option[NICsAnswers]]
+}
+
+@Singleton
+class NICsAnswersServiceImpl @Inject() (connector: SelfEmploymentConnector, repository: JourneyAnswersRepository)(implicit ec: ExecutionContext)
+ extends NICsAnswersService {
+
+ def saveAnswers(ctx: JourneyContextWithNino, answers: NICsAnswers)(implicit hc: HeaderCarrier): ApiResultT[Unit] =
+ for {
+ existingAnswers <- connector.getDisclosuresSubmission(ctx)
+ upsertRequest = RequestSchemaAPI1638.mkRequestBody(answers, existingAnswers)
+ _ <- upsertOrDeleteData(upsertRequest, ctx)
+ storageAnswers = NICsStorageAnswers.fromJourneyAnswers(answers)
+ _ <- repository.upsertAnswers(ctx.toJourneyContext(JourneyName.NationalInsuranceContributions), Json.toJson(storageAnswers))
+ } yield ()
+
+ def getAnswers(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier): ApiResultT[Option[NICsAnswers]] =
+ for {
+ apiAnswers <- connector.getDisclosuresSubmission(ctx)
+ dbAnswers <- repository.getAnswers[NICsStorageAnswers](ctx.toJourneyContext(JourneyName.NationalInsuranceContributions))
+ } yield NICsAnswers.mkPriorData(apiAnswers, dbAnswers)
+
+ private def upsertOrDeleteData(maybeClass2Nics: Option[RequestSchemaAPI1638], ctx: JourneyContextWithNino)(implicit
+ hc: HeaderCarrier): ApiResultT[Unit] =
+ maybeClass2Nics match {
+ case Some(data) => connector.upsertDisclosuresSubmission(ctx, data).void
+ case None => connector.deleteDisclosuresSubmission(ctx)
+ }
+
+}
diff --git a/build.sh b/build.sh
index be4594d5..37ded957 100755
--- a/build.sh
+++ b/build.sh
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
-sbt clean scalafmtAll scalafmtSbt compile
+sbt clean scalafmtAll scalafmtSbt compile test:compile it:compile
diff --git a/conf/app.routes b/conf/app.routes
index 0e0fbccc..92366c1c 100755
--- a/conf/app.routes
+++ b/conf/app.routes
@@ -96,3 +96,5 @@ GET /:taxYear/:businessId/capital-allowances-special-tax-sites/:nino/ans
POST /:taxYear/:businessId/capital-allowances-structures-buildings/:nino/answers controllers.JourneyAnswersController.saveStructuresBuildings(taxYear: TaxYear, businessId: BusinessId, nino: Nino)
GET /:taxYear/:businessId/capital-allowances-structures-buildings/:nino/answers controllers.JourneyAnswersController.getStructuresBuildings(taxYear: TaxYear, businessId: BusinessId, nino: Nino)
+POST /:taxYear/:businessId/national-insurance-contributions/:nino/answers controllers.JourneyAnswersController.saveNationalInsuranceContributions(taxYear: TaxYear, businessId: BusinessId, nino: Nino)
+GET /:taxYear/:businessId/national-insurance-contributions/:nino/answers controllers.JourneyAnswersController.getNationalInsuranceContributions(taxYear: TaxYear, businessId: BusinessId, nino: Nino)
diff --git a/conf/application.conf b/conf/application.conf
index 2d12c582..7d810c6e 100755
--- a/conf/application.conf
+++ b/conf/application.conf
@@ -83,6 +83,9 @@ microservice {
port = 9303 #This is the port for the income-tax-submission-stub
authorisation-token {
1171 = "secret"
+ 1638 = "secret"
+ 1639 = "secret"
+ 1640 = "secret"
1786 = "secret"
1802 = "secret"
1803 = "secret"
diff --git a/it/connectors/SelfEmploymentConnectorImplISpec.scala b/it/connectors/SelfEmploymentConnectorImplISpec.scala
index d96d1876..a81bc671 100644
--- a/it/connectors/SelfEmploymentConnectorImplISpec.scala
+++ b/it/connectors/SelfEmploymentConnectorImplISpec.scala
@@ -22,6 +22,8 @@ import connectors.data.{Api1786Test, Api1803Test}
import helpers.WiremockSpec
import models.common.JourneyContextWithNino
import models.common.TaxYear.{asTys, endDate, startDate}
+import models.connector.api_1638.{RequestSchemaAPI1638, RequestSchemaAPI1638Class2Nics}
+import models.connector.api_1639.{SuccessResponseAPI1639, SuccessResponseAPI1639Class2Nics}
import models.connector.api_1802.request.{CreateAmendSEAnnualSubmissionRequestBody, CreateAmendSEAnnualSubmissionRequestData}
import models.connector.api_1802.response.CreateAmendSEAnnualSubmissionResponse
import models.connector.api_1894.request._
@@ -29,6 +31,7 @@ import models.connector.api_1894.response.CreateSEPeriodSummaryResponse
import models.connector.api_1895.request.{AmendSEPeriodSummaryRequestBody, AmendSEPeriodSummaryRequestData, Incomes}
import models.connector.api_1895.response.AmendSEPeriodSummaryResponse
import models.connector.api_1965.{ListSEPeriodSummariesResponse, PeriodDetails}
+import org.scalatest.EitherValues._
import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
import play.api.http.Status.{CREATED, OK}
import play.api.libs.json.Json
@@ -122,6 +125,49 @@ class SelfEmploymentConnectorImplISpec extends WiremockSpec with IntegrationBase
}
}
+ "getDisclosuresSubmission" must {
+ "return None when no data" in {
+ val result = connector.getDisclosuresSubmission(ctx).value.futureValue
+ assert(result === Right(None))
+ }
+
+ "return existing data" in new ApiDisclosuresTest {
+ stubGetWithResponseBody(
+ url = downstreamUrl,
+ expectedStatus = OK,
+ expectedResponse = responseJson
+ )
+ val result = connector.getDisclosuresSubmission(ctx).value.futureValue.value
+ assert(result === Some(SuccessResponseAPI1639(None, Some(SuccessResponseAPI1639Class2Nics(Some(true))))))
+ }
+ }
+
+ "upsertDisclosuresSubmission" must {
+ "return unit" in new ApiDisclosuresTest {
+ val request: RequestSchemaAPI1638 = RequestSchemaAPI1638(None, Some(RequestSchemaAPI1638Class2Nics(Some(true))))
+ stubPutWithRequestAndResponseBody(
+ url = downstreamUrl,
+ requestBody = request,
+ expectedResponse = "",
+ expectedStatus = CREATED
+ )
+ val result = connector.upsertDisclosuresSubmission(ctx, request).value.futureValue
+ assert(result === Right(()))
+ }
+ }
+
+ "deleteDisclosuresSubmission" must {
+ "return unit" in new ApiDisclosuresTest {
+ stubDelete(
+ url = downstreamUrl,
+ expectedResponse = "",
+ expectedStatus = OK
+ )
+ val result = connector.deleteDisclosuresSubmission(ctx).value.futureValue
+ assert(result === Right(()))
+ }
+ }
+
trait Api1802Test {
val downstreamSuccessResponse: String = Json.stringify(Json.obj("transactionReference" -> "someId"))
val requestBody: CreateAmendSEAnnualSubmissionRequestBody = CreateAmendSEAnnualSubmissionRequestBody(None, None, None)
@@ -177,4 +223,16 @@ class SelfEmploymentConnectorImplISpec extends WiremockSpec with IntegrationBase
data.taxYear)}&to=${endDate(data.taxYear)}"
}
+ trait ApiDisclosuresTest {
+ val responseJson: String = Json.stringify(Json.parse(s"""
+ |{
+ | "class2Nics": {
+ | "class2VoluntaryContributions": true
+ | }
+ |}
+ |""".stripMargin))
+
+ val downstreamUrl = s"/income-tax/disclosures/${ctx.nino.value}/${ctx.taxYear.toYYYY_YY}"
+ }
+
}
diff --git a/it/helpers/WiremockStubHelpers.scala b/it/helpers/WiremockStubHelpers.scala
index 5b5d0d3f..bf0f096a 100644
--- a/it/helpers/WiremockStubHelpers.scala
+++ b/it/helpers/WiremockStubHelpers.scala
@@ -100,6 +100,21 @@ trait WiremockStubHelpers {
.withHeader("Content-Type", "application/json; charset=utf-8")))
}
+ def stubDelete(url: String, expectedResponse: String, expectedStatus: Int, requestHeaders: Seq[HttpHeader] = Seq.empty): StubMapping = {
+ val mapping: MappingBuilder = requestHeaders
+ .foldLeft(delete(urlMatching(url))) { (result, nxt) =>
+ result.withHeader(nxt.key(), equalTo(nxt.firstValue()))
+ }
+
+ stubFor(
+ mapping
+ .willReturn(
+ aResponse()
+ .withStatus(expectedStatus)
+ .withBody(expectedResponse)
+ .withHeader("Content-Type", "application/json; charset=utf-8")))
+ }
+
def auditStubs(): Unit = {
val auditResponseCode = 204
stubPostWithoutResponseAndRequestBody("/write/audit", auditResponseCode)
diff --git a/scalastyle-config.xml b/scalastyle-config.xml
index c981835d..aa26bbd7 100644
--- a/scalastyle-config.xml
+++ b/scalastyle-config.xml
@@ -96,11 +96,6 @@
-
-
-
-
-
diff --git a/test/controllers/JourneyAnswersControllerSpec.scala b/test/controllers/JourneyAnswersControllerSpec.scala
index d4847862..529daafd 100644
--- a/test/controllers/JourneyAnswersControllerSpec.scala
+++ b/test/controllers/JourneyAnswersControllerSpec.scala
@@ -45,6 +45,7 @@ import models.frontend.expenses.repairsandmaintenance.RepairsAndMaintenanceCosts
import models.frontend.expenses.staffcosts.StaffCostsJourneyAnswers
import models.frontend.expenses.tailoring.ExpensesTailoringAnswers._
import models.frontend.expenses.workplaceRunningCosts.WorkplaceRunningCostsAnswers
+import models.frontend.nics.NICsAnswers
import models.frontend.prepop.AdjustmentsPrepopAnswers.fromAnnualAdjustmentsType
import org.scalacheck.Gen
import org.scalamock.handlers.{CallHandler2, CallHandler3}
@@ -62,15 +63,24 @@ import scala.concurrent.Future
class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckPropertyChecks with TableDrivenPropertyChecks {
- val underTest = new JourneyAnswersController(
- auth = mockAuthorisedAction,
- cc = stubControllerComponents,
- abroadAnswersService = StubAbroadAnswersService(),
- incomeService = StubIncomeAnswersService(),
- expensesService = StubExpensesAnswersService(),
- capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(),
- prepopAnswersService = StubPrepopAnswersService()
- )
+ private def mkUnderTest(abroadAnswersService: StubAbroadAnswersService = StubAbroadAnswersService(),
+ incomeService: StubIncomeAnswersService = StubIncomeAnswersService(),
+ expensesService: StubExpensesAnswersService = StubExpensesAnswersService(),
+ capitalAllowancesService: StubCapitalAllowancesAnswersAnswersService = StubCapitalAllowancesAnswersAnswersService(),
+ prepopAnswersService: StubPrepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService: StubNICsAnswersService = StubNICsAnswersService()): JourneyAnswersController =
+ new JourneyAnswersController(
+ auth = mockAuthorisedAction,
+ cc = stubControllerComponents,
+ abroadAnswersService = abroadAnswersService,
+ incomeService = incomeService,
+ expensesService = expensesService,
+ capitalAllowancesService = capitalAllowancesService,
+ prepopAnswersService = prepopAnswersService,
+ nicsAnswersService = nicsAnswersService
+ )
+
+ val underTest = mkUnderTest()
private def checkNoContent(action: Action[AnyContent]): Unit =
behave like testRoute(
@@ -88,18 +98,38 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = capitalAllowancesService,
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
+ // TODO It's better to use lower testNoContent + testGetReturnAnswers + testSaveAnswers directly
private def checkGetAndSave[A: Writes](actionForGetNoContent: Action[AnyContent],
actionForGet: Action[AnyContent],
expectedBodyForGet: String,
dataGen: Gen[A],
actionForSave: Action[AnyContent]): Unit = {
+
+ testNoContent(actionForGetNoContent)
+ testGetReturnAnswers(actionForGet, expectedBodyForGet)
+
+ s"Save answers and return a $NO_CONTENT when successful" in {
+ forAll(dataGen) { data =>
+ behave like testRoute(
+ request = buildRequest(data),
+ expectedStatus = NO_CONTENT,
+ expectedBody = "",
+ methodBlock = () => actionForSave
+ )
+ }
+ }
+ }
+
+ def testNoContent(actionForGetNoContent: Action[AnyContent]): Unit =
s"Get return $NO_CONTENT if there is no answers" in {
checkNoContent(actionForGetNoContent)
}
+ def testGetReturnAnswers(actionForGet: Action[AnyContent], expectedBodyForGet: String): Unit =
s"Get return answers" in {
behave like testRoute(
request = buildRequestNoContent,
@@ -109,17 +139,15 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
)
}
+ def testSaveAnswers[A: Writes](actionForSave: Action[AnyContent], data: A): Unit =
s"Save answers and return a $NO_CONTENT when successful" in {
- forAll(dataGen) { data =>
- behave like testRoute(
- request = buildRequest(data),
- expectedStatus = NO_CONTENT,
- expectedBody = "",
- methodBlock = () => actionForSave
- )
- }
+ behave like testRoute(
+ request = buildRequest(data),
+ expectedStatus = NO_CONTENT,
+ expectedBody = "",
+ methodBlock = () => actionForSave
+ )
}
- }
"SelfEmploymentAbroadAnswers" should {
s"Get return $NO_CONTENT if there is no answers" in {
@@ -135,7 +163,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
@@ -170,7 +199,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(getAnswersRes = Some(answers).asRight),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
@@ -204,7 +234,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
prepopAnswersService = StubPrepopAnswersService(
getIncomeAnswersResult = incomePrepopAnswers.asRight,
getAdjustmentsAnswersResult = adjustmentsPrepopAnswers.asRight
- )
+ ),
+ nicsAnswersService = StubNICsAnswersService()
)
s"getIncomeAnswers from downstream" in {
behave like testRoute(
@@ -242,7 +273,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(getTailoringJourneyAnswers = journeyAnswers),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
request = buildRequestNoContent,
@@ -628,7 +660,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(getCapitalAllowancesTailoring = Some(answers).asRight),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
@@ -666,7 +699,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(getZeroEmissionCars = Some(answers).asRight),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
@@ -705,7 +739,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = StubExpensesAnswersService(),
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(getZeroEmissionGoodsVehicleCars = Some(answers).asRight),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
behave like testRoute(
@@ -809,6 +844,15 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
)
}
+ "NationalInsuranceContributions" should {
+ val answers = NICsAnswers(true)
+ val controllerWithData = mkUnderTest(nicsAnswersService = StubNICsAnswersService(getAnswersRes = Right(Some(answers))))
+
+ testNoContent(underTest.getNationalInsuranceContributions(currTaxYear, businessId, nino))
+ testSaveAnswers(underTest.saveNationalInsuranceContributions(currTaxYear, businessId, nino), answers)
+ testGetReturnAnswers(controllerWithData.getNationalInsuranceContributions(currTaxYear, businessId, nino), Json.toJson(answers).toString())
+ }
+
trait GetExpensesTest[T] {
val expensesService: ExpensesAnswersService = mock[ExpensesAnswersService]
val journeyAnswers: T
@@ -820,7 +864,8 @@ class JourneyAnswersControllerSpec extends ControllerBehaviours with ScalaCheckP
incomeService = StubIncomeAnswersService(),
expensesService = expensesService,
capitalAllowancesService = StubCapitalAllowancesAnswersAnswersService(),
- prepopAnswersService = StubPrepopAnswersService()
+ prepopAnswersService = StubPrepopAnswersService(),
+ nicsAnswersService = StubNICsAnswersService()
)
def mockExpensesService(): CallHandler3[JourneyContextWithNino, Api1786ExpensesResponseParser[T], HeaderCarrier, ApiResultT[T]] =
diff --git a/test/data/api1639/SuccessResponseAPI1639Data.scala b/test/data/api1639/SuccessResponseAPI1639Data.scala
new file mode 100644
index 00000000..58bc959e
--- /dev/null
+++ b/test/data/api1639/SuccessResponseAPI1639Data.scala
@@ -0,0 +1,27 @@
+/*
+ * 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 data.api1639
+
+import models.connector.api_1639.{SuccessResponseAPI1639, SuccessResponseAPI1639Class2Nics, SuccessResponseAPI1639TaxAvoidanceInner}
+import utils.BaseSpec.currTaxYear
+
+object SuccessResponseAPI1639Data {
+ val class2NicsTrue = SuccessResponseAPI1639(None, Some(SuccessResponseAPI1639Class2Nics(Some(true))))
+ val full = SuccessResponseAPI1639(
+ Some(List(SuccessResponseAPI1639TaxAvoidanceInner("arn", currTaxYear.endYear.toString))),
+ Some(SuccessResponseAPI1639Class2Nics(Some(true))))
+}
diff --git a/test/models/common/TaxYearSpec.scala b/test/models/common/TaxYearSpec.scala
index 2d198734..463404fe 100644
--- a/test/models/common/TaxYearSpec.scala
+++ b/test/models/common/TaxYearSpec.scala
@@ -16,21 +16,27 @@
package models.common
-import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
-import utils.BaseSpec
-
-class TaxYearSpec extends BaseSpec {
+import org.scalatest.wordspec.AnyWordSpecLike
+class TaxYearSpec extends AnyWordSpecLike {
private val year = TaxYear(2024)
- "getting start and end dates" must {
+ "getting start and end dates" should {
"get April 5th and 6th" in {
- TaxYear.startDate(year) shouldBe "2023-04-06"
- TaxYear.endDate(year) shouldBe "2024-04-05"
+ assert(TaxYear.startDate(year) === "2023-04-06")
+ assert(TaxYear.endDate(year) === "2024-04-05")
}
}
+
"get a TYS (YY-YY) format" in {
- TaxYear.asTys(year) shouldBe "23-24"
+ assert(TaxYear.asTys(year) === "23-24")
+ }
+
+ "toYYYY_YY" should {
+ "get a YYYY-YY format" in {
+ assert(TaxYear(2024).toYYYY_YY === "2023-24")
+ assert(TaxYear(1999).toYYYY_YY === "1998-99")
+ }
}
}
diff --git a/test/models/connector/api_1638/RequestSchemaAPI1638Spec.scala b/test/models/connector/api_1638/RequestSchemaAPI1638Spec.scala
new file mode 100644
index 00000000..7dc37eea
--- /dev/null
+++ b/test/models/connector/api_1638/RequestSchemaAPI1638Spec.scala
@@ -0,0 +1,44 @@
+/*
+ * 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.connector.api_1638
+
+import data.api1639.SuccessResponseAPI1639Data
+import models.frontend.nics.NICsAnswers
+import org.scalatest.wordspec.AnyWordSpecLike
+import utils.BaseSpec.currTaxYear
+
+class RequestSchemaAPI1638Spec extends AnyWordSpecLike {
+ "mkRequestBody" should {
+ "return class2 set to true" in {
+ val answers = NICsAnswers(true)
+ val result = RequestSchemaAPI1638.mkRequestBody(answers, None)
+ assert(result === Some(RequestSchemaAPI1638(None, Some(RequestSchemaAPI1638Class2Nics(Some(true))))))
+ }
+
+ "return None when answer class2 set to false and no other fields in the object exist" in {
+ val answers = NICsAnswers(false)
+ val result = RequestSchemaAPI1638.mkRequestBody(answers, maybeExistingDisclosures = None)
+ assert(result === None)
+ }
+
+ "return an object with class 2 set to None if other fields exist" in {
+ val answers = NICsAnswers(false)
+ val result = RequestSchemaAPI1638.mkRequestBody(answers, Some(SuccessResponseAPI1639Data.full))
+ assert(result === Some(RequestSchemaAPI1638(Some(List(RequestSchemaAPI1638TaxAvoidanceInner("arn", currTaxYear.toString))), None)))
+ }
+ }
+}
diff --git a/test/models/frontend/nics/NICsAnswersSpec.scala b/test/models/frontend/nics/NICsAnswersSpec.scala
new file mode 100644
index 00000000..e5cf496a
--- /dev/null
+++ b/test/models/frontend/nics/NICsAnswersSpec.scala
@@ -0,0 +1,47 @@
+/*
+ * 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.frontend.nics
+
+import data.api1639.SuccessResponseAPI1639Data.class2NicsTrue
+import models.database.nics.NICsStorageAnswers
+import org.scalatest.wordspec.AnyWordSpecLike
+import org.scalatest.OptionValues._
+
+class NICsAnswersSpec extends AnyWordSpecLike {
+ "fromApi1639" should {
+ "return None when there are no class 2 NICs" in {
+ val result = NICsAnswers.mkPriorData(None, None)
+ assert(result.isEmpty)
+ }
+
+ "return class2 from the API" in {
+ val result = NICsAnswers.mkPriorData(Some(class2NicsTrue), None).value
+ assert(result === NICsAnswers(true))
+ }
+
+ "return class2 from the DB if does not exist in API" in {
+ val result = NICsAnswers.mkPriorData(None, Some(NICsStorageAnswers(Some(false)))).value
+ assert(result === NICsAnswers(false))
+ }
+
+ "ignore value from DB if it exist in the API" in {
+ val result = NICsAnswers.mkPriorData(Some(class2NicsTrue), Some(NICsStorageAnswers(Some(false)))).value
+ assert(result === NICsAnswers(true))
+ }
+
+ }
+}
diff --git a/test/services/journeyAnswers/NICsAnswersServiceImplSpec.scala b/test/services/journeyAnswers/NICsAnswersServiceImplSpec.scala
new file mode 100644
index 00000000..69cb7c87
--- /dev/null
+++ b/test/services/journeyAnswers/NICsAnswersServiceImplSpec.scala
@@ -0,0 +1,132 @@
+/*
+ * 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 services.journeyAnswers
+
+import cats.implicits.catsSyntaxEitherId
+import models.connector.api_1638.{RequestSchemaAPI1638, RequestSchemaAPI1638Class2Nics, RequestSchemaAPI1638TaxAvoidanceInner}
+import models.connector.api_1639.{SuccessResponseAPI1639, SuccessResponseAPI1639Class2Nics, SuccessResponseAPI1639TaxAvoidanceInner}
+import models.database.nics.NICsStorageAnswers
+import models.frontend.nics.NICsAnswers
+import org.scalatest.EitherValues._
+import org.scalatest.wordspec.AnyWordSpecLike
+import play.api.libs.json.{JsObject, Json}
+import stubs.connectors.StubSelfEmploymentConnector
+import stubs.repositories.StubJourneyAnswersRepository
+import utils.BaseSpec.{currTaxYearEnd, hc, journeyCtxWithNino}
+import utils.EitherTTestOps.convertScalaFuture
+
+import scala.concurrent.ExecutionContext.Implicits.global
+
+class NICsAnswersServiceImplSpec extends AnyWordSpecLike {
+
+ "save answers" should {
+ "create a new answers if nothing already exist" in new StubbedService {
+ val answers = NICsAnswers(true)
+
+ val result = service.saveAnswers(journeyCtxWithNino, answers).value.futureValue
+
+ assert(result.isRight)
+ assert(connector.upsertDisclosuresSubmissionData === Some(RequestSchemaAPI1638(None, Some(RequestSchemaAPI1638Class2Nics(Some(true))))))
+ assert(repository.lastUpsertedAnswer === Some(JsObject.empty))
+ }
+
+ val disclosuresWithOtherFields =
+ SuccessResponseAPI1639(Some(List(SuccessResponseAPI1639TaxAvoidanceInner("srn", currTaxYearEnd.toUpperCase))), None)
+
+ "setting class2 does not override other fields and true is not stored in DB" in new StubbedService {
+ override val connector = StubSelfEmploymentConnector(getDisclosuresSubmissionResult = Some(disclosuresWithOtherFields).asRight)
+ val answers = NICsAnswers(true)
+
+ val result = service.saveAnswers(journeyCtxWithNino, answers).value.futureValue
+
+ assert(result.isRight)
+ assert(
+ connector.upsertDisclosuresSubmissionData === Some(
+ RequestSchemaAPI1638(
+ Some(List(RequestSchemaAPI1638TaxAvoidanceInner("srn", currTaxYearEnd.toUpperCase()))),
+ Some(RequestSchemaAPI1638Class2Nics(Some(true))))))
+ assert(repository.lastUpsertedAnswer === Some(JsObject.empty))
+ }
+
+ "settings class2 to None when class2 is false and there are other fields in the object" in new StubbedService {
+ override val connector = StubSelfEmploymentConnector(getDisclosuresSubmissionResult = Some(disclosuresWithOtherFields).asRight)
+ val answers = NICsAnswers(false)
+
+ val result = service.saveAnswers(journeyCtxWithNino, answers).value.futureValue
+
+ assert(result.isRight)
+ assert(
+ connector.upsertDisclosuresSubmissionData === Some(
+ RequestSchemaAPI1638(
+ Some(List(RequestSchemaAPI1638TaxAvoidanceInner("srn", currTaxYearEnd.toUpperCase()))),
+ None
+ )
+ )
+ )
+ assert(repository.lastUpsertedAnswer === Some(Json.toJson(NICsStorageAnswers(Some(false)))))
+ }
+
+ "call DELETE if setting to false and the object is empty" in new StubbedService {
+ override val connector = StubSelfEmploymentConnector(getDisclosuresSubmissionResult =
+ Some(SuccessResponseAPI1639(None, Some(SuccessResponseAPI1639Class2Nics(Some(true))))).asRight)
+ val answers = NICsAnswers(false)
+
+ val result = service.saveAnswers(journeyCtxWithNino, answers).value.futureValue
+
+ assert(result.isRight)
+ assert(connector.upsertDisclosuresSubmissionData === None)
+ assert(repository.lastUpsertedAnswer === Some(Json.toJson(answers)))
+ }
+
+ }
+
+ "get answers" should {
+ "return None if there are no answers" in new StubbedService {
+ val result = service.getAnswers(journeyCtxWithNino).value.futureValue.value
+ assert(result === None)
+ }
+
+ "return API version even if database exist" in new StubbedService {
+ override val connector = StubSelfEmploymentConnector(getDisclosuresSubmissionResult =
+ Some(SuccessResponseAPI1639(None, Some(SuccessResponseAPI1639Class2Nics(Some(true))))).asRight)
+ override val repository = StubJourneyAnswersRepository(
+ getAnswers = Right(Some(Json.toJson(NICsStorageAnswers(Some(false)))))
+ )
+
+ val result = service.getAnswers(journeyCtxWithNino).value.futureValue.value
+
+ assert(result === Some(NICsAnswers(true)))
+ }
+
+ "return database version if no API data exist" in new StubbedService {
+ override val repository = StubJourneyAnswersRepository(
+ getAnswers = Right(Some(Json.toJson(NICsStorageAnswers(Some(false)))))
+ )
+
+ val result = service.getAnswers(journeyCtxWithNino).value.futureValue.value
+
+ assert(result === Some(NICsAnswers(false)))
+ }
+ }
+
+ trait StubbedService {
+ val connector = StubSelfEmploymentConnector()
+ val repository = StubJourneyAnswersRepository()
+
+ def service = new NICsAnswersServiceImpl(connector, repository)
+ }
+}
diff --git a/test/stubs/connectors/StubSelfEmploymentConnector.scala b/test/stubs/connectors/StubSelfEmploymentConnector.scala
index 7a5fefeb..527ccc99 100644
--- a/test/stubs/connectors/StubSelfEmploymentConnector.scala
+++ b/test/stubs/connectors/StubSelfEmploymentConnector.scala
@@ -16,10 +16,13 @@
package stubs.connectors
+import cats.data.EitherT
import cats.implicits.{catsSyntaxEitherId, catsSyntaxOptionId}
import connectors.SelfEmploymentConnector
import connectors.SelfEmploymentConnector._
import models.common.{IdType, JourneyContextWithNino}
+import models.connector.api_1638.RequestSchemaAPI1638
+import models.connector.api_1639.SuccessResponseAPI1639
import models.connector.api_1786.{DeductionsType, SelfEmploymentDeductionsDetailTypePosNeg}
import models.connector.api_1802.request.CreateAmendSEAnnualSubmissionRequestData
import models.connector.api_1802.response.CreateAmendSEAnnualSubmissionResponse
@@ -30,6 +33,8 @@ import models.connector.api_1895.request.AmendSEPeriodSummaryRequestData
import models.connector.api_1895.response.AmendSEPeriodSummaryResponse
import models.connector.api_1965.{ListSEPeriodSummariesResponse, PeriodDetails}
import models.connector.{api_1171, api_1786}
+import models.domain.ApiResultT
+import models.error.ServiceError
import stubs.connectors.StubSelfEmploymentConnector._
import uk.gov.hmrc.http.HeaderCarrier
import utils.BaseSpec._
@@ -44,8 +49,12 @@ case class StubSelfEmploymentConnector(
createAmendSEAnnualSubmissionResult: Future[Api1802Response] = Future.successful(api1802SuccessResponse.asRight),
getAnnualSummariesResult: Future[Api1803Response] = Future.successful(api1803SuccessResponse.asRight),
listSEPeriodSummariesResult: Future[Api1965Response] = Future.successful(api1965MatchedResponse.asRight),
- getPeriodicSummaryDetailResult: Future[Api1786Response] = Future.successful(api1786EmptySuccessResponse.asRight))
- extends SelfEmploymentConnector {
+ getPeriodicSummaryDetailResult: Future[Api1786Response] = Future.successful(api1786EmptySuccessResponse.asRight),
+ getDisclosuresSubmissionResult: Either[ServiceError, Option[SuccessResponseAPI1639]] = Right(None),
+ upsertDisclosuresSubmissionResult: Either[ServiceError, Unit] = Right(()),
+ deleteDisclosuresSubmissionResult: Either[ServiceError, Unit] = Right(())
+) extends SelfEmploymentConnector {
+ var upsertDisclosuresSubmissionData: Option[RequestSchemaAPI1638] = None
override def createSEPeriodSummary(
data: CreateSEPeriodSummaryRequestData)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1894Response] =
@@ -70,6 +79,23 @@ case class StubSelfEmploymentConnector(
def getAnnualSummaries(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Api1803Response] =
getAnnualSummariesResult
+
+ def getDisclosuresSubmission(
+ ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Option[SuccessResponseAPI1639]] =
+ EitherT.fromEither[Future](getDisclosuresSubmissionResult)
+
+ def upsertDisclosuresSubmission(ctx: JourneyContextWithNino, data: RequestSchemaAPI1638)(implicit
+ hc: HeaderCarrier,
+ ec: ExecutionContext): ApiResultT[Unit] = {
+ upsertDisclosuresSubmissionData = Some(data)
+ EitherT.fromEither[Future](upsertDisclosuresSubmissionResult)
+ }
+
+ def deleteDisclosuresSubmission(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier, ec: ExecutionContext): ApiResultT[Unit] = {
+ upsertDisclosuresSubmissionData = None
+ EitherT.fromEither[Future](deleteDisclosuresSubmissionResult)
+ }
+
}
object StubSelfEmploymentConnector {
diff --git a/test/stubs/repositories/StubJourneyAnswersRepository.scala b/test/stubs/repositories/StubJourneyAnswersRepository.scala
index 07473d37..d9f7da2c 100644
--- a/test/stubs/repositories/StubJourneyAnswersRepository.scala
+++ b/test/stubs/repositories/StubJourneyAnswersRepository.scala
@@ -23,22 +23,29 @@ import models.database.JourneyAnswers
import models.domain.{ApiResultT, Business}
import models.error.ServiceError
import models.frontend.TaskList
-import play.api.libs.json.JsValue
+import models.jsonAs
+import org.scalatest.EitherValues._
+import play.api.libs.json.{JsObject, JsValue, Reads}
import repositories.JourneyAnswersRepository
import scala.concurrent.{ExecutionContext, Future}
+import scala.reflect.ClassTag
case class StubJourneyAnswersRepository(
getAnswer: Option[JourneyAnswers] = None,
+ getAnswers: Either[ServiceError, Option[JsValue]] = Right(None),
upsertDateField: Either[ServiceError, Unit] = Right(()),
upsertStatusField: Either[ServiceError, Unit] = Right(()),
getAllResult: Either[ServiceError, TaskList] = Right(TaskList.empty),
deleteOneOrMoreJourneys: Either[ServiceError, Unit] = Right(())
) extends JourneyAnswersRepository {
- implicit val ec: ExecutionContext = ExecutionContext.global
+ implicit val ec: ExecutionContext = ExecutionContext.global
+ var lastUpsertedAnswer: Option[JsValue] = None
- def upsertAnswers(ctx: JourneyContext, newData: JsValue): ApiResultT[Unit] =
+ def upsertAnswers(ctx: JourneyContext, newData: JsValue): ApiResultT[Unit] = {
+ lastUpsertedAnswer = Some(newData)
EitherT.fromEither[Future](upsertDateField)
+ }
def setStatus(ctx: JourneyContext, status: JourneyStatus): ApiResultT[Unit] =
EitherT.fromEither[Future](upsertStatusField)
@@ -53,4 +60,7 @@ case class StubJourneyAnswersRepository(
def getAll(taxYear: TaxYear, mtditid: Mtditid, businesses: List[Business]): ApiResultT[TaskList] =
EitherT.fromEither[Future](getAllResult)
+
+ def getAnswers[A: Reads](ctx: JourneyContext)(implicit ct: ClassTag[A]): ApiResultT[Option[A]] =
+ EitherT.fromEither(getAnswers.map(_.map(data => jsonAs[A](data.as[JsObject]).value)))
}
diff --git a/test/stubs/services/StubNICsAnswersService.scala b/test/stubs/services/StubNICsAnswersService.scala
new file mode 100644
index 00000000..c58f4324
--- /dev/null
+++ b/test/stubs/services/StubNICsAnswersService.scala
@@ -0,0 +1,37 @@
+/*
+ * 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 stubs.services
+
+import cats.data.EitherT
+import models.common.JourneyContextWithNino
+import models.domain.ApiResultT
+import models.error.ServiceError
+import models.frontend.nics.NICsAnswers
+import services.journeyAnswers.NICsAnswersService
+import uk.gov.hmrc.http.HeaderCarrier
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+
+case class StubNICsAnswersService(getAnswersRes: Either[ServiceError, Option[NICsAnswers]] = Right(None)) extends NICsAnswersService {
+
+ def saveAnswers(ctx: JourneyContextWithNino, answers: NICsAnswers)(implicit hc: HeaderCarrier): ApiResultT[Unit] =
+ EitherT.rightT[Future, ServiceError](())
+
+ def getAnswers(ctx: JourneyContextWithNino)(implicit hc: HeaderCarrier): ApiResultT[Option[NICsAnswers]] =
+ EitherT.fromEither[Future](getAnswersRes)
+}