Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ class FormpProxyConnector @Inject() (
)
.execute[CreateNilMonthlyReturnResponse]

def updateNilMonthlyReturn(
req: NilMonthlyReturnRequest
def updateMonthlyReturn(
req: UpdateMonthlyReturnRequest
)(implicit hc: HeaderCarrier): Future[Unit] =
http
.post(url"$base/cis/monthly-return/nil/update")
.post(url"$base/cis/monthly-return/update")
.withBody(Json.toJson(req))
.execute[HttpResponse]
.flatMap { resp =>
if (resp.status / 100 == 2) Future.unit
if (resp.status == 204) Future.unit
else Future.failed(UpstreamErrorResponse(resp.body, resp.status, resp.status))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,23 @@ class MonthlyReturnsController @Inject() (
)
}

def updateNil(): Action[JsValue] = authorise.async(parse.json) { implicit request =>
def updateMonthlyReturn(): Action[JsValue] = authorise.async(parse.json) { implicit request =>
given HeaderCarrier = HeaderCarrierConverter.fromRequest(request)
request.body
.validate[NilMonthlyReturnRequest]
.validate[UpdateMonthlyReturnRequest]
.fold(
_ => Future.successful(BadRequest(Json.obj("message" -> "Invalid payload"))),
payload =>
service
.updateNilMonthlyReturn(payload)
.updateMonthlyReturn(payload)
.map(_ => NoContent)
.recover { case u: UpstreamErrorResponse => Status(u.statusCode)(Json.obj("message" -> u.message)) }
.recover {
case u: UpstreamErrorResponse =>
Status(u.statusCode)(Json.obj("message" -> u.message))
case NonFatal(t) =>
logger.error("[updateMonthlyReturn] failed", t)
InternalServerError(Json.obj("message" -> "Unexpected error"))
}
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.constructionindustryscheme.models.requests

import play.api.libs.json.{Json, OFormat}

case class UpdateMonthlyReturnRequest(
instanceId: String,
taxYear: Int,
taxMonth: Int,
amendment: String,
decEmpStatusConsidered: Option[String] = None,
decAllSubsVerified: Option[String] = None,
decNoMoreSubPayments: Option[String] = None,
decNilReturnNoPayments: Option[String] = None,
decInformationCorrect: Option[String] = None,
nilReturnIndicator: String,
status: String,
version: Option[Long] = None
)

object UpdateMonthlyReturnRequest {
given format: OFormat[UpdateMonthlyReturnRequest] = Json.format[UpdateMonthlyReturnRequest]
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ class MonthlyReturnService @Inject() (
}
}

def updateNilMonthlyReturn(
req: NilMonthlyReturnRequest
def updateMonthlyReturn(
req: UpdateMonthlyReturnRequest
)(implicit hc: HeaderCarrier): Future[Unit] =
formp.updateNilMonthlyReturn(req)
formp.updateMonthlyReturn(req)

def createMonthlyReturn(req: MonthlyReturnRequest)(implicit hc: HeaderCarrier): Future[Unit] =
formp.createMonthlyReturn(req)
Expand Down
2 changes: 1 addition & 1 deletion conf/app.routes
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ GET /taxpayer uk.gov.
GET /monthly-returns uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.getAllMonthlyReturns(cisId: Option[String])
GET /monthly-returns/unsubmitted/:instanceId uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.getUnsubmittedMonthlyReturns(instanceId: String)
POST /monthly-returns/nil/create uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.createNil()
POST /monthly-returns/nil/update uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.updateNil()
POST /monthly-returns/update uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.updateMonthlyReturn()
POST /monthly-returns/standard/create uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.createMonthlyReturn
POST /monthly-returns-edit uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.getMonthlyReturnForEdit
POST /monthly-return-item/sync uk.gov.hmrc.constructionindustryscheme.controllers.MonthlyReturnsController.syncSelectedSubcontractors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,37 +127,51 @@ class FormpProxyConnectorIntegrationSpec
}
}

"FormpProxyConnector updateNilMonthlyReturn" should {
"FormpProxyConnector updateMonthlyReturn" should {

"POST request and return Unit on 2xx" in {
val req = NilMonthlyReturnRequest(
val req = UpdateMonthlyReturnRequest(
instanceId = instanceId,
taxYear = 2025,
taxMonth = 2,
decInformationCorrect = "Y",
decNilReturnNoPayments = "Y"
taxMonth = 1,
amendment = "N",
decInformationCorrect = Some("Y"),
decNilReturnNoPayments = Some("Y"),
nilReturnIndicator = "Y",
status = "STARTED",
version = Some(1L)
)

stubFor(
post(urlPathEqualTo("/formp-proxy/cis/monthly-return/nil/update"))
post(urlPathEqualTo("/formp-proxy/cis/monthly-return/update"))
.withHeader("Content-Type", equalTo("application/json"))
.withRequestBody(equalToJson(Json.toJson(req).as[JsObject].toString(), true, true))
.willReturn(aResponse().withStatus(204))
)

connector.updateNilMonthlyReturn(req).futureValue mustBe ((): Unit)
connector.updateMonthlyReturn(req).futureValue mustBe ((): Unit)
}

"fail with UpstreamErrorResponse when upstream returns non-2xx" in {
val req = NilMonthlyReturnRequest(instanceId, 2025, 2, "Y", "Y")
val req = UpdateMonthlyReturnRequest(
instanceId = instanceId,
taxYear = 2025,
taxMonth = 1,
amendment = "N",
decInformationCorrect = Some("Y"),
decNilReturnNoPayments = Some("Y"),
nilReturnIndicator = "Y",
status = "STARTED",
version = Some(1L)
)

stubFor(
post(urlPathEqualTo("/formp-proxy/cis/monthly-return/nil/update"))
post(urlPathEqualTo("/formp-proxy/cis/monthly-return/update"))
.withRequestBody(equalToJson(Json.toJson(req).as[JsObject].toString(), true, true))
.willReturn(aResponse().withStatus(500).withBody("""{"message":"boom"}"""))
)

val ex = connector.updateNilMonthlyReturn(req).failed.futureValue
val ex = connector.updateMonthlyReturn(req).failed.futureValue
ex mustBe a[UpstreamErrorResponse]
ex.asInstanceOf[UpstreamErrorResponse].statusCode mustBe 500
}
Expand Down
94 changes: 94 additions & 0 deletions test/controllers/MonthlyReturnsControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,100 @@ class MonthlyReturnsControllerSpec extends SpecBase {
}
}

"POST /cis/monthly-return/update (updateMonthlyReturn)" - {

"return 204 NoContent when service succeeds" in new SetupAuthOnly {
val payload = UpdateMonthlyReturnRequest(
instanceId = "abc-123",
taxYear = 2025,
taxMonth = 1,
amendment = "N",
decInformationCorrect = Some("Y"),
nilReturnIndicator = "N",
status = "STARTED",
version = None
)

when(mockMonthlyReturnService.updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
.thenReturn(Future.successful(()))

val request =
FakeRequest(POST, "/")
.withHeaders("Content-Type" -> "application/json")
.withBody(Json.toJson(payload))

val result = call(controller.updateMonthlyReturn(), request)

status(result) mustBe NO_CONTENT
verify(mockMonthlyReturnService).updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier])
}

"return 400 BadRequest when payload is invalid" in new SetupAuthOnly {
val request =
FakeRequest(POST, "/")
.withHeaders("Content-Type" -> "application/json")
.withBody(Json.obj("bad" -> "json"))

val result = call(controller.updateMonthlyReturn(), request)

status(result) mustBe BAD_REQUEST
contentAsJson(result) mustBe Json.obj("message" -> "Invalid payload")
verifyNoInteractions(mockMonthlyReturnService)
}

"return upstream status and message when service fails with UpstreamErrorResponse" in new SetupAuthOnly {
val payload = UpdateMonthlyReturnRequest(
instanceId = "abc-123",
taxYear = 2025,
taxMonth = 1,
amendment = "N",
decInformationCorrect = Some("Y"),
nilReturnIndicator = "N",
status = "STARTED",
version = None
)

when(mockMonthlyReturnService.updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
.thenReturn(Future.failed(UpstreamErrorResponse("formp proxy failure", BAD_GATEWAY)))

val request =
FakeRequest(POST, "/")
.withHeaders("Content-Type" -> "application/json")
.withBody(Json.toJson(payload))

val result = call(controller.updateMonthlyReturn(), request)

status(result) mustBe BAD_GATEWAY
contentAsJson(result) mustBe Json.obj("message" -> "formp proxy failure")
}

"return 500 InternalServerError when service fails with NonFatal" in new SetupAuthOnly {
val payload = UpdateMonthlyReturnRequest(
instanceId = "abc-123",
taxYear = 2025,
taxMonth = 1,
amendment = "N",
decInformationCorrect = Some("Y"),
nilReturnIndicator = "N",
status = "STARTED",
version = None
)

when(mockMonthlyReturnService.updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
.thenReturn(Future.failed(new RuntimeException("boom")))

val request =
FakeRequest(POST, "/")
.withHeaders("Content-Type" -> "application/json")
.withBody(Json.toJson(payload))

val result = call(controller.updateMonthlyReturn(), request)

status(result) mustBe INTERNAL_SERVER_ERROR
contentAsJson(result) mustBe Json.obj("message" -> "Unexpected error")
}
}

}

private lazy val sampleWrapper: UserMonthlyReturns = UserMonthlyReturns(
Expand Down
42 changes: 42 additions & 0 deletions test/models/requests/UpdateMonthlyReturnRequestSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.requests

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import play.api.libs.json.{JsSuccess, Json}
import uk.gov.hmrc.constructionindustryscheme.models.requests.UpdateMonthlyReturnRequest

class UpdateMonthlyReturnRequestSpec extends AnyWordSpec with Matchers {

"UpdateMonthlyReturnRequest.format" should {

"serialize and deserialize (round-trip) model" in {
val model = UpdateMonthlyReturnRequest(
instanceId = "instance-123",
taxYear = 2024,
taxMonth = 1,
amendment = "false",
nilReturnIndicator = "false",
status = "Draft"
)

val json = Json.toJson(model)
json.validate[UpdateMonthlyReturnRequest] shouldBe JsSuccess(model)
}
}
}
34 changes: 21 additions & 13 deletions test/services/MonthlyReturnServiceSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,48 +175,56 @@ class MonthlyReturnServiceSpec extends SpecBase {
}
}

"updateNilMonthlyReturn" - {
"updateMonthlyReturn" - {

"delegates to formp connector and returns Unit" in {
val s = setup; import s._

val payload = NilMonthlyReturnRequest(
val payload = UpdateMonthlyReturnRequest(
instanceId = cisInstanceId,
taxYear = 2025,
taxMonth = 1,
decInformationCorrect = "Y",
decNilReturnNoPayments = "Y"
amendment = "N",
decInformationCorrect = Some("Y"),
decNilReturnNoPayments = Some("Y"),
nilReturnIndicator = "Y",
status = "STARTED",
version = Some(1L)
)

when(formpProxy.updateNilMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
when(formpProxy.updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
.thenReturn(Future.successful(()))

val result = service.updateNilMonthlyReturn(payload).futureValue
val result = service.updateMonthlyReturn(payload).futureValue
result mustBe ()

verify(formpProxy).updateNilMonthlyReturn(eqTo(payload))(any[HeaderCarrier])
verify(formpProxy).updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier])
verifyNoInteractions(datacacheProxy)
}

"propagates failure from formp connector" in {
val s = setup; import s._

val payload = NilMonthlyReturnRequest(
val payload = UpdateMonthlyReturnRequest(
instanceId = cisInstanceId,
taxYear = 2025,
taxMonth = 1,
decInformationCorrect = "Y",
decNilReturnNoPayments = "Y"
amendment = "N",
decInformationCorrect = Some("Y"),
decNilReturnNoPayments = Some("Y"),
nilReturnIndicator = "Y",
status = "STARTED",
version = Some(1L)
)
val boom = UpstreamErrorResponse("formp proxy failure", 500)

when(formpProxy.updateNilMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
when(formpProxy.updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier]))
.thenReturn(Future.failed(boom))

val ex = service.updateNilMonthlyReturn(payload).failed.futureValue
val ex = service.updateMonthlyReturn(payload).failed.futureValue
ex mustBe boom

verify(formpProxy).updateNilMonthlyReturn(eqTo(payload))(any[HeaderCarrier])
verify(formpProxy).updateMonthlyReturn(eqTo(payload))(any[HeaderCarrier])
verifyNoInteractions(datacacheProxy)
}
}
Expand Down