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 @@ -21,6 +21,7 @@ import com.google.inject.{AbstractModule, Provides}
import play.api.libs.concurrent.PekkoGuiceSupport
import play.api.{Configuration, Environment}
import uk.gov.hmrc.auth.core.AuthConnector
import uk.gov.hmrc.automatedexportsystem.services.{SubmissionService, SubmissionServiceImpl}
import uk.gov.hmrc.play.bootstrap.auth.DefaultAuthConnector
import uk.gov.hmrc.play.bootstrap.config.ServicesConfig

Expand All @@ -33,8 +34,10 @@ class GuiceModule(
@unused configuration: Configuration
) extends AbstractModule
with PekkoGuiceSupport {
override def configure(): Unit =
override def configure(): Unit = {
bind(classOf[AuthConnector]).to(classOf[DefaultAuthConnector]).asEagerSingleton()
bind(classOf[SubmissionService]).to(classOf[SubmissionServiceImpl]).asEagerSingleton()
}

@Provides
@Named("eisBearerToken")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@

package uk.gov.hmrc.automatedexportsystem.controllers

import play.api.http.ContentTypes
import play.api.mvc.{Action, AnyContent, ControllerComponents, EssentialAction}
import uk.gov.hmrc.automatedexportsystem.controllers.actions.{AesAuthAction, AesAuthRequestRefiner, XmlPayloadActionRefiner, XmlValidationActionRefiner}
import uk.gov.hmrc.automatedexportsystem.controllers.parsers.XmlBodyParsers
import uk.gov.hmrc.automatedexportsystem.errors.ResponseCode
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.ExportOperationType.Awaiting
import uk.gov.hmrc.automatedexportsystem.models.responses.AesErrorResponse.toErrorResponse
import uk.gov.hmrc.automatedexportsystem.parsers.SubmissionRequestParser
import uk.gov.hmrc.automatedexportsystem.services.{AesIE507XmlValidationService, SubmissionService}
import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController

import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext
import scala.concurrent.{ExecutionContext, Future}
import scala.xml.NodeSeq

@Singleton
Expand All @@ -41,13 +43,36 @@ class SubmissionController @Inject() (
) extends BackendController(cc):
given ec: ExecutionContext = cc.executionContext

private lazy val messageXmlValidatedAction: Action[NodeSeq] =
Action(xmlBodyParsers.utf8)
private lazy val messageXmlValidatedAction: Action[NodeSeq] = {
val composed = Action(xmlBodyParsers.utf8)
.andThen(aesAuthRequestRefiner)
.andThen(xmlPayloadActionRefiner)
.andThen(xmlValidationActionRefiner) { _ =>
Status(ResponseCode.Accepted.status)
.andThen(xmlValidationActionRefiner)

composed.async { request =>
SubmissionRequestParser.fromXml(request.validatedXml) match {
case Left(parseErr) =>
val errorXml =
<Error>
<Code>INVALID_XML</Code>
<Message>
{parseErr}
</Message>
</Error>

Future.successful(BadRequest(errorXml).as(ContentTypes.XML))

case Right(submissionRequest) =>
submissionService.submitMessage(submissionRequest, Awaiting, request.eori).value.map {
case Right(_) =>
Accepted

case Left(err) =>
InternalServerError(err.toString)
}
}
}
}

def message: EssentialAction =
aesAuthEssentialAction(messageXmlValidatedAction)
Expand All @@ -56,10 +81,8 @@ class SubmissionController @Inject() (
Action
.andThen(aesAuthRequestRefiner)
.async(aesAuthRequest =>
val eoriNumber: EoriNumber = EoriNumber(aesAuthRequest.eori)

submissionService
.getSubmissions(eoriNumber)
.getSubmissions(aesAuthRequest.eori)
.fold(
error => error.toErrorResponse.toResult.withHeaders(),
submissionSummaryList => Status(ResponseCode.Ok.status)(submissionSummaryList.toXml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package uk.gov.hmrc.automatedexportsystem.controllers.actions
import play.api.Logging
import play.api.mvc.*
import uk.gov.hmrc.automatedexportsystem.controllers.actions.request.{AesAuthAttr, AesAuthRequest}
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
Expand All @@ -33,7 +34,7 @@ class AesAuthRequestRefiner @Inject() ()(using protected val ec: ExecutionContex
Future.successful {
request.attrs.get(AesAuthAttr.Eori) match {
case Some(eori) =>
Right(AesAuthRequest(eori, request))
Right(AesAuthRequest(EoriNumber(eori), request))
case None =>
logger.warn(s"Missing authenticated EORI in request attrs [path=${request.path}]")
Left(Unauthorized)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
package uk.gov.hmrc.automatedexportsystem.controllers.actions.request

import play.api.mvc.{Request, WrappedRequest}
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber

case class AesAuthRequest[A](eori: String, request: Request[A]) extends WrappedRequest[A](request)
case class AesAuthRequest[A](eori: EoriNumber, request: Request[A]) extends WrappedRequest[A](request)
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
package uk.gov.hmrc.automatedexportsystem.controllers.actions.request

import play.api.mvc.{Request, WrappedRequest}
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber

import scala.annotation.unused
import scala.xml.NodeSeq

final case class ValidatedXmlRequest[T](@unused validatedXml: NodeSeq, request: Request[T], eori: String) extends WrappedRequest(request)
final case class ValidatedXmlRequest[T](@unused validatedXml: NodeSeq, request: Request[T], eori: EoriNumber) extends WrappedRequest(request)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
package uk.gov.hmrc.automatedexportsystem.controllers.actions.request

import play.api.mvc.{Request, WrappedRequest}
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber

import scala.xml.NodeSeq

final case class XmlPayloadRequest[T](xml: NodeSeq, request: Request[T], eori: String) extends WrappedRequest(request)
final case class XmlPayloadRequest[T](xml: NodeSeq, request: Request[T], eori: EoriNumber) extends WrappedRequest(request)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package uk.gov.hmrc.automatedexportsystem.models.aesIE507

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

final case class ContainerIdentificationNumber(value: Int) extends AnyVal
final case class ContainerIdentificationNumber(value: String) extends AnyVal

object ContainerIdentificationNumber:
given mongoFormat: Format[ContainerIdentificationNumber] = Json.valueFormat[ContainerIdentificationNumber]
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum ExportOperationType(val status: Int):
case Standard extends ExportOperationType(1)
case Amend extends ExportOperationType(2)
case Cancel extends ExportOperationType(3)
case Awaiting extends ExportOperationType(4)

object ExportOperationType:
given mongoFormat: Format[ExportOperationType] = Format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import play.api.libs.json.{Format, Json}

final case class GoodsItem(
declarationGoodsItemNumber: Option[DeclarationGoodsItemNumber],
referenceNumberUcr: Option[ReferenceNumberUcr],
commodity: Commodity,
packaging: Option[NonEmptyList[Packaging]]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import uk.gov.hmrc.automatedexportsystem.models.aesIE507.*
import java.time.Instant

case class MongoAesIE507Message(
_id: SubmissionId,
submissionId: SubmissionId,
eoriNumber: EoriNumber,
createdAt: Instant,
updatedAt: Instant,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.automatedexportsystem.models.request

import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{CustomsOfficeOfExitActual, EoriNumber, ExportOperation, GoodsShipment, SubmissionId}
import uk.gov.hmrc.automatedexportsystem.models.mongo.write.MongoAesIE507Message
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.ExportOperationType
import java.time.Instant
import java.util.UUID

case class SubmissionRequest(
submissionId: Option[SubmissionId],
exportOperation: ExportOperation,
customsOfficeOfExitActual: CustomsOfficeOfExitActual,
goodsShipment: Option[GoodsShipment]
) {
def toMongoMessage(
operationType: ExportOperationType,
eoriNumber: EoriNumber
): MongoAesIE507Message = {
val now = Instant.now()
MongoAesIE507Message(
submissionId = submissionId.getOrElse(SubmissionId(UUID.randomUUID())),
eoriNumber = eoriNumber,
createdAt = now,
updatedAt = now,
exportOperation = exportOperation.copy(exportOperationType = operationType),
customsOfficeOfExitActual = customsOfficeOfExitActual,
goodsShipment = goodsShipment
)
}
}
sealed trait SubmissionResult

object SubmissionResult {
case object Created extends SubmissionResult
case object Updated extends SubmissionResult
case object Awaiting extends SubmissionResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

package uk.gov.hmrc.automatedexportsystem.models.responses

import play.api.libs.json.*
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.*
import uk.gov.hmrc.automatedexportsystem.models.mongo.write.MongoAesIE507Message

import java.time.format.DateTimeFormatter
import java.time.{LocalDateTime, ZoneOffset}
import java.time.{Instant, LocalDateTime, ZoneOffset}
import scala.xml.{Elem, NodeSeq}

final case class SubmissionSummaryList(submissions: List[SubmissionSummary]):
Expand Down Expand Up @@ -53,12 +54,47 @@ final case class SubmissionSummary(
</Submission>

object SubmissionSummary:
import LocalDateTimeFormat.given
given OFormat[SubmissionSummary] = Json.format[SubmissionSummary]
// implicit val format: OFormat[SubmissionSummary] = Json.format[SubmissionSummary]
def fromMongoAesIE507Message(message: MongoAesIE507Message): SubmissionSummary =
SubmissionSummary(
submissionId = message._id,
submissionId = message.submissionId,
mrn = message.exportOperation.mrn,
ducr = message.goodsShipment.map(_.consignment.referenceNumberUCR),
officeOfExitCode = message.customsOfficeOfExitActual.referenceNumber,
updatedAt = LocalDateTime.ofInstant(message.updatedAt, ZoneOffset.UTC),
status = message.exportOperation.exportOperationType
)

object LocalDateTimeFormat:
private val iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME

given Format[LocalDateTime] = new Format[LocalDateTime]:
override def reads(json: JsValue): JsResult[LocalDateTime] =
json match
case JsString(value) =>
JsSuccess(LocalDateTime.parse(value, iso))

case JsObject(fields) if fields.contains("$date") =>
fields("$date") match
case JsString(isoInstant) =>
JsSuccess(LocalDateTime.ofInstant(Instant.parse(isoInstant), ZoneOffset.UTC))

case JsObject(longObj) if longObj.contains("$numberLong") =>
longObj("$numberLong") match
case JsString(epochMs) =>
JsSuccess(LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMs.toLong), ZoneOffset.UTC))
case other =>
JsError(s"Unsupported $$numberLong payload for LocalDateTime: $other")
case other =>
JsError(s"Unsupported $$date payload for LocalDateTime: $other")

case JsNumber(epochMs) =>
JsSuccess(LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMs.toLong), ZoneOffset.UTC))

case other =>
JsError(s"Cannot parse LocalDateTime from: $other")

override def writes(value: LocalDateTime): JsValue =
JsString(value.format(iso))
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.automatedexportsystem.parsers.AESIE507

import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{ActiveBorderTransportMeans, IdentificationNumber, Nationality, TypeOfIdentification}
import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.textOptChild

import scala.xml.Node

object ActiveBorderTransportMeansParser {

def parseActiveBorderTransportMeans(n: Node): Either[String, Option[ActiveBorderTransportMeans]] = {
val maybeNode = (n \ Tags.ActiveBorderTransportMeans).headOption
Right(
maybeNode.map { m =>
ActiveBorderTransportMeans(
typeOfIdentification = textOptChild(m, Tags.TypeOfIdentification).map(TypeOfIdentification.apply),
identificationNumber = textOptChild(m, Tags.IdentificationNumber).map(IdentificationNumber.apply),
nationality = textOptChild(m, Tags.Nationality).map(Nationality.apply)
)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.automatedexportsystem.parsers.AESIE507

import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{Consignment, ModeOfTransportAtBorder, ParentUcrId, ReferenceNumberUcr}
import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.*
import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.{ActiveBorderTransportMeansParser, LocationOfGoodsParser, TransportEquipmentParser}

import scala.xml.Node

object ConsignmentParser {
def parseConsignment(n: Node): Either[String, Consignment] =
for {
referenceNumberUcr <- req(textOptChild(n, Tags.ReferenceNumberUCR), Tags.ReferenceNumberUCR).map(ReferenceNumberUcr.apply)

modeOfTransportAtBorder <- parseOptionalInt(textOptChild(n, Tags.ModeOfTransportAtBorder), Tags.ModeOfTransportAtBorder)
.map(_.map(ModeOfTransportAtBorder.apply))

locationNode <- req((n \ Tags.LocationOfGoods).headOption, Tags.LocationOfGoods)
locationOfGoods <- LocationOfGoodsParser.parseLocationOfGoods(locationNode)

transportEquipment <- TransportEquipmentParser.parseTransportEquipment(n)
seals <- SealsParser.parseSeals(n)
goodsReferences <- GoodsReferenceParser.parseGoodsReferences(n)
borderMeans <- ActiveBorderTransportMeansParser.parseActiveBorderTransportMeans(n)
transportDocs <- TransportDocumentParser.parseTransportDocuments(n)
} yield Consignment(
modeOfTransportAtBorder = modeOfTransportAtBorder,
referenceNumberUCR = referenceNumberUcr,
parentUcrId = textOptChild(n, Tags.ParentUCRID).map(ParentUcrId.apply),
transportEquipment = transportEquipment,
seal = seals,
goodsReference = goodsReferences,
locationOfGoods = locationOfGoods,
activeBorderTransportMeans = borderMeans,
transportDocument = transportDocs
)
}
Loading