diff --git a/app/connectors/ConstructionIndustrySchemeConnector.scala b/app/connectors/ConstructionIndustrySchemeConnector.scala index b2040c8f..74960b28 100644 --- a/app/connectors/ConstructionIndustrySchemeConnector.scala +++ b/app/connectors/ConstructionIndustrySchemeConnector.scala @@ -16,6 +16,7 @@ package connectors +import models.amend.CreateAmendedMonthlyReturnRequest import models.monthlyreturns.* import models.requests.{GetMonthlyReturnForEditRequest, SendSuccessEmailRequest} import models.submission.* @@ -234,4 +235,16 @@ class ConstructionIndustrySchemeConnector @Inject() (config: ServicesConfig, htt } } + def createAmendedMonthlyReturn(request: CreateAmendedMonthlyReturnRequest)(implicit hc: HeaderCarrier): Future[Unit] = + http + .post(url"$cisBaseUrl/amend-monthly-return/create") + .withBody(Json.toJson(request)) + .execute[HttpResponse] + .flatMap { response => + response.status match { + case CREATED => Future.unit + case status => Future.failed(UpstreamErrorResponse(response.body, status, status)) + } + } + } diff --git a/app/controllers/amend/ConfirmAmendmentController.scala b/app/controllers/amend/ConfirmAmendmentController.scala index 740caf36..240900bf 100644 --- a/app/controllers/amend/ConfirmAmendmentController.scala +++ b/app/controllers/amend/ConfirmAmendmentController.scala @@ -17,38 +17,218 @@ package controllers.amend import controllers.actions.* -import pages.amend.ConfirmAmendmentPage +import models.ReturnType.{MonthlyNilReturn, MonthlyStandardReturn} +import models.{ReturnType, UserAnswers} +import models.amend.{AmendmentDetails, CreateAmendedMonthlyReturnRequest} +import models.monthlyreturns.{ContinueReturnJourneyQueryParams, MonthlyReturn, Submission} +import models.requests.OptionalDataRequest +import pages.agent.AgentClientDataPage +import pages.amend.{AmendmentDetailsPage, ConfirmAmendmentPage} +import pages.monthlyreturns.CisIdPage +import play.api.Logging import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository +import services.{AmendMonthlyReturnService, MonthlyReturnService} +import uk.gov.hmrc.http.HeaderCarrier import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController +import uk.gov.hmrc.play.http.HeaderCarrierConverter import views.html.amend.ConfirmAmendmentView import javax.inject.Inject -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} class ConfirmAmendmentController @Inject() ( override val messagesApi: MessagesApi, identify: IdentifierAction, getData: DataRetrievalAction, - requireData: DataRequiredAction, sessionRepository: SessionRepository, + amendMonthlyReturnService: AmendMonthlyReturnService, + monthlyReturnService: MonthlyReturnService, val controllerComponents: MessagesControllerComponents, view: ConfirmAmendmentView )(implicit ec: ExecutionContext) extends FrontendBaseController - with I18nSupport { + with I18nSupport + with Logging { - def onPageLoad: Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => - Ok(view()) - } + def onPageLoad(queryParams: ContinueReturnJourneyQueryParams): Action[AnyContent] = + (identify andThen getData).async { implicit request => + implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequestAndSession(request, request.session) + + validateUserCanAccessRequest(queryParams.instanceId) + .flatMap { + case true => + retrieveAndStoreAmendmentDetails(queryParams).map(_ => Ok(view())) + + case false => + logger.warn( + s"[ConfirmAmendmentController] User ${request.userId} attempted to access instanceId ${queryParams.instanceId} " + + s"which they are not authorised to access" + ) + Future.successful(Redirect(controllers.routes.JourneyRecoveryController.onPageLoad())) + } + .recover { case ex => + logger.warn( + s"[ConfirmAmendmentController] Failed to validate access for instanceId ${queryParams.instanceId}", + ex + ) + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + } + } def onSubmit: Action[AnyContent] = - (identify andThen getData andThen requireData).async { implicit request => - val updatedAnswers = request.userAnswers.set(ConfirmAmendmentPage, true).get + (identify andThen getData).async { implicit request => + request.userAnswers.flatMap(_.get(AmendmentDetailsPage)) match { + case Some(amendmentDetails) => + implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequestAndSession(request, request.session) + + val queryParams = ContinueReturnJourneyQueryParams( + instanceId = amendmentDetails.instanceId, + taxYear = amendmentDetails.taxYear, + taxMonth = amendmentDetails.taxMonth + ) + + validateUserCanAccessRequest(queryParams.instanceId) + .flatMap { + case true => + val createRequest = CreateAmendedMonthlyReturnRequest( + instanceId = amendmentDetails.instanceId, + taxYear = amendmentDetails.taxYear, + taxMonth = amendmentDetails.taxMonth, + version = 0 + ) + + for { + _ <- amendMonthlyReturnService.createAmendedMonthlyReturn(createRequest) + updatedAnswers = + request.userAnswers.getOrElse(UserAnswers(request.userId)).set(ConfirmAmendmentPage, true).get + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect( + controllers.amend.routes.ConfirmAmendmentController.onPageLoad(queryParams) + ) // TODO: DTR-4657 + + case false => + logger.warn( + s"[ConfirmAmendmentController][onSubmit] User ${request.userId} attempted to submit for instanceId ${queryParams.instanceId} " + + s"which they are not authorised to access" + ) + Future.successful(Redirect(controllers.routes.JourneyRecoveryController.onPageLoad())) + } + .recover { case ex => + logger.warn( + s"[ConfirmAmendmentController][onSubmit] Failed to validate access for instanceId ${queryParams.instanceId}", + ex + ) + Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) + } - sessionRepository.set(updatedAnswers).map { _ => - Redirect(controllers.amend.routes.ConfirmAmendmentController.onPageLoad()) + case None => + logger.warn(s"[ConfirmAmendmentController] AmendmentDetails missing from userAnswers") + Future.successful(Redirect(controllers.routes.JourneyRecoveryController.onPageLoad())) } } + + private def validateUserCanAccessRequest( + instanceId: String + )(implicit request: OptionalDataRequest[_], hc: HeaderCarrier): Future[Boolean] = + if (request.isAgent) { + validateAgentCanAccessClient() + } else { + validateOrgCanAccessInstanceId(instanceId) + } + + private def validateAgentCanAccessClient()(implicit + request: OptionalDataRequest[_], + hc: HeaderCarrier + ): Future[Boolean] = + monthlyReturnService.getAgentClient(request.userId).flatMap { + case Some(agentClientData) => + val updatedUserAnswers = + request.userAnswers + .getOrElse(UserAnswers(request.userId)) + .set(AgentClientDataPage, agentClientData) + .get + + sessionRepository.set(updatedUserAnswers).flatMap { _ => + monthlyReturnService.hasClient(agentClientData.taxOfficeNumber, agentClientData.taxOfficeReference) + } + + case None => + logger.warn(s"[ConfirmAmendmentController] Agent user ${request.userId} has no AgentClientData") + Future.successful(false) + } + + private def validateOrgCanAccessInstanceId(instanceId: String)(implicit hc: HeaderCarrier): Future[Boolean] = + monthlyReturnService.getCisTaxpayer().map { taxPayer => + val authorised = taxPayer.uniqueId == instanceId + + if (!authorised) { + logger.warn( + s"[ConfirmAmendmentController] Organisation user attempted to access instanceId $instanceId " + + s"which does not match their uniqueId ${taxPayer.uniqueId}" + ) + } + + authorised + } + + private def retrieveAndStoreAmendmentDetails( + params: ContinueReturnJourneyQueryParams + )(implicit request: OptionalDataRequest[_], hc: HeaderCarrier): Future[Unit] = + monthlyReturnService + .retrieveMonthlyReturnForEditDetails( + instanceId = params.instanceId, + taxMonth = params.taxMonth, + taxYear = params.taxYear + ) + .flatMap { details => + details.monthlyReturn.find(matchesQueryParams(_, params)) match { + case Some(monthlyReturn) => + val amendmentDetails = toAmendmentDetails(params, monthlyReturn, details.submission) + val updatedUserAnswers = + request.userAnswers + .getOrElse(UserAnswers(request.userId)) + .set(CisIdPage, params.instanceId) + .flatMap(_.set(AmendmentDetailsPage, amendmentDetails)) + .get + + sessionRepository.set(updatedUserAnswers).map(_ => ()) + + case None => + Future.failed( + new RuntimeException( + s"No monthly return found for instanceId ${params.instanceId}, taxMonth ${params.taxMonth}, taxYear ${params.taxYear}" + ) + ) + } + } + + private def matchesQueryParams(monthlyReturn: MonthlyReturn, queryParams: ContinueReturnJourneyQueryParams): Boolean = + monthlyReturn.taxYear == queryParams.taxYear && + monthlyReturn.taxMonth == queryParams.taxMonth + + private def toAmendmentDetails( + params: ContinueReturnJourneyQueryParams, + monthlyReturn: MonthlyReturn, + submissions: Seq[Submission] + ): AmendmentDetails = + AmendmentDetails( + instanceId = params.instanceId, + taxYear = params.taxYear, + taxMonth = params.taxMonth, + returnType = deriveReturnType(monthlyReturn), + acceptedTime = acceptedTimeForMonthlyReturn(monthlyReturn, submissions) + ) + + private def deriveReturnType(monthlyReturn: MonthlyReturn): ReturnType = + monthlyReturn.nilReturnIndicator match { + case Some("Y") => MonthlyNilReturn + case _ => MonthlyStandardReturn + } + + private def acceptedTimeForMonthlyReturn(monthlyReturn: MonthlyReturn, submissions: Seq[Submission]): Option[String] = + submissions + .find(_.activeObjectId.contains(monthlyReturn.monthlyReturnId)) + .flatMap(_.acceptedTime) } diff --git a/app/models/amend/AmendmentDetails.scala b/app/models/amend/AmendmentDetails.scala new file mode 100644 index 00000000..2a4b60bd --- /dev/null +++ b/app/models/amend/AmendmentDetails.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.amend + +import models.ReturnType +import play.api.libs.json.{Json, OFormat} + +case class AmendmentDetails( + instanceId: String, + taxYear: Int, + taxMonth: Int, + returnType: ReturnType, + acceptedTime: Option[String] +) + +object AmendmentDetails { + given format: OFormat[AmendmentDetails] = Json.format[AmendmentDetails] +} diff --git a/app/models/amend/CreateAmendedMonthlyReturnRequest.scala b/app/models/amend/CreateAmendedMonthlyReturnRequest.scala new file mode 100644 index 00000000..3bfb4aa2 --- /dev/null +++ b/app/models/amend/CreateAmendedMonthlyReturnRequest.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.amend + +import play.api.libs.json.{Json, OFormat} + +case class CreateAmendedMonthlyReturnRequest( + instanceId: String, + taxYear: Int, + taxMonth: Int, + version: Int +) + +object CreateAmendedMonthlyReturnRequest { + given format: OFormat[CreateAmendedMonthlyReturnRequest] = Json.format[CreateAmendedMonthlyReturnRequest] +} diff --git a/app/pages/amend/AmendmentDetailsPage.scala b/app/pages/amend/AmendmentDetailsPage.scala new file mode 100644 index 00000000..718c4e99 --- /dev/null +++ b/app/pages/amend/AmendmentDetailsPage.scala @@ -0,0 +1,29 @@ +/* + * 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 pages.amend + +import models.amend.AmendmentDetails +import pages.QuestionPage +import play.api.libs.json.JsPath + +case object AmendmentDetailsPage extends QuestionPage[AmendmentDetails] { + + override def path: JsPath = JsPath \ toString + + override def toString: String = "amendmentDetails" + +} diff --git a/app/services/AmendMonthlyReturnService.scala b/app/services/AmendMonthlyReturnService.scala new file mode 100644 index 00000000..f9e0c5cd --- /dev/null +++ b/app/services/AmendMonthlyReturnService.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services + +import connectors.ConstructionIndustrySchemeConnector +import models.amend.CreateAmendedMonthlyReturnRequest +import uk.gov.hmrc.http.HeaderCarrier + +import javax.inject.{Inject, Singleton} +import scala.concurrent.Future + +@Singleton +class AmendMonthlyReturnService @Inject() (cisConnector: ConstructionIndustrySchemeConnector) { + + def createAmendedMonthlyReturn(request: CreateAmendedMonthlyReturnRequest)(implicit hc: HeaderCarrier): Future[Unit] = + cisConnector.createAmendedMonthlyReturn(request) + +} diff --git a/app/services/MonthlyReturnService.scala b/app/services/MonthlyReturnService.scala index a3ce4209..cbc53ca6 100644 --- a/app/services/MonthlyReturnService.scala +++ b/app/services/MonthlyReturnService.scala @@ -45,6 +45,9 @@ class MonthlyReturnService @Inject() ( )(implicit ec: ExecutionContext) extends Logging { + def getCisTaxpayer(implicit hc: HeaderCarrier): Future[CisTaxpayer] = + cisConnector.getCisTaxpayer() + def resolveAndStoreCisId(ua: UserAnswers, isAgent: Boolean)(implicit hc: HeaderCarrier ): Future[(String, UserAnswers)] = diff --git a/conf/app.routes b/conf/app.routes index fcd90f4d..1edd5686 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -171,5 +171,5 @@ POST /monthly-return/cancel-amended-monthly-return GET /amend-monthly-return/what-do-you-want-to-amend-nil controllers.amend.WhatDoYouWantToAmendNilController.onPageLoad() POST /amend-monthly-return/what-do-you-want-to-amend-nil controllers.amend.WhatDoYouWantToAmendNilController.onSubmit() -GET /manage-cis-return/amend-monthly-return/confirm-amendment controllers.amend.ConfirmAmendmentController.onPageLoad() -POST /manage-cis-return/amend-monthly-return/confirm-amendment controllers.amend.ConfirmAmendmentController.onSubmit() +GET /manage-cis-return/amend-monthly-return/confirm-amendments controllers.amend.ConfirmAmendmentController.onPageLoad(queryParams: models.monthlyreturns.ContinueReturnJourneyQueryParams) +POST /manage-cis-return/amend-monthly-return/confirm-amendments controllers.amend.ConfirmAmendmentController.onSubmit() diff --git a/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala b/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala index 1e99d658..dfc729ea 100644 --- a/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala +++ b/it/test/connectors/ConstructionIndustrySchemeConnectorSpec.scala @@ -19,6 +19,7 @@ package connectors import com.github.tomakehurst.wiremock.client.WireMock.* import itutil.ApplicationWithWiremock import models.ReturnType.MonthlyNilReturn +import models.amend.CreateAmendedMonthlyReturnRequest import models.requests.SendSuccessEmailRequest import models.monthlyreturns.* import models.submission.{ChrisSubmissionRequest, CreateSubmissionRequest, UpdateSubmissionRequest} @@ -989,4 +990,44 @@ class ConstructionIndustrySchemeConnectorSpec extends AnyWordSpec ex.asInstanceOf[UpstreamErrorResponse].statusCode mustBe INTERNAL_SERVER_ERROR } } + + "createAmendedMonthlyReturn(payload)" should { + + "POST /cis/amend-monthly-return/create and return Unit on 201" in { + val req = CreateAmendedMonthlyReturnRequest( + instanceId = cisId, + taxYear = 2025, + taxMonth = 1, + version = 0 + ) + + stubFor( + post(urlPathEqualTo("/cis/amend-monthly-return/create")) + .withHeader("Content-Type", equalTo("application/json")) + .withRequestBody(equalToJson(Json.toJson(req).toString(), true, true)) + .willReturn(aResponse().withStatus(CREATED)) + ) + + connector.createAmendedMonthlyReturn(req).futureValue mustBe ((): Unit) + } + + "fail the future when BE returns non-201 (e.g. 500)" in { + val req = CreateAmendedMonthlyReturnRequest( + instanceId = cisId, + taxYear = 2025, + taxMonth = 1, + version = 0 + ) + + stubFor( + post(urlPathEqualTo("/cis/amend-monthly-return/create")) + .willReturn(aResponse().withStatus(INTERNAL_SERVER_ERROR).withBody("boom")) + ) + + val ex = connector.createAmendedMonthlyReturn(req).failed.futureValue + + ex mustBe a[UpstreamErrorResponse] + ex.asInstanceOf[UpstreamErrorResponse].statusCode mustBe INTERNAL_SERVER_ERROR + } + } } diff --git a/test/controllers/amend/ConfirmAmendmentControllerSpec.scala b/test/controllers/amend/ConfirmAmendmentControllerSpec.scala index 481d7ab6..fc8c8e84 100644 --- a/test/controllers/amend/ConfirmAmendmentControllerSpec.scala +++ b/test/controllers/amend/ConfirmAmendmentControllerSpec.scala @@ -17,61 +17,519 @@ package controllers.amend import base.SpecBase +import controllers.actions.{FakeDataRetrievalAction, FakeIdentifierAction} +import models.ReturnType.{MonthlyNilReturn, MonthlyStandardReturn} +import models.UserAnswers +import models.agent.AgentClientData +import models.amend.{AmendmentDetails, CreateAmendedMonthlyReturnRequest} +import models.monthlyreturns.* +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when +import org.mockito.Mockito.* +import org.scalatest.BeforeAndAfterEach import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar.mock -import play.api.inject.bind +import pages.amend.{AmendmentDetailsPage, ConfirmAmendmentPage} +import pages.monthlyreturns.CisIdPage +import play.api.i18n.MessagesApi +import play.api.mvc.{MessagesControllerComponents, PlayBodyParsers} import play.api.test.FakeRequest import play.api.test.Helpers.* import repositories.SessionRepository +import services.{AmendMonthlyReturnService, MonthlyReturnService} +import uk.gov.hmrc.http.HeaderCarrier import views.html.amend.ConfirmAmendmentView -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -class ConfirmAmendmentControllerSpec extends SpecBase { - val confirmAmendmentRoute: String = controllers.amend.routes.ConfirmAmendmentController.onSubmit().url +class ConfirmAmendmentControllerSpec extends SpecBase with MockitoSugar with BeforeAndAfterEach { - "ConfirmAmendment Controller" - { + implicit val ec: ExecutionContext = ExecutionContext.global - "must return OK and the correct view for a GET" in { + private val mockSessionRepository = mock[SessionRepository] + private val mockAmendMonthlyReturnService = mock[AmendMonthlyReturnService] + private val mockMonthlyReturnService = mock[MonthlyReturnService] + private val mockView = mock[ConfirmAmendmentView] - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + private val queryParams = ContinueReturnJourneyQueryParams( + instanceId = "CIS-123", + taxYear = 2025, + taxMonth = 3 + ) - running(application) { - val request = FakeRequest(GET, controllers.amend.routes.ConfirmAmendmentController.onPageLoad().url) + private def taxpayer(cisId: String = "CIS-123"): CisTaxpayer = + CisTaxpayer( + uniqueId = cisId, + taxOfficeNumber = "111", + taxOfficeRef = "AB123", + aoDistrict = None, + aoPayType = None, + aoCheckCode = None, + aoReference = None, + validBusinessAddr = None, + correlation = None, + ggAgentId = None, + employerName1 = Some("Test Ltd"), + employerName2 = None, + agentOwnRef = None, + schemeName = None, + utr = None, + enrolledSig = None + ) - val result = route(application, request).value + private def amendmentDetails: AmendmentDetails = + AmendmentDetails( + instanceId = "CIS-123", + taxYear = 2025, + taxMonth = 3, + returnType = MonthlyStandardReturn, + acceptedTime = Some("2025-04-05T12:00:00Z") + ) - val view = application.injector.instanceOf[ConfirmAmendmentView] + private def monthlyReturnPayload( + taxYear: Int = 2025, + taxMonth: Int = 3, + nilReturnIndicator: Option[String] = Some("N"), + acceptedTime: Option[String] = Some("2025-04-05T12:00:00Z") + ): GetAllMonthlyReturnDetailsResponse = + GetAllMonthlyReturnDetailsResponse( + scheme = Nil, + monthlyReturn = Seq( + MonthlyReturn( + monthlyReturnId = 101, + taxYear = taxYear, + taxMonth = taxMonth, + nilReturnIndicator = nilReturnIndicator + ) + ), + subcontractors = Nil, + monthlyReturnItems = Nil, + submission = Seq( + Submission( + submissionId = 1, + submissionType = "MONTHLY_RETURN", + activeObjectId = Some(101), + status = None, + hmrcMarkGenerated = None, + hmrcMarkGgis = None, + emailRecipient = None, + acceptedTime = acceptedTime, + createDate = None, + lastUpdate = None, + schemeId = 1, + agentId = None, + l_Migrated = None, + submissionRequestDate = None, + govTalkErrorCode = None, + govTalkErrorType = None, + govTalkErrorMessage = None + ) + ) + ) - status(result) mustEqual OK - contentAsString(result) mustEqual view()(request, messages(application)).toString + private def agentClientData: AgentClientData = + AgentClientData( + uniqueId = "CIS-123", + taxOfficeNumber = "163", + taxOfficeReference = "AB0063", + schemeName = Some("ABC Ltd") + ) + + private def mockOrgAccess(): Unit = + when(mockMonthlyReturnService.getCisTaxpayer(any[HeaderCarrier])) + .thenReturn(Future.successful(taxpayer("CIS-123"))) + + private def mockOrgAccessDenied(): Unit = + when(mockMonthlyReturnService.getCisTaxpayer(any[HeaderCarrier])) + .thenReturn(Future.successful(taxpayer("DIFFERENT-CIS-ID"))) + + private def mockOrgAccessFails(): Unit = + when(mockMonthlyReturnService.getCisTaxpayer(any[HeaderCarrier])) + .thenReturn(Future.failed(new RuntimeException("taxpayer lookup failed"))) + + private def mockRetrieveMonthlyReturn( + payload: GetAllMonthlyReturnDetailsResponse = monthlyReturnPayload() + ): Unit = + when( + mockMonthlyReturnService.retrieveMonthlyReturnForEditDetails( + any[String], + any[Int], + any[Int] + )(any[HeaderCarrier]) + ).thenReturn(Future.successful(payload)) + + private def mockRetrieveMonthlyReturnFails(): Unit = + when( + mockMonthlyReturnService.retrieveMonthlyReturnForEditDetails( + any[String], + any[Int], + any[Int] + )(any[HeaderCarrier]) + ).thenReturn(Future.failed(new RuntimeException("retrieve failed"))) + + private def controller( + userAnswers: Option[UserAnswers] = Some(UserAnswers("test-user")), + isAgent: Boolean = false + ): ConfirmAmendmentController = { + val messagesApi = app.injector.instanceOf[MessagesApi] + val mcc = app.injector.instanceOf[MessagesControllerComponents] + val bodyParsers = app.injector.instanceOf[PlayBodyParsers] + + when(mockView()(any(), any())) + .thenReturn(play.twirl.api.HtmlFormat.empty) + + new ConfirmAmendmentController( + messagesApi = messagesApi, + identify = new FakeIdentifierAction( + isAgent = isAgent, + hasAgentRef = true, + hasEmployeeRef = true + )(bodyParsers), + getData = new FakeDataRetrievalAction(userAnswers), + sessionRepository = mockSessionRepository, + amendMonthlyReturnService = mockAmendMonthlyReturnService, + monthlyReturnService = mockMonthlyReturnService, + controllerComponents = mcc, + view = mockView + ) + } + + override def beforeEach(): Unit = { + super.beforeEach() + + reset( + mockSessionRepository, + mockAmendMonthlyReturnService, + mockMonthlyReturnService, + mockView + ) + + when(mockView()(any(), any())) + .thenReturn(play.twirl.api.HtmlFormat.empty) + } + + "ConfirmAmendmentController" - { + + "onPageLoad" - { + + "must return OK and store amendment details when organisation user is authorised" in { + mockOrgAccess() + mockRetrieveMonthlyReturn() + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.successful(true)) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe OK + + val uaCaptor = ArgumentCaptor.forClass(classOf[UserAnswers]) + verify(mockSessionRepository).set(uaCaptor.capture()) + + val savedUa = uaCaptor.getValue + + savedUa.get(CisIdPage) mustBe Some("CIS-123") + savedUa.get(AmendmentDetailsPage) mustBe Some(amendmentDetails) + + verify(mockMonthlyReturnService).getCisTaxpayer(any[HeaderCarrier]) + verify(mockMonthlyReturnService).retrieveMonthlyReturnForEditDetails( + any[String], + any[Int], + any[Int] + )(any[HeaderCarrier]) } - } - "must redirect to the same page when onSubmit is true" in { + "must store MonthlyNilReturn when nilReturnIndicator is Y" in { + mockOrgAccess() + + mockRetrieveMonthlyReturn( + monthlyReturnPayload( + nilReturnIndicator = Some("Y"), + acceptedTime = None + ) + ) + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.successful(true)) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe OK - val mockSessionRepository = mock[SessionRepository] + val uaCaptor = ArgumentCaptor.forClass(classOf[UserAnswers]) + verify(mockSessionRepository).set(uaCaptor.capture()) - when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val savedDetails = uaCaptor.getValue.get(AmendmentDetailsPage).value - val application = - applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides( - bind[SessionRepository].toInstance(mockSessionRepository) + savedDetails.returnType mustBe MonthlyNilReturn + savedDetails.acceptedTime mustBe None + } + + "must redirect to Journey Recovery when organisation user is not authorised" in { + mockOrgAccessDenied() + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + + verify(mockMonthlyReturnService).getCisTaxpayer(any[HeaderCarrier]) + verify(mockMonthlyReturnService, times(0)).retrieveMonthlyReturnForEditDetails( + any[String], + any[Int], + any[Int] + )(any[HeaderCarrier]) + } + + "must redirect to Journey Recovery when organisation validation fails" in { + mockOrgAccessFails() + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when retrieve amendment details fails" in { + mockOrgAccess() + mockRetrieveMonthlyReturnFails() + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when no matching monthly return is found" in { + mockOrgAccess() + + mockRetrieveMonthlyReturn( + monthlyReturnPayload( + taxYear = 2024, + taxMonth = 12 ) - .build() + ) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller().onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must allow agent when agent client exists and hasClient returns true" in { + when(mockMonthlyReturnService.getAgentClient(any[String])(any[HeaderCarrier], any[ExecutionContext])) + .thenReturn(Future.successful(Some(agentClientData))) + + when(mockMonthlyReturnService.hasClient(any[String], any[String])(any[HeaderCarrier])) + .thenReturn(Future.successful(true)) + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.successful(true)) + + mockRetrieveMonthlyReturn() + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller(isAgent = true).onPageLoad(queryParams)(request) + + status(result) mustBe OK + + verify(mockMonthlyReturnService).getAgentClient(any[String])(any[HeaderCarrier], any[ExecutionContext]) + verify(mockMonthlyReturnService).hasClient(any[String], any[String])(any[HeaderCarrier]) + } + + "must redirect to Journey Recovery when agent client data is missing" in { + when(mockMonthlyReturnService.getAgentClient(any[String])(any[HeaderCarrier], any[ExecutionContext])) + .thenReturn(Future.successful(None)) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller(isAgent = true).onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when agent no longer has access to client" in { + when(mockMonthlyReturnService.getAgentClient(any[String])(any[HeaderCarrier], any[ExecutionContext])) + .thenReturn(Future.successful(Some(agentClientData))) + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.successful(true)) + + when(mockMonthlyReturnService.hasClient(any[String], any[String])(any[HeaderCarrier])) + .thenReturn(Future.successful(false)) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller(isAgent = true).onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when agent validation fails" in { + when(mockMonthlyReturnService.getAgentClient(any[String])(any[HeaderCarrier], any[ExecutionContext])) + .thenReturn(Future.failed(new RuntimeException("agent lookup failed"))) + + val request = FakeRequest(GET, routes.ConfirmAmendmentController.onPageLoad(queryParams).url) + + val result = controller(isAgent = true).onPageLoad(queryParams)(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + } + + "onSubmit" - { + + "must create amended monthly return, store confirmation, and redirect when organisation user is authorised" in { + mockOrgAccess() + + val userAnswers = UserAnswers("test-user") + .set(AmendmentDetailsPage, amendmentDetails) + .get + + when( + mockAmendMonthlyReturnService.createAmendedMonthlyReturn( + any[CreateAmendedMonthlyReturnRequest] + )(any[HeaderCarrier]) + ).thenReturn(Future.successful(())) + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.successful(true)) + + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() + + val result = controller(Some(userAnswers)).onSubmit(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe + routes.ConfirmAmendmentController.onPageLoad(queryParams).url + + val requestCaptor = ArgumentCaptor.forClass(classOf[CreateAmendedMonthlyReturnRequest]) + + verify(mockAmendMonthlyReturnService).createAmendedMonthlyReturn(requestCaptor.capture())( + any[HeaderCarrier] + ) + + requestCaptor.getValue mustBe CreateAmendedMonthlyReturnRequest( + instanceId = "CIS-123", + taxYear = 2025, + taxMonth = 3, + version = 0 + ) + + val uaCaptor = ArgumentCaptor.forClass(classOf[UserAnswers]) + verify(mockSessionRepository).set(uaCaptor.capture()) + + uaCaptor.getValue.get(ConfirmAmendmentPage) mustBe Some(true) + } + + "must redirect to Journey Recovery when AmendmentDetails are missing" in { + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() + + val result = controller(Some(UserAnswers("test-user"))).onSubmit(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + + verifyNoInteractions(mockAmendMonthlyReturnService) + } + + "must redirect to Journey Recovery when organisation user is not authorised" in { + mockOrgAccessDenied() + + val userAnswers = UserAnswers("test-user") + .set(AmendmentDetailsPage, amendmentDetails) + .get + + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() + + val result = controller(Some(userAnswers)).onSubmit(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + + verifyNoInteractions(mockAmendMonthlyReturnService) + } + + "must redirect to Journey Recovery when organisation validation fails" in { + mockOrgAccessFails() + + val userAnswers = UserAnswers("test-user") + .set(AmendmentDetailsPage, amendmentDetails) + .get + + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() + + val result = controller(Some(userAnswers)).onSubmit(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when create amended monthly return fails" in { + mockOrgAccess() + + val userAnswers = UserAnswers("test-user") + .set(AmendmentDetailsPage, amendmentDetails) + .get + + when( + mockAmendMonthlyReturnService.createAmendedMonthlyReturn( + any[CreateAmendedMonthlyReturnRequest] + )(any[HeaderCarrier]) + ).thenReturn(Future.failed(new RuntimeException("create failed"))) + + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() + + val result = controller(Some(userAnswers)).onSubmit(request) + + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url + } + + "must redirect to Journey Recovery when session save fails" in { + mockOrgAccess() + + val userAnswers = UserAnswers("test-user") + .set(AmendmentDetailsPage, amendmentDetails) + .get + + when( + mockAmendMonthlyReturnService.createAmendedMonthlyReturn( + any[CreateAmendedMonthlyReturnRequest] + )(any[HeaderCarrier]) + ).thenReturn(Future.successful(())) + + when(mockSessionRepository.set(any[UserAnswers])) + .thenReturn(Future.failed(new RuntimeException("session failed"))) - running(application) { - val request = - FakeRequest(POST, confirmAmendmentRoute) - .withFormUrlEncodedBody() + val request = FakeRequest(POST, routes.ConfirmAmendmentController.onSubmit().url) + .withFormUrlEncodedBody() - val result = route(application, request).value + val result = controller(Some(userAnswers)).onSubmit(request) - status(result) mustEqual SEE_OTHER - redirectLocation(result).value mustEqual controllers.amend.routes.ConfirmAmendmentController.onPageLoad().url + status(result) mustBe SEE_OTHER + redirectLocation(result).value mustBe controllers.routes.JourneyRecoveryController.onPageLoad().url } } } diff --git a/test/controllers/amend/ConfirmCancelAmendmentYesNoControllerSpec.scala b/test/controllers/amend/ConfirmCancelAmendmentYesNoControllerSpec.scala index 1c750c94..dd806610 100644 --- a/test/controllers/amend/ConfirmCancelAmendmentYesNoControllerSpec.scala +++ b/test/controllers/amend/ConfirmCancelAmendmentYesNoControllerSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 controllers.amend import base.SpecBase diff --git a/test/controllers/monthlyreturns/CheckYourAnswersControllerSpec.scala b/test/controllers/monthlyreturns/CheckYourAnswersControllerSpec.scala index 2450f464..b22460b2 100644 --- a/test/controllers/monthlyreturns/CheckYourAnswersControllerSpec.scala +++ b/test/controllers/monthlyreturns/CheckYourAnswersControllerSpec.scala @@ -18,7 +18,6 @@ package controllers.monthlyreturns import base.SpecBase import models.monthlyreturns.Declaration.Confirmed -import models.monthlyreturns.UpdateMonthlyReturnRequest import models.{ReturnType, UserAnswers} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.* @@ -33,7 +32,6 @@ import viewmodels.checkAnswers.monthlyreturns.* import viewmodels.govuk.SummaryListFluency import views.html.monthlyreturns.CheckYourAnswersView import pages.submission.SubmissionJourneyCompletedPage -import uk.gov.hmrc.http.HeaderCarrier import java.time.LocalDate import scala.concurrent.Future diff --git a/test/forms/amend/ConfirmCancelAmendmentYesNoFormProviderSpec.scala b/test/forms/amend/ConfirmCancelAmendmentYesNoFormProviderSpec.scala index 4c8500d6..06311158 100644 --- a/test/forms/amend/ConfirmCancelAmendmentYesNoFormProviderSpec.scala +++ b/test/forms/amend/ConfirmCancelAmendmentYesNoFormProviderSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 forms.amend import forms.behaviours.BooleanFieldBehaviours diff --git a/test/models/amend/AmendmentDetailsSpec.scala b/test/models/amend/AmendmentDetailsSpec.scala new file mode 100644 index 00000000..62f57713 --- /dev/null +++ b/test/models/amend/AmendmentDetailsSpec.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.amend + +import base.SpecBase +import models.ReturnType +import play.api.libs.json.Json + +class AmendmentDetailsSpec extends SpecBase { + + "AmendmentDetails" - { + + "must round-trip JSON" in { + val model = AmendmentDetails( + instanceId = "1", + taxYear = 2025, + taxMonth = 1, + returnType = ReturnType.MonthlyNilReturn, + acceptedTime = Some("2025-01-01T12:00:00Z") + ) + + Json.fromJson[AmendmentDetails](Json.toJson(model)).get mustBe model + } + } +} diff --git a/test/models/amend/CreateAmendedMonthlyReturnRequestSpec.scala b/test/models/amend/CreateAmendedMonthlyReturnRequestSpec.scala new file mode 100644 index 00000000..891b7b07 --- /dev/null +++ b/test/models/amend/CreateAmendedMonthlyReturnRequestSpec.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models.amend + +import base.SpecBase +import play.api.libs.json.Json + +class CreateAmendedMonthlyReturnRequestSpec extends SpecBase { + + "CreateAmendedMonthlyReturnRequest" - { + + "must round-trip JSON" in { + val model = CreateAmendedMonthlyReturnRequest( + instanceId = "1234567890", + taxYear = 2025, + taxMonth = 1, + version = 0 + ) + + Json.fromJson[CreateAmendedMonthlyReturnRequest](Json.toJson(model)).get mustBe model + } + } +} diff --git a/test/pages/amend/AmendmentDetailsPageSpec.scala b/test/pages/amend/AmendmentDetailsPageSpec.scala new file mode 100644 index 00000000..d28b06ed --- /dev/null +++ b/test/pages/amend/AmendmentDetailsPageSpec.scala @@ -0,0 +1,31 @@ +/* + * 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 pages.amend + +import base.SpecBase +import play.api.libs.json.JsPath + +class AmendmentDetailsPageSpec extends SpecBase { + + "AmendmentDetailsPage" - { + + "must use amendmentDetails path" in { + AmendmentDetailsPage.path mustBe JsPath \ "amendmentDetails" + AmendmentDetailsPage.toString mustBe "amendmentDetails" + } + } +} diff --git a/test/pages/submission/SubmissionJourneyCompletedPageSpec.scala b/test/pages/submission/SubmissionJourneyCompletedPageSpec.scala index c5931e82..0e0a1313 100644 --- a/test/pages/submission/SubmissionJourneyCompletedPageSpec.scala +++ b/test/pages/submission/SubmissionJourneyCompletedPageSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 pages.submission import base.SpecBase diff --git a/test/services/AmendMonthlyReturnServiceSpec.scala b/test/services/AmendMonthlyReturnServiceSpec.scala new file mode 100644 index 00000000..e9390750 --- /dev/null +++ b/test/services/AmendMonthlyReturnServiceSpec.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2026 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services + +import base.SpecBase +import connectors.ConstructionIndustrySchemeConnector +import models.amend.CreateAmendedMonthlyReturnRequest +import org.mockito.Mockito.* +import org.mockito.ArgumentMatchers.any +import org.scalatestplus.mockito.MockitoSugar.mock +import uk.gov.hmrc.http.HeaderCarrier + +import scala.concurrent.Future + +class AmendMonthlyReturnServiceSpec extends SpecBase { + + "AmendMonthlyReturnService" - { + + "createAmendedMonthlyReturn should delegate to the CIS connector" in { + implicit val hc: HeaderCarrier = HeaderCarrier() + + val mockConnector = mock[ConstructionIndustrySchemeConnector] + + val request = CreateAmendedMonthlyReturnRequest( + instanceId = "1", + taxYear = 2025, + taxMonth = 1, + version = 0 + ) + + when( + mockConnector.createAmendedMonthlyReturn(any[CreateAmendedMonthlyReturnRequest]())( + any[HeaderCarrier]() + ) + ) thenReturn Future.successful(()) + + val service = new AmendMonthlyReturnService(mockConnector) + + service.createAmendedMonthlyReturn(request).futureValue mustBe ((): Unit) + + verify(mockConnector).createAmendedMonthlyReturn(request)(hc) + } + } +} diff --git a/test/services/MonthlyReturnServiceSpec.scala b/test/services/MonthlyReturnServiceSpec.scala index ae33e0f1..da748a12 100644 --- a/test/services/MonthlyReturnServiceSpec.scala +++ b/test/services/MonthlyReturnServiceSpec.scala @@ -92,6 +92,43 @@ class MonthlyReturnServiceSpec extends SpecBase { enrolledSig = None ) + "getCisTaxpayer" - { + + "delegate to connector and return the taxpayer" in { + val (service, connector, sessionRepo) = newService() + + val taxpayer = createTaxpayer() + + when(connector.getCisTaxpayer()(any[HeaderCarrier])) + .thenReturn(Future.successful(taxpayer)) + + val result = service.getCisTaxpayer.futureValue + + result mustBe taxpayer + + verify(connector).getCisTaxpayer()(any[HeaderCarrier]) + verifyNoInteractions(sessionRepo) + verifyNoMoreInteractions(connector) + } + + "propagate failures from the connector" in { + val (service, connector, sessionRepo) = newService() + + when(connector.getCisTaxpayer()(any[HeaderCarrier])) + .thenReturn(Future.failed(new RuntimeException("upstream failed"))) + + val ex = intercept[RuntimeException] { + service.getCisTaxpayer.futureValue + } + + ex.getMessage must include("upstream failed") + + verify(connector).getCisTaxpayer()(any[HeaderCarrier]) + verifyNoInteractions(sessionRepo) + verifyNoMoreInteractions(connector) + } + } + "resolveAndStoreCisId" - { "return existing cisId from UserAnswers without calling BE" in { diff --git a/test/views/amend/ConfirmCancelAmendmentYesNoViewSpec.scala b/test/views/amend/ConfirmCancelAmendmentYesNoViewSpec.scala index 876b167e..a797813a 100644 --- a/test/views/amend/ConfirmCancelAmendmentYesNoViewSpec.scala +++ b/test/views/amend/ConfirmCancelAmendmentYesNoViewSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 views.amend import base.SpecBase