diff --git a/app/uk/gov/hmrc/automatedexportsystem/config/GuiceModule.scala b/app/uk/gov/hmrc/automatedexportsystem/config/GuiceModule.scala
index ac28ec0..6c00e2d 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/config/GuiceModule.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/config/GuiceModule.scala
@@ -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
@@ -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")
diff --git a/app/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionController.scala b/app/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionController.scala
index aa5bdc3..b24dee0 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionController.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionController.scala
@@ -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
@@ -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 =
+
+ INVALID_XML
+
+ {parseErr}
+
+
+
+ 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)
@@ -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)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefiner.scala b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefiner.scala
index 5c581a6..218ae93 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefiner.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefiner.scala
@@ -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}
@@ -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)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/AesAuthRequest.scala b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/AesAuthRequest.scala
index c8289b0..3546ddc 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/AesAuthRequest.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/AesAuthRequest.scala
@@ -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)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/ValidatedXmlRequest.scala b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/ValidatedXmlRequest.scala
index 093351e..6fe9315 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/ValidatedXmlRequest.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/ValidatedXmlRequest.scala
@@ -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)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/XmlPayloadRequest.scala b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/XmlPayloadRequest.scala
index 8735e74..6223440 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/XmlPayloadRequest.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/controllers/actions/request/XmlPayloadRequest.scala
@@ -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)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ContainerIdentificationNumber.scala b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ContainerIdentificationNumber.scala
index ba256b3..645766c 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ContainerIdentificationNumber.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ContainerIdentificationNumber.scala
@@ -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]
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ExportOperationType.scala b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ExportOperationType.scala
index dffe94f..d3e7982 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ExportOperationType.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/ExportOperationType.scala
@@ -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(
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/GoodsItem.scala b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/GoodsItem.scala
index 76db690..a4a7d34 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/GoodsItem.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/aesIE507/GoodsItem.scala
@@ -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]]
)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507Message.scala b/app/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507Message.scala
index f22af3d..efc409c 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507Message.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507Message.scala
@@ -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,
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/request/SubmissionRequest.scala b/app/uk/gov/hmrc/automatedexportsystem/models/request/SubmissionRequest.scala
new file mode 100644
index 0000000..88f0107
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/request/SubmissionRequest.scala
@@ -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
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/models/responses/SubmissionSummaryList.scala b/app/uk/gov/hmrc/automatedexportsystem/models/responses/SubmissionSummaryList.scala
index 7ddfef1..7eb07ef 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/models/responses/SubmissionSummaryList.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/models/responses/SubmissionSummaryList.scala
@@ -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]):
@@ -53,12 +54,47 @@ final case class SubmissionSummary(
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))
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParser.scala
new file mode 100644
index 0000000..a8f99a8
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParser.scala
@@ -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)
+ )
+ }
+ )
+ }
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParser.scala
new file mode 100644
index 0000000..2719cf2
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParser.scala
@@ -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
+ )
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParser.scala
new file mode 100644
index 0000000..fcef936
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParser.scala
@@ -0,0 +1,54 @@
+/*
+ * 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.{DiscrepanciesExist, ExportOperation, ExportOperationType, Mrn, SplitIndicator}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.{parseBoolean, req, textOptChild}
+
+import scala.xml.Node
+
+object ExportOperationParser {
+ private def parseExportOperationType(raw: String): Either[String, ExportOperationType] =
+ raw.toIntOption
+ .toRight(s"Invalid ${Tags.ExportOperationType}: [$raw]")
+ .flatMap { status =>
+ ExportOperationType.values
+ .find(_.status == status)
+ .toRight(s"Invalid ${Tags.ExportOperationType}: [$status]")
+ }
+
+ def parseExportOperation(n: Node): Either[String, ExportOperation] =
+ for {
+ exportOperationType <- req(textOptChild(n, Tags.ExportOperationType), Tags.ExportOperationType)
+ .flatMap(parseExportOperationType)
+
+ mrn <- req(textOptChild(n, Tags.MRN), Tags.MRN).map(Mrn.apply)
+
+ discrepancies <- req(textOptChild(n, Tags.DiscrepanciesExist), Tags.DiscrepanciesExist)
+ .flatMap(parseBoolean)
+ .map(DiscrepanciesExist.apply)
+
+ split <- req(textOptChild(n, Tags.SplitIndicator), Tags.SplitIndicator)
+ .flatMap(parseBoolean)
+ .map(SplitIndicator.apply)
+ } yield ExportOperation(
+ exportOperationType = exportOperationType,
+ mrn = mrn,
+ discrepanciesExist = discrepancies,
+ splitIndicator = split
+ )
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemsParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemsParser.scala
new file mode 100644
index 0000000..7774d04
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemsParser.scala
@@ -0,0 +1,46 @@
+/*
+ * 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 cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{Commodity, DeclarationGoodsItemNumber, GoodsItem, GrossMass, NetMass, ReferenceNumberUcr}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.*
+
+import scala.xml.Node
+
+object GoodsItemsParser {
+ def parseGoodsItems(n: Node): Either[String, Option[NonEmptyList[GoodsItem]]] = {
+ val nodes = (n \\ Tags.GoodsItem).toList
+ sequence(nodes.map(parseGoodsItemNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parseGoodsItemNode(n: Node): Either[String, GoodsItem] =
+ for {
+ goodsMeasure <- req((n \ Tags.Commodity \ Tags.GoodsMeasure).headOption, Tags.GoodsMeasure)
+ gross <- req(textOptChild(goodsMeasure, Tags.GrossMass), Tags.GrossMass).flatMap(parseBigDecimal).map(GrossMass.apply)
+ net <- req(textOptChild(goodsMeasure, Tags.NetMass), Tags.NetMass).flatMap(parseBigDecimal).map(NetMass.apply)
+ packaging <- PackagingParser.parsePackaging(n)
+ declarationNo <- parseOptionalInt(textOptChild(n, Tags.DeclarationGoodsItemNumber), Tags.DeclarationGoodsItemNumber)
+ .map(_.map(DeclarationGoodsItemNumber.apply))
+ } yield GoodsItem(
+ declarationGoodsItemNumber = declarationNo,
+ referenceNumberUcr = textOptChild(n, Tags.ReferenceNumberUCR).map(_.trim).filter(_.nonEmpty).map(ReferenceNumberUcr.apply),
+ commodity = Commodity(grossMass = gross, netMass = net),
+ packaging = packaging
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsReferenceParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsReferenceParser.scala
new file mode 100644
index 0000000..e2adca7
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsReferenceParser.scala
@@ -0,0 +1,41 @@
+/*
+ * 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 cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{DeclarationGoodsItemNumber, GoodsReference, SequenceNumber}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.*
+
+import scala.xml.Node
+
+object GoodsReferenceParser {
+ def parseGoodsReferences(n: Node): Either[String, Option[NonEmptyList[GoodsReference]]] = {
+ val nodes = (n \ Tags.GoodsReference).toList
+ sequence(nodes.map(parseGoodsReferenceNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parseGoodsReferenceNode(n: Node): Either[String, GoodsReference] =
+ for {
+ sequenceNumber <- parseOptionalInt(textOptChild(n, Tags.SequenceNumber), Tags.SequenceNumber).map(_.map(SequenceNumber.apply))
+ declarationNo <- parseOptionalInt(textOptChild(n, Tags.DeclarationGoodsItemNumber), Tags.DeclarationGoodsItemNumber)
+ .map(_.map(DeclarationGoodsItemNumber.apply))
+ } yield GoodsReference(
+ sequenceNumber = sequenceNumber,
+ declarationGoodsItemNumber = declarationNo
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParser.scala
new file mode 100644
index 0000000..1b93384
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParser.scala
@@ -0,0 +1,40 @@
+/*
+ * 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.GoodsShipment
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.req
+import scala.xml.Node
+
+object GoodsShipmentParser {
+ def parseGoodsShipmentOpt(root: Node): Either[String, Option[GoodsShipment]] =
+ (root \ Tags.GoodsShipment).headOption match
+ case None =>
+ Right(None)
+ case Some(goodsShipmentNode) =>
+ parseGoodsShipment(goodsShipmentNode).map(Some(_))
+
+ private def parseGoodsShipment(n: Node): Either[String, GoodsShipment] =
+ for {
+ consignmentNode <- req((n \ Tags.Consignment).headOption, Tags.Consignment)
+ consignment <- ConsignmentParser.parseConsignment(consignmentNode)
+ goodsItems <- GoodsItemsParser.parseGoodsItems(n)
+ } yield GoodsShipment(
+ consignment = consignment,
+ goodsItem = goodsItems
+ )
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Helpers.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Helpers.scala
new file mode 100644
index 0000000..0db7457
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Helpers.scala
@@ -0,0 +1,65 @@
+/*
+ * 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 java.util.UUID
+import scala.util.Try
+import scala.xml.{Node, NodeSeq}
+
+object Helpers {
+
+ def textOptDeep(xml: NodeSeq, tag: String): Option[String] =
+ (xml \\ tag).headOption.map(_.text.trim).filter(_.nonEmpty)
+
+ def textOptChild(n: Node, tag: String): Option[String] =
+ (n \ tag).headOption.map(_.text.trim).filter(_.nonEmpty)
+
+ def req[A](opt: Option[A], field: String): Either[String, A] =
+ opt.toRight(s"Missing required field: $field")
+
+ def parseOptionalInt(raw: Option[String], field: String): Either[String, Option[Int]] =
+ raw match {
+ case None => Right(None)
+ case Some(v) =>
+ Try(v.trim.toInt).toEither.left.map(_ => s"Invalid integer for $field: $v").map(Some(_))
+ }
+
+ def parseBigDecimal(s: String): Either[String, BigDecimal] =
+ Try(BigDecimal(s.trim)).toEither.left.map(_ => s"Invalid decimal: $s")
+
+ def parseBoolean(s: String): Either[String, Boolean] =
+ s.trim.toLowerCase match {
+ case "true" | "1" => Right(true)
+ case "false" | "0" => Right(false)
+ case other => Left(s"Invalid boolean: $other")
+ }
+
+ def parseOptionalUuid(raw: Option[String]): Either[String, Option[UUID]] =
+ raw match {
+ case None => Right(None)
+ case Some(v) =>
+ Try(UUID.fromString(v.trim)).toEither.left.map(_ => s"Invalid UUID: $v").map(Some(_))
+ }
+
+ def sequence[A](xs: List[Either[String, A]]): Either[String, List[A]] =
+ xs.foldRight(Right(Nil): Either[String, List[A]]) { (e, acc) =>
+ for {
+ x <- e
+ a <- acc
+ } yield x :: a
+ }
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParser.scala
new file mode 100644
index 0000000..d35e37e
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParser.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 uk.gov.hmrc.automatedexportsystem.parsers.AESIE507
+
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{AdditionalIdentifier, AuthorisationNumber, LocationOfGoods, QualifierOfIdentification, TypeOfLocation, UnLocode}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.{req, textOptChild}
+
+import scala.xml.Node
+
+object LocationOfGoodsParser {
+ def parseLocationOfGoods(n: Node): Either[String, LocationOfGoods] =
+ for {
+ typeOfLocation <- req(textOptChild(n, Tags.TypeOfLocation), Tags.TypeOfLocation).map(TypeOfLocation.apply)
+ qualifier <- req(textOptChild(n, Tags.QualifierOfIdentification), Tags.QualifierOfIdentification).map(QualifierOfIdentification.apply)
+ } yield LocationOfGoods(
+ typeOfLocation = typeOfLocation,
+ qualifierOfIdentification = qualifier,
+ authorisationNumber = textOptChild(n, Tags.AuthorisationNumber).map(AuthorisationNumber.apply),
+ additionalIdentifier = textOptChild(n, Tags.AdditionalIdentifier).map(AdditionalIdentifier.apply),
+ unLocode = textOptChild(n, Tags.UNLocode).map(UnLocode.apply)
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/PackagingParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/PackagingParser.scala
new file mode 100644
index 0000000..ecacff2
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/PackagingParser.scala
@@ -0,0 +1,43 @@
+/*
+ * 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 cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{NumberOfPackages, Packaging, SequenceNumber, ShippingMarks, TypeOfPackages}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.{parseOptionalInt, sequence, textOptChild}
+
+import scala.xml.Node
+
+object PackagingParser {
+
+ def parsePackaging(n: Node): Either[String, Option[NonEmptyList[Packaging]]] = {
+ val nodes = (n \ Tags.Packaging).toList
+ sequence(nodes.map(parsePackagingNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parsePackagingNode(n: Node): Either[String, Packaging] =
+ for {
+ sequenceNumber <- parseOptionalInt(textOptChild(n, Tags.SequenceNumber), Tags.SequenceNumber).map(_.map(SequenceNumber.apply))
+ numberOfPackages <- parseOptionalInt(textOptChild(n, Tags.NumberOfPackages), Tags.NumberOfPackages).map(_.map(NumberOfPackages.apply))
+ } yield Packaging(
+ sequenceNumber = sequenceNumber,
+ typeOfPackages = textOptChild(n, Tags.TypeOfPackages).map(TypeOfPackages.apply),
+ numberOfPackages = numberOfPackages,
+ shippingMarks = textOptChild(n, Tags.ShippingMarks).map(ShippingMarks.apply)
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/SealsParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/SealsParser.scala
new file mode 100644
index 0000000..24926f7
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/SealsParser.scala
@@ -0,0 +1,40 @@
+/*
+ * 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 cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{Seal, SealIdentifier, SequenceNumber}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.{parseOptionalInt, sequence, textOptChild}
+
+import scala.xml.Node
+
+object SealsParser {
+
+ def parseSeals(n: Node): Either[String, Option[NonEmptyList[Seal]]] = {
+ val nodes = (n \ Tags.Seal).toList
+ sequence(nodes.map(parseSealNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parseSealNode(n: Node): Either[String, Seal] =
+ for {
+ sequenceNumber <- parseOptionalInt(textOptChild(n, Tags.SequenceNumber), Tags.SequenceNumber).map(_.map(SequenceNumber.apply))
+ } yield Seal(
+ sequenceNumber = sequenceNumber,
+ sealIdentifier = textOptChild(n, Tags.Identifier).map(SealIdentifier.apply)
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Tags.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Tags.scala
new file mode 100644
index 0000000..ce09bf2
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/Tags.scala
@@ -0,0 +1,71 @@
+/*
+ * 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
+
+object Tags:
+ val SubmissionId = "submissionId"
+ val ExportOperation = "ExportOperation"
+ val ExportOperationType = "type"
+
+ val MRN = "MRN"
+ val DiscrepanciesExist = "discrepanciesExist"
+ val SplitIndicator = "splitIndicator"
+
+ val CustomsOfficeOfExitActual = "CustomsOfficeOfExitActual"
+ val ReferenceNumber = "referenceNumber"
+
+ val GoodsShipment = "GoodsShipment"
+ val Consignment = "Consignment"
+ val ModeOfTransportAtBorder = "modeOfTransportAtBorder"
+ val ReferenceNumberUCR = "referenceNumberUCR"
+ val ParentUCRID = "parentUCRID"
+
+ val LocationOfGoods = "LocationOfGoods"
+ val TypeOfLocation = "typeOfLocation"
+ val QualifierOfIdentification = "qualifierOfIdentification"
+ val AuthorisationNumber = "authorisationNumber"
+ val AdditionalIdentifier = "additionalIdentifier"
+ val UNLocode = "UNLocode"
+
+ val TransportEquipment = "TransportEquipment"
+ val NumberOfSeals = "numberOfSeals"
+ val ContainerIdentificationNumber = "containerIdentificationNumber"
+
+ val Seal = "Seal"
+ val Identifier = "identifier"
+
+ val GoodsReference = "GoodsReference"
+ val DeclarationGoodsItemNumber = "declarationGoodsItemNumber"
+
+ val ActiveBorderTransportMeans = "ActiveBorderTransportMeans"
+ val TypeOfIdentification = "typeOfIdentification"
+ val IdentificationNumber = "identificationNumber"
+ val Nationality = "nationality"
+
+ val TransportDocument = "TransportDocument"
+ val SequenceNumber = "sequenceNumber"
+ val Type = "type"
+
+ val GoodsItem = "GoodsItem"
+ val Commodity = "Commodity"
+ val GoodsMeasure = "GoodsMeasure"
+ val GrossMass = "grossMass"
+ val NetMass = "netMass"
+ val Packaging = "Packaging"
+ val TypeOfPackages = "typeOfPackages"
+ val NumberOfPackages = "numberOfPackages"
+ val ShippingMarks = "shippingMarks"
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportDocumentParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportDocumentParser.scala
new file mode 100644
index 0000000..6806aae
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportDocumentParser.scala
@@ -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 uk.gov.hmrc.automatedexportsystem.parsers.AESIE507
+
+import cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{ReferenceNumber, SequenceNumber, TransportDocument, TransportDocumentType}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.{parseOptionalInt, sequence, textOptChild}
+
+import scala.xml.Node
+
+object TransportDocumentParser {
+
+ def parseTransportDocuments(n: Node): Either[String, Option[NonEmptyList[TransportDocument]]] = {
+ val nodes = (n \ Tags.TransportDocument).toList
+ sequence(nodes.map(parseTransportDocumentNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parseTransportDocumentNode(n: Node): Either[String, TransportDocument] =
+ for {
+ sequenceNumber <- parseOptionalInt(textOptChild(n, Tags.SequenceNumber), Tags.SequenceNumber).map(_.map(SequenceNumber.apply))
+ documentType <- parseOptionalInt(textOptChild(n, Tags.Type), Tags.Type).map(_.map(TransportDocumentType.apply))
+ } yield TransportDocument(
+ sequenceNumber = sequenceNumber,
+ transportDocumentType = documentType,
+ referenceNumber = textOptChild(n, Tags.ReferenceNumber).map(ReferenceNumber.apply)
+ )
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportEquipmentParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportEquipmentParser.scala
new file mode 100644
index 0000000..7874926
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/TransportEquipmentParser.scala
@@ -0,0 +1,43 @@
+/*
+ * 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 cats.data.NonEmptyList
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{ContainerIdentificationNumber, NumberOfSeals, SequenceNumber, TransportEquipment}
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.*
+
+import scala.xml.Node
+
+object TransportEquipmentParser {
+ def parseTransportEquipment(n: Node): Either[String, Option[NonEmptyList[TransportEquipment]]] = {
+ val nodes = (n \ Tags.TransportEquipment).toList
+ sequence(nodes.map(parseTransportEquipmentNode)).map(NonEmptyList.fromList)
+ }
+
+ private def parseTransportEquipmentNode(n: Node): Either[String, TransportEquipment] =
+ for {
+ sequenceNumber <- parseOptionalInt(textOptChild(n, Tags.SequenceNumber), Tags.SequenceNumber).map(_.map(SequenceNumber.apply))
+ numberOfSeals <- parseOptionalInt(textOptChild(n, Tags.NumberOfSeals), Tags.NumberOfSeals).map(_.map(NumberOfSeals.apply))
+ } yield TransportEquipment(
+ sequenceNumber = sequenceNumber,
+ containerIdentificationNumber = textOptChild(n, Tags.ContainerIdentificationNumber)
+ .map(_.trim)
+ .filter(_.nonEmpty)
+ .map(ContainerIdentificationNumber.apply),
+ numberOfSeals = numberOfSeals
+ )
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/CodeListParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/CodeListParser.scala
index 1cbf4ee..056cc7b 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/parsers/CodeListParser.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/CodeListParser.scala
@@ -16,10 +16,10 @@
package uk.gov.hmrc.automatedexportsystem.parsers
+import org.xml.sax.SAXParseException
import uk.gov.hmrc.automatedexportsystem.models.codelists.CodeList
import java.time.LocalDateTime
-import org.xml.sax.SAXParseException
import scala.xml.XML
import scala.xml.parsing.FatalError
diff --git a/app/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParser.scala b/app/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParser.scala
new file mode 100644
index 0000000..0a22120
--- /dev/null
+++ b/app/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParser.scala
@@ -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.parsers
+
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.*
+import uk.gov.hmrc.automatedexportsystem.models.request.SubmissionRequest
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.Helpers.*
+import uk.gov.hmrc.automatedexportsystem.parsers.AESIE507.{ExportOperationParser, GoodsShipmentParser, Tags}
+
+import java.util.UUID
+import scala.xml.{Node, NodeSeq}
+
+object SubmissionRequestParser {
+
+ def fromXml(xml: NodeSeq): Either[String, SubmissionRequest] =
+ for {
+ submissionId <- parseOptionalSubmissionId(textOptDeep(xml, Tags.SubmissionId))
+ exportOpNode <- req((xml \\ Tags.ExportOperation).headOption, Tags.ExportOperation)
+ exportOp <- ExportOperationParser.parseExportOperation(exportOpNode)
+
+ officeNode <- req((xml \\ Tags.CustomsOfficeOfExitActual).headOption, Tags.CustomsOfficeOfExitActual)
+ office <- parseCustomsOfficeOfExitActual(officeNode)
+
+ shipment <- GoodsShipmentParser.parseGoodsShipmentOpt(xml.head)
+ } yield SubmissionRequest(
+ submissionId = submissionId,
+ exportOperation = exportOp,
+ customsOfficeOfExitActual = office,
+ goodsShipment = shipment
+ )
+
+ private def parseOptionalSubmissionId(raw: Option[String]): Either[String, Option[SubmissionId]] =
+ parseOptionalUuid(raw).map(_.map(SubmissionId.apply))
+
+ private def parseCustomsOfficeOfExitActual(n: Node): Either[String, CustomsOfficeOfExitActual] =
+ req(textOptChild(n, Tags.ReferenceNumber), Tags.ReferenceNumber)
+ .map(v => CustomsOfficeOfExitActual(ReferenceNumber(v)))
+
+}
diff --git a/app/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507Repository.scala b/app/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507Repository.scala
index 5fe3327..393b196 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507Repository.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507Repository.scala
@@ -18,30 +18,34 @@ package uk.gov.hmrc.automatedexportsystem.repositories
import cats.data.{EitherT, NonEmptyList}
import com.google.inject.ImplementedBy
-import com.mongodb.client.model.{IndexModel, IndexOptions}
+import com.mongodb.client.model.{IndexModel, IndexOptions, Projections, Sorts}
+import com.mongodb.{MongoNotPrimaryException, MongoSocketException, MongoTimeoutException}
import org.apache.pekko.pattern.RetrySupport
-import org.mongodb.scala.AggregateObservable
+import org.mongodb.scala.MongoException
import org.mongodb.scala.bson.conversions.Bson
-import org.mongodb.scala.model.{Aggregates, Filters, Indexes}
+import org.mongodb.scala.model.Filters.equal
+import org.mongodb.scala.model.{Aggregates, Filters, Indexes, ReplaceOptions}
import play.api.Logging
import uk.gov.hmrc.automatedexportsystem.config.AppConfig
import uk.gov.hmrc.automatedexportsystem.errors.MongoError
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{EoriNumber, SubmissionId}
import uk.gov.hmrc.automatedexportsystem.models.mongo.write.MongoAesIE507Message
+import uk.gov.hmrc.automatedexportsystem.models.responses.SubmissionSummary
import uk.gov.hmrc.mongo.MongoComponent
-import uk.gov.hmrc.mongo.play.json.PlayMongoRepository
import java.util.concurrent.TimeUnit
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
-import scala.reflect.ClassTag
import scala.util.control.NonFatal
+import org.bson.codecs.Codec
+import play.api.libs.json.OFormat
+import uk.gov.hmrc.mongo.play.json.{Codecs, PlayMongoRepository}
@ImplementedBy(classOf[AesIE507RepositoryImpl])
trait AesIE507Repository:
- def getMessages(eori: EoriNumber): EitherT[Future, MongoError, NonEmptyList[MongoAesIE507Message]]
-
- def getMessage(eori: EoriNumber, submissionId: SubmissionId): EitherT[Future, MongoError, MongoAesIE507Message]
+ def getMessages(eori: EoriNumber): EitherT[Future, MongoError, NonEmptyList[SubmissionSummary]]
+ def getMessage(eori: EoriNumber, submissionId: SubmissionId): EitherT[Future, MongoError, MongoAesIE507Message]
+ def submit(submission: MongoAesIE507Message): EitherT[Future, MongoError, Boolean]
@Singleton
class AesIE507RepositoryImpl @Inject() (
@@ -61,70 +65,126 @@ class AesIE507RepositoryImpl @Inject() (
IndexModel(
Indexes.compoundIndex(
Indexes.ascending("eoriNumber"),
- Indexes.ascending("_id")
+ Indexes.ascending("submissionId")
)
)
+ ),
+ extraCodecs = Seq[Codec[?]](
+ Codecs.playFormatCodec[SubmissionSummary](summon[OFormat[SubmissionSummary]])
)
),
AesIE507Repository,
Logging:
- def getMessages(eori: EoriNumber): EitherT[Future, MongoError, NonEmptyList[MongoAesIE507Message]] =
- val filter: Bson = Aggregates.filter(Filters.eq("eoriNumber", eori.value))
- val pipeline: Seq[Bson] = Seq(filter)
+ override def getMessages(eori: EoriNumber): EitherT[Future, MongoError, NonEmptyList[SubmissionSummary]] =
+ val pipeline: Seq[Bson] = Seq(
+ Aggregates.filter(Filters.eq("eoriNumber", eori.value)),
+ Aggregates.project(
+ Projections.fields(
+ Projections.computed("submissionId", "$submissionId"),
+ Projections.computed("ducr", "$goodsShipment.consignment.referenceNumberUCR"),
+ Projections.computed("mrn", "$exportOperation.mrn"),
+ Projections.computed("officeOfExitCode", "$customsOfficeOfExitActual.referenceNumber"),
+ Projections.computed("status", "$exportOperation.exportOperationType"),
+ Projections.computed("updatedAt", "$updatedAt"),
+ Projections.excludeId()
+ )
+ ),
+ Aggregates.sort(Sorts.descending("lastUpdated"))
+ )
- retryPipeline(pipeline)((obs: AggregateObservable[MongoAesIE507Message]) =>
- obs
+ val op: Future[Either[MongoError, NonEmptyList[SubmissionSummary]]] =
+ collection
+ .aggregate[SubmissionSummary](pipeline)
.toFuture()
- .map {
- case seq if seq.isEmpty =>
- Left(MongoError.DocumentNotFound(s"No documents found for EORI: ${eori.value}"))
- case seq => Right(NonEmptyList(seq.head, seq.tail.toList))
+ .map { summaries =>
+ NonEmptyList
+ .fromList(summaries.toList)
+ .toRight(
+ MongoError.DocumentNotFound(s"No documents found for EORI: ${eori.value}")
+ )
}
- )
- def getMessage(eori: EoriNumber, submissionId: SubmissionId): EitherT[Future, MongoError, MongoAesIE507Message] =
- val filter: Bson = Aggregates.filter(
- Filters.and(
- Filters.eq("eoriNumber", eori.value),
- Filters.eq("_id", submissionId.value.toString)
- )
- )
-
- val pipeline: Seq[Bson] = Seq(filter)
+ retryPipeline("getMessages", Map("eori" -> eori.value))(op)
- retryPipeline(pipeline)((obs: AggregateObservable[MongoAesIE507Message]) =>
- obs
+ override def getMessage(
+ eori: EoriNumber,
+ submissionId: SubmissionId
+ ): EitherT[Future, MongoError, MongoAesIE507Message] =
+ val op: Future[Either[MongoError, MongoAesIE507Message]] =
+ collection
+ .find(
+ Filters.and(
+ Filters.eq("eoriNumber", eori.value),
+ Filters.eq("submissionId", submissionId.value.toString)
+ )
+ )
.headOption()
- .map(opt =>
- opt.toRight(
+ .map(
+ _.toRight(
MongoError.DocumentNotFound(
- s"No document found for EORI: ${eori.value} " +
- s"and submissionId: ${submissionId.value}"
+ s"No document found for EORI: ${eori.value} and submissionId: ${submissionId.value}"
)
)
)
- )
- private def retryPipeline[T: ClassTag, R](
- pipeline: Seq[Bson]
- )(transform: AggregateObservable[T] => Future[Either[MongoError, R]]): EitherT[Future, MongoError, R] = {
- def func(): Future[Either[MongoError, R]] =
- transform(collection.aggregate[T](pipeline))
+ retryPipeline(
+ operationName = "getMessage",
+ context = Map("eoriNumber" -> eori.value, "submissionId" -> submissionId.value.toString)
+ )(op)
+
+ override def submit(submission: MongoAesIE507Message): EitherT[Future, MongoError, Boolean] =
+ val op: Future[Either[MongoError, Boolean]] =
+ collection
+ .replaceOne(
+ filter = equal("submissionId", submission.submissionId.value),
+ replacement = submission,
+ options = ReplaceOptions().upsert(true)
+ )
+ .toFuture()
+ .map(result => Right[MongoError, Boolean](result.getUpsertedId != null))
+
+ retryPipeline(
+ operationName = "submitUpsert",
+ context = Map("submissionId" -> submission.submissionId.value.toString)
+ )(op)
+
+ private def retryPipeline[R](
+ operationName: String,
+ context: Map[String, String]
+ )(
+ op: => Future[Either[MongoError, R]]
+ ): EitherT[Future, MongoError, R] =
+ def attempt(): Future[Either[MongoError, R]] =
+ op.recover {
+ case me: MongoException if !MongoRetryable.isRetryable(me) =>
+ Left(MongoError.UnexpectedError(me))
+ }
EitherT(
RetrySupport
.retry(
- attempt = func,
+ attempt = attempt,
attempts = appConfig.mongoRetryAttempts
)
.recover { case NonFatal(ex) =>
+ val ctx =
+ if context.isEmpty then ""
+ else context.map { case (k, v) => s"$k=$v" }.mkString(" ", " ", "")
+
logger.error(
- s"Aggregation pipeline ${pipeline.mkString} failed after" +
- s" ${appConfig.mongoRetryAttempts + 1} attempts with error: $ex"
+ s"$operationName failed after ${appConfig.mongoRetryAttempts + 1} attempts$ctx: " +
+ s"${ex.getClass.getSimpleName}: ${ex.getMessage}"
)
-
Left(MongoError.UnexpectedError(ex))
}
)
- }
+
+ private object MongoRetryable:
+ def isRetryable(t: Throwable): Boolean = t match
+ case _: MongoTimeoutException => true
+ case _: MongoSocketException => true
+ case _: MongoNotPrimaryException => true
+ case me: MongoException if me.hasErrorLabel("RetryableWriteError") => true
+ case me: MongoException if me.hasErrorLabel("TransientTransactionError") => true
+ case _ => false
diff --git a/app/uk/gov/hmrc/automatedexportsystem/services/SubmissionService.scala b/app/uk/gov/hmrc/automatedexportsystem/services/SubmissionService.scala
index 1227564..fbe9e44 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/services/SubmissionService.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/services/SubmissionService.scala
@@ -15,31 +15,34 @@
*/
package uk.gov.hmrc.automatedexportsystem.services
-
import cats.data.EitherT
import jakarta.inject.Singleton
import uk.gov.hmrc.automatedexportsystem.errors.{MongoError, SubmissionServiceError}
-import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber
-import uk.gov.hmrc.automatedexportsystem.models.mongo.write.MongoAesIE507Message
-import uk.gov.hmrc.automatedexportsystem.models.responses.{SubmissionSummary, SubmissionSummaryList}
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{EoriNumber, ExportOperationType}
+import uk.gov.hmrc.automatedexportsystem.models.request.{SubmissionRequest, SubmissionResult}
+import uk.gov.hmrc.automatedexportsystem.models.responses.SubmissionSummaryList
import uk.gov.hmrc.automatedexportsystem.repositories.AesIE507Repository
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
+trait SubmissionService {
+ def submitMessage(
+ request: SubmissionRequest,
+ exportOperationType: ExportOperationType,
+ eoriNumber: EoriNumber
+ ): EitherT[Future, MongoError, SubmissionResult]
+
+ def getSubmissions(eoriNumber: EoriNumber): EitherT[Future, SubmissionServiceError, SubmissionSummaryList]
+}
+
@Singleton
-class SubmissionService @Inject() (aesIE507Repository: AesIE507Repository)(using ExecutionContext):
- def getSubmissions(eoriNumber: EoriNumber): EitherT[Future, SubmissionServiceError, SubmissionSummaryList] =
+class SubmissionServiceImpl @Inject() (aesIE507Repository: AesIE507Repository)(using ExecutionContext) extends SubmissionService:
+ override def getSubmissions(eoriNumber: EoriNumber): EitherT[Future, SubmissionServiceError, SubmissionSummaryList] =
val submissionSummaryListResult: EitherT[Future, MongoError, SubmissionSummaryList] =
aesIE507Repository
.getMessages(eoriNumber)
- .map(messageNel =>
- SubmissionSummaryList(
- messageNel.toList.map(
- SubmissionSummary.fromMongoAesIE507Message
- )
- )
- )
+ .map(messageNel => SubmissionSummaryList(messageNel.toList))
submissionSummaryListResult.leftFlatMap {
case MongoError.DocumentNotFound(_) =>
@@ -56,3 +59,12 @@ class SubmissionService @Inject() (aesIE507Repository: AesIE507Repository)(using
)
}
end getSubmissions
+
+ override def submitMessage(
+ request: SubmissionRequest,
+ exportOperationType: ExportOperationType,
+ eoriNumber: EoriNumber
+ ): EitherT[Future, MongoError, SubmissionResult] =
+ aesIE507Repository
+ .submit(request.toMongoMessage(exportOperationType, eoriNumber))
+ .map(created => if (created) SubmissionResult.Created else SubmissionResult.Updated)
diff --git a/app/uk/gov/hmrc/automatedexportsystem/xml/XsdValidator.scala b/app/uk/gov/hmrc/automatedexportsystem/xml/XsdValidator.scala
index 59508fe..e4324cd 100644
--- a/app/uk/gov/hmrc/automatedexportsystem/xml/XsdValidator.scala
+++ b/app/uk/gov/hmrc/automatedexportsystem/xml/XsdValidator.scala
@@ -18,49 +18,43 @@ package uk.gov.hmrc.automatedexportsystem.xml
import cats.data.NonEmptyList
import cats.syntax.either.*
-import org.xml.sax.{ErrorHandler, SAXParseException}
+import org.xml.sax.{ErrorHandler, InputSource, SAXParseException}
import uk.gov.hmrc.automatedexportsystem.errors.SchemaError.{SchemaNotFoundError, SchemaParseError}
import uk.gov.hmrc.automatedexportsystem.errors.{SchemaError, XmlFailedValidationError, XmlSchemaValidationError}
import java.io.StringReader
import javax.xml.XMLConstants
+import javax.xml.parsers.SAXParserFactory
import javax.xml.transform.Source
-import javax.xml.transform.stream.StreamSource
+import javax.xml.transform.sax.SAXSource
import javax.xml.validation.{Schema, SchemaFactory, Validator}
import scala.collection.mutable.ArrayBuffer
-import scala.xml.NodeSeq
+import scala.xml.{InputSource, NodeSeq}
final class XsdValidator private (schema: Schema):
def validate(xml: NodeSeq): Either[XmlFailedValidationError, Unit] =
- val xmlSource: Source = XsdValidator.xmlToSource(xml)
-
+ val xmlSource: Source = XsdValidator.xmlToSaxSource(xml)
validate(xmlSource)
private def validate(xml: Source): Either[XmlFailedValidationError, Unit] =
val errorHandler: XsdValidator.XsdErrorHandler = XsdValidator.XsdErrorHandler()
val validator: Validator = getValidator(errorHandler)
- val validateResult: Either[XmlFailedValidationError, Unit] =
- Either
- .catchOnly[SAXParseException](validator.validate(xml))
- .leftMap(saxe => XmlFailedValidationError(NonEmptyList.of(XmlSchemaValidationError.fromSaxe(saxe))))
-
- validateResult.flatMap { _ =>
- val errors: List[SAXParseException] = errorHandler.getErrors
+ val thrown: Option[SAXParseException] =
+ Either.catchOnly[SAXParseException](validator.validate(xml)).left.toOption
- val xmlFailedValidationResult: Either[XmlFailedValidationError, Unit] =
- NonEmptyList
- .fromList(errors)
- .map(saxeNel => XmlFailedValidationError(saxeNel.map(XmlSchemaValidationError.fromSaxe)))
- .toLeft(())
+ val allErrors: List[SAXParseException] =
+ (errorHandler.getErrors ++ thrown.toList)
+ .distinctBy(e => (e.getLineNumber, e.getColumnNumber, e.getMessage))
- xmlFailedValidationResult
- }
+ NonEmptyList
+ .fromList(allErrors)
+ .map(saxeNel => XmlFailedValidationError(saxeNel.map(XmlSchemaValidationError.fromSaxe)))
+ .toLeft(())
private def getValidator(errorHandler: ErrorHandler): Validator =
val validator: Validator = schema.newValidator()
validator.setErrorHandler(errorHandler)
-
validator
end XsdValidator
@@ -68,32 +62,37 @@ object XsdValidator:
private class XsdErrorHandler extends ErrorHandler:
private lazy val errorBuffer: ArrayBuffer[SAXParseException] = ArrayBuffer.empty
- def warning(exception: SAXParseException): Unit = errorBuffer.addOne(exception)
+ def warning(exception: SAXParseException): Unit =
+ errorBuffer.addOne(exception)
- def error(exception: SAXParseException): Unit = errorBuffer.addOne(exception)
+ def error(exception: SAXParseException): Unit =
+ errorBuffer.addOne(exception)
- def fatalError(exception: SAXParseException): Unit = errorBuffer.addOne(exception)
+ def fatalError(exception: SAXParseException): Unit =
+ errorBuffer.addOne(exception)
+ throw exception
def getErrors: List[SAXParseException] = errorBuffer.toList
end XsdErrorHandler
- private def xmlToSource(xml: NodeSeq): Source =
- StreamSource(StringReader(xml.toString))
+ private def xmlToSaxSource(xml: NodeSeq): Source =
+ val spf = SAXParserFactory.newInstance()
+ spf.setNamespaceAware(true)
+ spf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
+
+ val xmlReader = spf.newSAXParser().getXMLReader
+ val inputSource = InputSource(new StringReader(xml.toString))
+ SAXSource(xmlReader, inputSource)
def fromXsdPath(path: String): Either[SchemaError, XsdValidator] =
Either
.catchNonFatal {
val schemaFactory: SchemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
- // needed because by default the XML parser restricts the maxOccurs attribute to 5000
- // we either turn the secure processing off here, or change the maxOccurs that are > 5000 in
- // the schemas to 5000
schemaFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false)
val schema: Schema = schemaFactory.newSchema(getClass.getResource(path))
-
XsdValidator(schema)
}
.leftMap {
case _: NullPointerException => SchemaNotFoundError(xsdPath = path)
case saxe: SAXParseException => SchemaParseError(SchemaError.XsdStructureError.fromSaxe(saxe))
}
-end XsdValidator
diff --git a/conf/1.0/schemas/AESSchema.xsd b/conf/1.0/schemas/AESSchema.xsd
index 7a853d2..1da301e 100644
--- a/conf/1.0/schemas/AESSchema.xsd
+++ b/conf/1.0/schemas/AESSchema.xsd
@@ -1,5 +1,10 @@
-
+
@@ -20,21 +25,12 @@
-
+
SUBMISSION_ID
-
-
-
-
-
-
-
-
-
-
+
diff --git a/conf/1.0/schemas/BaseTypes.xsd b/conf/1.0/schemas/BaseTypes.xsd
index f14ef07..ddbee79 100644
--- a/conf/1.0/schemas/BaseTypes.xsd
+++ b/conf/1.0/schemas/BaseTypes.xsd
@@ -20,6 +20,11 @@
+
+
+
+
+
diff --git a/conf/application.conf b/conf/application.conf
index 9bd68b6..a62754e 100644
--- a/conf/application.conf
+++ b/conf/application.conf
@@ -54,7 +54,7 @@ play.http.router = prod.Routes
# Microservice specific config
mongodb {
- uri = "mongodb://localhost:27017/automated-export-system"
+ uri = "mongodb://localhost:27017/automated-export-system?uuidRepresentation=standard"
timeToLiveInSeconds = 2419200 # 28 days in seconds
replaceIndexes = true
retryAttempts = 0
diff --git a/it/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerITSpec.scala b/it/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerITSpec.scala
index f94b59c..7577ac3 100644
--- a/it/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerITSpec.scala
+++ b/it/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerITSpec.scala
@@ -76,7 +76,7 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
val mongoAesIE507Message1: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id1),
+ submissionId = SubmissionId(id1),
eoriNumber = EoriNumber(eori),
createdAt = instant,
updatedAt = instant,
@@ -99,7 +99,7 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
NonEmptyList.one(
TransportEquipment(
sequenceNumber = Some(SequenceNumber(1)),
- containerIdentificationNumber = Some(ContainerIdentificationNumber(1)),
+ containerIdentificationNumber = Some(ContainerIdentificationNumber("some-id")),
numberOfSeals = Some(NumberOfSeals(1))
)
)
@@ -147,6 +147,7 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
goodsItem = Some(
NonEmptyList.one(
GoodsItem(
+ referenceNumberUcr = Some(ReferenceNumberUcr("ducr")),
declarationGoodsItemNumber = Some(DeclarationGoodsItemNumber(1)),
commodity = Commodity(
grossMass = GrossMass(100.55),
@@ -171,7 +172,7 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
val mongoAesIE507Message2: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id2),
+ submissionId = SubmissionId(id2),
eoriNumber = EoriNumber(eori),
createdAt = instant,
updatedAt = instant,
@@ -301,11 +302,6 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
BAD_REQUEST
XML failed schema validation
-
- 3
- 26
- cvc-complex-type.2.4.a: Invalid content was found starting with element 'ExportOperation'. One of '{{status}}' is expected.
-
5
33
@@ -367,7 +363,7 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
2
24
- cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{{1,35}}' for type 'UK_AlphaNumeric35Type'.
+ cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{{1,36}}' for type 'UK_AlphaNumeric36Type'.
2
@@ -375,62 +371,52 @@ class SubmissionControllerITSpec extends BaseISpec, MockitoSugar:
cvc-type.3.1.3: The value '' of element 'submissionId' is not valid.
- 3
- 18
- cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{{1,35}}' for type 'UK_AlphaNumeric35Type'.
-
-
- 3
- 18
- cvc-type.3.1.3: The value '' of element 'status' is not valid.
-
-
- 5
+ 4
20
cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '[1-3]{{1}}' for type 'UK_OneToThreeType'.
- 5
+ 4
20
cvc-type.3.1.3: The value '' of element 'type' is not valid.
- 6
+ 5
19
cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '([2][4-9]|[3-9][0-9])[A-Z]{{2}}[A-Z0-9]{{12}}[A-E][0-9]' for type 'UK_MRNType'.
- 6
+ 5
19
cvc-type.3.1.3: The value '' of element 'MRN' is not valid.
- 7
+ 6
34
cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration '[0, 1]'. It must be a value from the enumeration.
- 7
+ 6
34
cvc-type.3.1.3: The value '' of element 'discrepanciesExist' is not valid.
- 8
+ 7
30
cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration '[0, 1]'. It must be a value from the enumeration.
- 8
+ 7
30
cvc-type.3.1.3: The value '' of element 'splitIndicator' is not valid.
- 11
+ 10
31
cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '[A-Z]{{2}}[A-Z0-9]{{6}}' for type 'UK_ReferenceNumberType'.
- 11
+ 10
31
cvc-type.3.1.3: The value '' of element 'referenceNumber' is not valid.
diff --git a/test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositorySpec.scala b/it/test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositoryISpec.scala
similarity index 87%
rename from test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositorySpec.scala
rename to it/test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositoryISpec.scala
index 1afee30..2775412 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositorySpec.scala
+++ b/it/test/uk/gov/hmrc/automatedexportsystem/repositories/AesIE507RepositoryISpec.scala
@@ -20,6 +20,7 @@ import cats.data.NonEmptyList
import org.mockito.Mockito.when
import org.mongodb.scala.model.{Filters, Indexes}
import org.scalacheck.Arbitrary.arbitrary
+import org.scalacheck.Gen
import org.scalatest.EitherValues
import org.scalatest.freespec.AnyFreeSpecLike
import org.scalatest.matchers.should.Matchers
@@ -30,13 +31,14 @@ import uk.gov.hmrc.automatedexportsystem.errors.MongoError
import uk.gov.hmrc.automatedexportsystem.generators.MongoAesIE507MessageGenerator
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.{EoriNumber, SubmissionId}
import uk.gov.hmrc.automatedexportsystem.models.mongo.write.MongoAesIE507Message
+import uk.gov.hmrc.automatedexportsystem.models.responses.SubmissionSummary
import uk.gov.hmrc.mongo.test.DefaultPlayMongoRepositorySupport
import java.util.UUID
import java.util.concurrent.TimeUnit
import scala.concurrent.ExecutionContext
-class AesIE507RepositorySpec
+class AesIE507RepositoryISpec
extends AnyFreeSpecLike,
Matchers,
EitherValues,
@@ -57,6 +59,13 @@ class AesIE507RepositorySpec
val submissionId: SubmissionId = SubmissionId(UUID.fromString("6fb33641-6dc7-4a4f-adef-06238c13a317"))
val eoriNumber: EoriNumber = EoriNumber("eoriNumber")
+ extension (mongoAesIE507MessageGen: Gen[MongoAesIE507Message])
+ def withEori(eoriNumber: EoriNumber): Gen[MongoAesIE507Message] =
+ mongoAesIE507MessageGen.map(_.copy(eoriNumber = eoriNumber))
+
+ def withSubmissionId(submissionId: SubmissionId): Gen[MongoAesIE507Message] =
+ mongoAesIE507MessageGen.map(_.copy(submissionId = submissionId))
+
"AesIE507Repository" - {
import helpers.GenHelpers.*
@@ -70,7 +79,12 @@ class AesIE507RepositorySpec
forAll { (message: MongoAesIE507Message) =>
insert(message).futureValue
- find(Filters.eq("_id", message._id.value.toString)).futureValue shouldBe Seq(message)
+ find(
+ Filters.and(
+ Filters.eq("eoriNumber", message.eoriNumber.value),
+ Filters.eq("submissionId", message.submissionId.value.toString)
+ )
+ ).futureValue shouldBe Seq(message)
}
".getMessages" - {
@@ -89,11 +103,11 @@ class AesIE507RepositorySpec
repository.collection.insertMany(mongoAesIE507Messages).head().futureValue
- val messages: NonEmptyList[MongoAesIE507Message] =
+ val summariesNel: NonEmptyList[SubmissionSummary] =
repository.getMessages(TestData.eoriNumber).value.futureValue.value
- messages.length shouldBe 1
- messages.toList shouldBe mongoAesIE507MessagesMatchingEori
+ val summaries = summariesNel.toList
+ summaries.length shouldBe 1
}
"when there are multiple documents in the collection with that eori" in {
@@ -108,11 +122,14 @@ class AesIE507RepositorySpec
repository.collection.insertMany(mongoAesIE507Messages).head().futureValue
- val messages: NonEmptyList[MongoAesIE507Message] =
+ val messages: NonEmptyList[SubmissionSummary] =
repository.getMessages(TestData.eoriNumber).value.futureValue.value
+ val expected: List[SubmissionSummary] =
+ mongoAesIE507MessagesMatchingEori.toList.map(SubmissionSummary.fromMongoAesIE507Message)
+
messages.length shouldBe 5
- messages.toList should contain theSameElementsAs mongoAesIE507MessagesMatchingEori
+ messages.toList should contain theSameElementsAs expected
}
}
diff --git a/test/helpers/GenHelpers.scala b/test/helpers/GenHelpers.scala
index 47f9c60..94888b4 100644
--- a/test/helpers/GenHelpers.scala
+++ b/test/helpers/GenHelpers.scala
@@ -26,6 +26,6 @@ trait GenHelpers:
mongoAesIE507MessageGen.map(_.copy(eoriNumber = eoriNumber))
def withSubmissionId(submissionId: SubmissionId): Gen[MongoAesIE507Message] =
- mongoAesIE507MessageGen.map(_.copy(_id = submissionId))
+ mongoAesIE507MessageGen.map(_.copy(submissionId = submissionId))
object GenHelpers extends GenHelpers
diff --git a/test/resources/testdata/aesIE507RequestInvalidBadPatterns.xml b/test/resources/testdata/aesIE507RequestInvalidBadPatterns.xml
index dc9429a..c4b6fba 100644
--- a/test/resources/testdata/aesIE507RequestInvalidBadPatterns.xml
+++ b/test/resources/testdata/aesIE507RequestInvalidBadPatterns.xml
@@ -1,7 +1,6 @@
-
diff --git a/test/resources/testdata/aesIE507RequestInvalidMissingRequired.xml b/test/resources/testdata/aesIE507RequestInvalidMissingRequired.xml
index 5d1c75b..3e52420 100644
--- a/test/resources/testdata/aesIE507RequestInvalidMissingRequired.xml
+++ b/test/resources/testdata/aesIE507RequestInvalidMissingRequired.xml
@@ -1,6 +1,6 @@
- AH1237HAS
+ a2841a52-3ded-42cc-bcc3-1d7af9cad072
1
0
diff --git a/test/resources/testdata/aesIE507RequestValid.xml b/test/resources/testdata/aesIE507RequestValid.xml
index cc660d2..ae4579b 100644
--- a/test/resources/testdata/aesIE507RequestValid.xml
+++ b/test/resources/testdata/aesIE507RequestValid.xml
@@ -1,7 +1,6 @@
- AH1237HAS
- ST91823ZX
+ 700d2c74-f313-4dfb-89d3-0f9ad1bb9377
1
26GB0000X6524786A9
diff --git a/test/resources/testdata/aesIE507RequestValidNoOptionals.xml b/test/resources/testdata/aesIE507RequestValidNoOptionals.xml
index 7880ec0..06f9f1c 100644
--- a/test/resources/testdata/aesIE507RequestValidNoOptionals.xml
+++ b/test/resources/testdata/aesIE507RequestValidNoOptionals.xml
@@ -1,7 +1,6 @@
- AH1237HAS
- ST91823ZX
+ 700d2c74-f313-4dfb-89d3-0f9ad1bb9377
1
26GB0000X6524786A9
diff --git a/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerSpec.scala
index 4196d50..44c66d2 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/controllers/SubmissionControllerSpec.scala
@@ -19,6 +19,7 @@ package uk.gov.hmrc.automatedexportsystem.controllers
import cats.data.{EitherT, NonEmptyList}
import helpers.XmlOps
import org.apache.pekko.util.ByteString
+import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.when
import org.scalatest.EitherValues
import play.api.http.{HttpVerbs, MimeTypes, Status as StatusValues}
@@ -33,6 +34,7 @@ import uk.gov.hmrc.automatedexportsystem.errors.{SchemaError, SubmissionServiceE
import uk.gov.hmrc.automatedexportsystem.generators.MongoAesIE507MessageGenerator
import uk.gov.hmrc.automatedexportsystem.helpers.{AllMocks, BaseSpec}
import uk.gov.hmrc.automatedexportsystem.models.aesIE507.*
+import uk.gov.hmrc.automatedexportsystem.models.request.SubmissionResult.Created
import uk.gov.hmrc.automatedexportsystem.models.responses.{SubmissionSummary, SubmissionSummaryList}
import uk.gov.hmrc.automatedexportsystem.services.{AesIE507XmlValidationService, SubmissionService}
import uk.gov.hmrc.automatedexportsystem.util.IdGenerator
@@ -52,6 +54,7 @@ class SubmissionControllerSpec extends BaseSpec, EitherValues, AllMocks, MongoAe
val xmlValidationActionRefiner: XmlValidationActionRefiner[AesIE507XmlValidationService] =
XmlValidationActionRefiner(xmlValidationService)
+ val mockSubmissionService = mock[SubmissionService]
val idGenerator: IdGenerator = mock[IdGenerator]
val aesAuthAction: AesAuthAction =
@@ -119,14 +122,25 @@ class SubmissionControllerSpec extends BaseSpec, EitherValues, AllMocks, MongoAe
"when applied with a Request containing a valid XML body that passes IE507 request schema validation" in {
val requestXml: Elem =
- I'm valid XML
-
+
+
+ 1
+ 26GB0000X6524786A9
+ 0
+ 0
+
+
+ GB000001
+
+
+ when(submissionService.submitMessage(any(), any(), any()))
+ .thenReturn(EitherT(Future.successful(Right(()))))
val request: FakeRequest[NodeSeq] =
FakeRequest(HttpVerbs.POST, "/dummy/path")
.withHeaders("content-type" -> "application/xml")
.withBody(requestXml)
-
when(xmlValidationService.validate(requestXml)).thenReturn(EitherT(Future.successful(Right(()))))
+ when(submissionService.submitMessage(any(), any(), any())).thenReturn(EitherT(Future.successful(Right(Created))))
val result: Future[Result] = Helpers.call(submissionController.message, request)
diff --git a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefinerSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefinerSpec.scala
index 34e7b5e..3f7e19d 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefinerSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/AesAuthRequestRefinerSpec.scala
@@ -19,25 +19,27 @@ import play.api.mvc.{AnyContentAsEmpty, Request}
import play.api.test.FakeRequest
import uk.gov.hmrc.automatedexportsystem.controllers.actions.request.AesAuthAttr
import uk.gov.hmrc.automatedexportsystem.helpers.BaseSpec
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
class AesAuthRequestRefinerSpec extends BaseSpec:
private val refiner = new AesAuthRequestRefiner
+ private val eori = EoriNumber("GB123456789000")
"AesAuthRequestRefiner.refine" - {
"return Right(AesAuthRequest) when EORI attr is present" in {
val request: Request[AnyContentAsEmpty.type] =
FakeRequest("POST", "/")
- .addAttr(AesAuthAttr.Eori, "GB123456789000")
+ .addAttr(AesAuthAttr.Eori, eori.value)
val result = Await.result(refiner.refine(request), 2.seconds)
result match
case Right(aesAuthRequest) =>
- aesAuthRequest.eori shouldBe "GB123456789000"
+ aesAuthRequest.eori shouldBe eori
aesAuthRequest.request shouldBe request
case Left(_) =>
fail("Expected Right(AesAuthRequest) but got Left")
diff --git a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlPayloadActionRefinerSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlPayloadActionRefinerSpec.scala
index 7262674..61d582b 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlPayloadActionRefinerSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlPayloadActionRefinerSpec.scala
@@ -26,6 +26,7 @@ import play.api.mvc.*
import play.api.mvc.Results.Status
import play.api.test.{DefaultAwaitTimeout, FakeRequest, Helpers}
import uk.gov.hmrc.automatedexportsystem.controllers.actions.request.AesAuthRequest
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber
import scala.concurrent.{ExecutionContext, Future}
import scala.xml.{Elem, NodeSeq}
@@ -35,7 +36,7 @@ class XmlPayloadActionRefinerSpec extends AnyFreeSpecLike, Matchers, EitherValue
val xmlPayloadActionRefiner: XmlPayloadActionRefiner = XmlPayloadActionRefiner()
- val eori = "some-eori"
+ val eori = EoriNumber("some-eori")
"XmlPayloadActionRefiner" - {
".invokeBlock" - {
diff --git a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlValidationActionRefinerSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlValidationActionRefinerSpec.scala
index 360f96b..73b3148 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlValidationActionRefinerSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/controllers/actions/XmlValidationActionRefinerSpec.scala
@@ -32,6 +32,7 @@ import play.api.mvc.{AnyContent, Request, Result}
import play.api.test.{DefaultAwaitTimeout, FakeRequest, Helpers}
import uk.gov.hmrc.automatedexportsystem.controllers.actions.request.XmlPayloadRequest
import uk.gov.hmrc.automatedexportsystem.errors.{SchemaError, XmlFailedValidationError, XmlSchemaValidationError}
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.EoriNumber
import uk.gov.hmrc.automatedexportsystem.services.XmlValidationService
import scala.concurrent.{ExecutionContext, Future}
@@ -45,7 +46,7 @@ class XmlValidationActionRefinerSpec extends AnyFreeSpecLike, Matchers, EitherVa
val xmlValidationActionRefiner: XmlValidationActionRefiner[XmlValidationService] =
XmlValidationActionRefiner(xmlValidationService)
- val eori = "some-eori"
+ val eori = EoriNumber("some-eori")
"XmlValidationActionRefiner" - {
diff --git a/test/uk/gov/hmrc/automatedexportsystem/generators/AesIE507Generators.scala b/test/uk/gov/hmrc/automatedexportsystem/generators/AesIE507Generators.scala
index ce58771..7986e33 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/generators/AesIE507Generators.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/generators/AesIE507Generators.scala
@@ -97,7 +97,7 @@ trait AesIE507Generators extends BaseGenerators:
given containerIdentificationNumberArb: Arbitrary[ContainerIdentificationNumber] =
Arbitrary {
- arbitrary[Int].map(ContainerIdentificationNumber.apply)
+ arbitrary[String].map(ContainerIdentificationNumber.apply)
}
given numberOfSealsArb: Arbitrary[NumberOfSeals] =
@@ -286,9 +286,10 @@ trait AesIE507Generators extends BaseGenerators:
Arbitrary {
for
declarationGoodsItemNumber <- arbitrary[Option[DeclarationGoodsItemNumber]]
+ referenceNumberUCR <- arbitrary[Option[ReferenceNumberUcr]]
commodity <- arbitrary[Commodity]
packaging <- arbitrary[Option[NonEmptyList[Packaging]]]
- yield GoodsItem(declarationGoodsItemNumber, commodity, packaging)
+ yield GoodsItem(declarationGoodsItemNumber, referenceNumberUCR, commodity, packaging)
}
given goodsShipmentArb: Arbitrary[GoodsShipment] =
diff --git a/test/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507MessageSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507MessageSpec.scala
index bee7413..138b9f0 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507MessageSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/models/mongo/write/MongoAesIE507MessageSpec.scala
@@ -36,7 +36,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageAllFields: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id),
+ submissionId = SubmissionId(id),
eoriNumber = EoriNumber("eoriNumber"),
createdAt = Instant.ofEpochMilli(instant),
updatedAt = Instant.ofEpochMilli(instant),
@@ -59,7 +59,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
NonEmptyList.one(
TransportEquipment(
sequenceNumber = Some(SequenceNumber(1)),
- containerIdentificationNumber = Some(ContainerIdentificationNumber(1)),
+ containerIdentificationNumber = Some(ContainerIdentificationNumber("some-number")),
numberOfSeals = Some(NumberOfSeals(1))
)
)
@@ -107,6 +107,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
goodsItem = Some(
NonEmptyList.one(
GoodsItem(
+ referenceNumberUcr = None,
declarationGoodsItemNumber = Some(DeclarationGoodsItemNumber(1)),
commodity = Commodity(
grossMass = GrossMass(100.55),
@@ -132,7 +133,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageAllFieldsJson: JsValue =
Json.parse(s"""
|{
- | "_id" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
+ | "submissionId" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
| "eoriNumber" : "eoriNumber",
| "createdAt" : {
| "$$date" : {
@@ -160,7 +161,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
| "parentUcrId" : "parentUcrId",
| "transportEquipment" : [ {
| "sequenceNumber" : 1,
- | "containerIdentificationNumber" : 1,
+ | "containerIdentificationNumber" : "some-number",
| "numberOfSeals" : 1
| } ],
| "seal" : [ {
@@ -208,7 +209,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageNoOptionalGoodsShipment: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id),
+ submissionId = SubmissionId(id),
eoriNumber = EoriNumber("eoriNumber"),
createdAt = Instant.ofEpochMilli(instant),
updatedAt = Instant.ofEpochMilli(instant),
@@ -227,7 +228,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageNoOptionalGoodsShipmentJson: JsValue =
Json.parse(s"""
|{
- | "_id" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
+ | "submissionId" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
| "eoriNumber" : "eoriNumber",
| "createdAt" : {
| "$$date" : {
@@ -253,7 +254,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageNoObjOptionals: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id),
+ submissionId = SubmissionId(id),
eoriNumber = EoriNumber("eoriNumber"),
createdAt = Instant.ofEpochMilli(instant),
updatedAt = Instant.ofEpochMilli(instant),
@@ -324,6 +325,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
goodsItem = Some(
NonEmptyList.one(
GoodsItem(
+ referenceNumberUcr = None,
declarationGoodsItemNumber = None,
commodity = Commodity(
grossMass = GrossMass(100.55),
@@ -349,7 +351,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageNoObjOptionalsJson: JsValue =
Json.parse(s"""
|{
- | "_id" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
+ | "submissionId" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
| "eoriNumber" : "eoriNumber",
| "createdAt" : {
| "$$date" : {
@@ -396,7 +398,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageEmptyLists: MongoAesIE507Message =
MongoAesIE507Message(
- _id = SubmissionId(id),
+ submissionId = SubmissionId(id),
eoriNumber = EoriNumber("eoriNumber"),
createdAt = Instant.ofEpochMilli(instant),
updatedAt = Instant.ofEpochMilli(instant),
@@ -442,7 +444,7 @@ class MongoAesIE507MessageSpec extends AnyFreeSpecLike, Matchers, EitherValues,
val mongoAesIE507MessageEmptyListsJson: JsValue =
Json.parse(s"""
|{
- | "_id" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
+ | "submissionId" : "6fb33641-6dc7-4a4f-adef-06238c13a317",
| "eoriNumber" : "eoriNumber",
| "createdAt" : {
| "$$date" : {
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParserSpec.scala
new file mode 100644
index 0000000..f8f1008
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ActiveBorderTransportMeansParserSpec.scala
@@ -0,0 +1,91 @@
+/*
+ * 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.helpers.BaseSpec
+
+import scala.xml.XML
+
+class ActiveBorderTransportMeansParserSpec extends BaseSpec:
+
+ "parseActiveBorderTransportMeans" - {
+
+ "return Right(None) when ActiveBorderTransportMeans is absent" in {
+ val xml =
+ XML.loadString(
+ """
+ | value
+ |""".stripMargin
+ )
+
+ val result = ActiveBorderTransportMeansParser.parseActiveBorderTransportMeans(xml)
+
+ result shouldBe Right(None)
+ }
+
+ "return populated model when all optional fields are present" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 10
+ | IMO1234567
+ | GB
+ |
+ |""".stripMargin
+ )
+
+ val result = ActiveBorderTransportMeansParser.parseActiveBorderTransportMeans(xml)
+
+ result.isRight shouldBe true
+ val abtm = result.toOption.flatten.value
+ abtm.typeOfIdentification.map(_.value) shouldBe Some("10")
+ abtm.identificationNumber.map(_.value) shouldBe Some("IMO1234567")
+ abtm.nationality.map(_.value) shouldBe Some("GB")
+ }
+
+ "return model with None fields when child tags are missing" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |""".stripMargin
+ )
+
+ val result = ActiveBorderTransportMeansParser.parseActiveBorderTransportMeans(xml)
+
+ val abtm = result.toOption.flatten.value
+ abtm.typeOfIdentification shouldBe None
+ abtm.identificationNumber shouldBe None
+ abtm.nationality shouldBe None
+ }
+
+ "trim whitespace in text nodes" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | IMO1234567
+ |
+ |""".stripMargin
+ )
+
+ val result = ActiveBorderTransportMeansParser.parseActiveBorderTransportMeans(xml)
+
+ result.toOption.flatten.value.identificationNumber.map(_.value) shouldBe Some("IMO1234567")
+ }
+ }
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParserSpec.scala
new file mode 100644
index 0000000..2c52778
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ConsignmentParserSpec.scala
@@ -0,0 +1,138 @@
+/*
+ * 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 org.scalatest.EitherValues
+import org.scalatest.EitherValues.*
+import uk.gov.hmrc.automatedexportsystem.helpers.BaseSpec
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.TypeOfIdentification
+
+import scala.xml.XML
+
+class ConsignmentParserSpec extends BaseSpec {
+
+ "parseConsignment" - {
+ "parse a valid consignment with all fields present" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 1
+ | 6GB536187624189-S458
+ | GB/ABC-12345
+ |
+ |
+ | 1
+ | CONT1234567890123
+ | 2
+ |
+ |
+ |
+ | 1
+ | SEAL123
+ |
+ |
+ |
+ | 1
+ | 1
+ |
+ |
+ |
+ | A
+ | B
+ | AUTH12345
+ | AD01
+ | UNLOCODE123
+ |
+ |
+ |
+ | 20
+ | IDNUMBER123
+ | GB
+ |
+ |
+ |
+ | 1
+ | 2
+ | REF123
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = ConsignmentParser.parseConsignment(xml)
+
+ result.isRight shouldBe true
+ val parsed = result.value
+
+ parsed.modeOfTransportAtBorder.map(_.value) shouldBe Some(1)
+ parsed.referenceNumberUCR.value shouldBe "6GB536187624189-S458"
+ parsed.parentUcrId.map(_.value) shouldBe Some("GB/ABC-12345")
+
+ parsed.transportEquipment.map(_.length) shouldBe Some(1)
+ parsed.seal.map(_.length) shouldBe Some(1)
+ parsed.goodsReference.map(_.length) shouldBe Some(1)
+ parsed.transportDocument.map(_.length) shouldBe Some(1)
+
+ parsed.activeBorderTransportMeans.map(_.typeOfIdentification.value) shouldBe Some(TypeOfIdentification("20"))
+ parsed.locationOfGoods.typeOfLocation.value shouldBe "A"
+ }
+
+ "return populated model when only required fields are present" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 6GB536187624189-S458
+ |
+ | A
+ | B
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = ConsignmentParser.parseConsignment(xml)
+
+ result.isRight shouldBe true
+ val parsed = result.value
+
+ parsed.referenceNumberUCR.value shouldBe "6GB536187624189-S458"
+ parsed.locationOfGoods.typeOfLocation.value shouldBe "A"
+ parsed.locationOfGoods.qualifierOfIdentification.value shouldBe "B"
+ }
+
+ "return no model, but error msg when required fields are missing" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ | B
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = ConsignmentParser.parseConsignment(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase() should include("missing required field: referencenumberucr")
+ }
+
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParserSpec.scala
new file mode 100644
index 0000000..acdc107
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/ExportOperationParserSpec.scala
@@ -0,0 +1,89 @@
+/*
+ * 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.helpers.BaseSpec
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.ExportOperation
+import org.scalatest.EitherValues
+import org.scalatest.EitherValues.*
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.ExportOperationType.Standard
+
+import scala.xml.XML
+
+class ExportOperationParserSpec extends BaseSpec {
+ "parseExportOperation" - {
+
+ "parse valid ExportOperation XML" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |""".stripMargin
+ )
+
+ val result = ExportOperationParser.parseExportOperation(xml)
+
+ result.isRight shouldBe true
+ val parsed: ExportOperation = result.value
+
+ parsed.exportOperationType shouldBe Standard
+ parsed.mrn.value shouldBe "23GB12345678901234"
+ parsed.discrepanciesExist.value shouldBe false
+ parsed.splitIndicator.value shouldBe false
+ }
+
+ "return Awaiting when exportOperationType is missing" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |""".stripMargin
+ )
+
+ val result = ExportOperationParser.parseExportOperation(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase should include("missing required field: type")
+ }
+
+ "return Left when MRN is missing" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ | 1
+ | 0
+ | 0
+ |
+ |""".stripMargin
+ )
+
+ val result = ExportOperationParser.parseExportOperation(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase should include("missing required field: mrn")
+ }
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemParserSpec.scala
new file mode 100644
index 0000000..6c90af7
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsItemParserSpec.scala
@@ -0,0 +1,180 @@
+/*
+ * 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.helpers.BaseSpec
+
+import org.scalatest.EitherValues
+import org.scalatest.EitherValues.*
+import scala.xml.XML
+
+class GoodsItemParserSpec extends BaseSpec {
+ "parseGoodsItem" - {
+ "return Right(None) when no GoodsItem nodes exist" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ GoodsItemsParser.parseGoodsItems(xml) shouldBe Right(None)
+ }
+
+ "return Right(Some(nonEmptyList)) when GoodsItem is valid" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ | 12.1
+ | 11.0
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ | 1
+ | UCR-123
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsItemsParser.parseGoodsItems(xml)
+
+ result.isRight shouldBe true
+ val items = result.value.value
+ items.length shouldBe 1
+
+ val item = items.head
+ item.commodity.grossMass.value shouldBe BigDecimal("12.1")
+ item.commodity.netMass.value shouldBe BigDecimal("11.0")
+ item.declarationGoodsItemNumber.map(_.value) shouldBe Some(1)
+ item.referenceNumberUcr.map(_.value) shouldBe Some("UCR-123")
+ }
+
+ "return Left when GoodsMeasure is missing" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsItemsParser.parseGoodsItems(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: goodsmeasure"
+ }
+
+ "return Left when grossMass is not numeric" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ | abc
+ | 11.0
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsItemsParser.parseGoodsItems(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "invalid decimal: abc"
+ }
+
+ "return Left when netMass is not numeric" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ | 12.1
+ | xyz
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsItemsParser.parseGoodsItems(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "invalid decimal: xyz"
+ }
+
+ "return Left when declarationGoodsItemNumber is invalid" in {
+ val xml =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ | 12.1
+ | 11.0
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ | abc
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsItemsParser.parseGoodsItems(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "invalid integer for declarationgoodsitemnumber: abc"
+ }
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParserSpec.scala
new file mode 100644
index 0000000..a03c0ca
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/GoodsShipmentParserSpec.scala
@@ -0,0 +1,130 @@
+/*
+ * 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.helpers.BaseSpec
+import org.scalatest.EitherValues
+import org.scalatest.EitherValues.*
+import scala.xml.XML
+
+class GoodsShipmentParserSpec extends BaseSpec {
+
+ "parseGoodsShipment" - {
+
+ "return Left when GoodsShipment node is missing" in {
+ val xml = XML.loadString("")
+
+ val result = GoodsShipmentParser.parseGoodsShipmentOpt(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: consignment"
+ }
+
+ "return Left when Consignment empty" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |""".stripMargin
+ )
+ val result = GoodsShipmentParser.parseGoodsShipmentOpt(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: referencenumberucr"
+ }
+
+ "return Right with valid consignment" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ | 6GB536187624189-S458
+ |
+ | 1
+ | 1
+ |
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsShipmentParser.parseGoodsShipmentOpt(xml)
+ result.isRight shouldBe true
+
+ val shipmentOpt = result.value
+ shipmentOpt.isDefined shouldBe true
+
+ val shipment = shipmentOpt.value
+ shipment.consignment.referenceNumberUCR.value shouldBe "6GB536187624189-S458"
+ shipment.consignment.locationOfGoods.typeOfLocation.value shouldBe "1"
+ shipment.consignment.locationOfGoods.qualifierOfIdentification.value shouldBe "1"
+ shipment.goodsItem shouldBe None
+ }
+
+ "return Right when GoodsItem is valid" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ |
+ | 6GB536187624189-S458
+ |
+ | 1
+ | 1
+ |
+ |
+ |
+ |
+ |
+ | 12.1
+ | 11.0
+ |
+ |
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = GoodsShipmentParser.parseGoodsShipmentOpt(xml)
+ result.isRight shouldBe true
+
+ val shipmentOpt = result.value
+ shipmentOpt.isDefined shouldBe true
+
+ val shipment = shipmentOpt.value
+
+ shipment.goodsItem.isDefined shouldBe true
+ shipment.goodsItem.value.length shouldBe 1
+
+ val item = shipment.goodsItem.value.head
+ item.commodity.grossMass.value shouldBe BigDecimal("12.1")
+ item.commodity.netMass.value shouldBe BigDecimal("11.0")
+ item.declarationGoodsItemNumber shouldBe None
+ item.referenceNumberUcr shouldBe None
+ }
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParserSpec.scala
new file mode 100644
index 0000000..fdea59b
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/AESIE507/LocationOfGoodsParserSpec.scala
@@ -0,0 +1,107 @@
+/*
+ * 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.helpers.BaseSpec
+import org.scalatest.EitherValues
+import org.scalatest.EitherValues.*
+import scala.xml.XML
+
+class LocationOfGoodsParserSpec extends BaseSpec {
+ "parseLocationOfGoods" - {
+ "return Right when required fields are present (and optional absent)" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ | 1
+ | 1
+ |
+ |""".stripMargin
+ )
+
+ val result = LocationOfGoodsParser.parseLocationOfGoods(xml)
+
+ result.isRight shouldBe true
+ val parsed = result.value
+
+ parsed.typeOfLocation.value shouldBe "1"
+ parsed.qualifierOfIdentification.value shouldBe "1"
+ parsed.authorisationNumber shouldBe None
+ parsed.additionalIdentifier shouldBe None
+ parsed.unLocode shouldBe None
+ }
+
+ "return Right when all fields are present" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ | 1
+ | 1
+ | AUTH123
+ | ADD456
+ | GBLON
+ |
+ |""".stripMargin
+ )
+
+ val result = LocationOfGoodsParser.parseLocationOfGoods(xml)
+
+ result.isRight shouldBe true
+ val parsed = result.value
+
+ parsed.typeOfLocation.value shouldBe "1"
+ parsed.qualifierOfIdentification.value shouldBe "1"
+ parsed.authorisationNumber.map(_.value) shouldBe Some("AUTH123")
+ parsed.additionalIdentifier.map(_.value) shouldBe Some("ADD456")
+ parsed.unLocode.map(_.value) shouldBe Some("GBLON")
+ }
+
+ "return Left when typeOfLocation is missing" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ | 1
+ |
+ |""".stripMargin
+ )
+
+ val result = LocationOfGoodsParser.parseLocationOfGoods(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: typeoflocation"
+ }
+
+ "return Left when qualifierOfIdentification is missing" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ | 1
+ |
+ |""".stripMargin
+ )
+
+ val result = LocationOfGoodsParser.parseLocationOfGoods(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: qualifierofidentification"
+ }
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParserSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParserSpec.scala
new file mode 100644
index 0000000..5f2923f
--- /dev/null
+++ b/test/uk/gov/hmrc/automatedexportsystem/parsers/SubmissionRequestParserSpec.scala
@@ -0,0 +1,202 @@
+/*
+ * 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
+
+import org.scalatest.EitherValues.*
+import uk.gov.hmrc.automatedexportsystem.helpers.BaseSpec
+import uk.gov.hmrc.automatedexportsystem.models.aesIE507.ExportOperationType
+
+import scala.xml.XML
+
+class SubmissionRequestParserSpec extends BaseSpec {
+
+ "SubmissionRequestParser.fromXml" - {
+
+ "return Right with all sections parsed when XML is valid" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ | 123e4567-e89b-12d3-a456-426614174000
+ |
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |
+ |
+ | GB000001
+ |
+ |
+ |
+ |
+ | 6GB536187624189-S458
+ |
+ | 1
+ | 1
+ |
+ |
+ |
+ |
+ |
+ | 12.1
+ | 11.0
+ |
+ |
+ |
+ | 1
+ | BX
+ |
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isRight shouldBe true
+ val parsed = result.value
+
+ parsed.submissionId.map(_.value.toString) shouldBe Some("123e4567-e89b-12d3-a456-426614174000")
+ parsed.exportOperation.exportOperationType shouldBe ExportOperationType.Standard
+ parsed.exportOperation.mrn.value shouldBe "23GB12345678901234"
+ parsed.exportOperation.discrepanciesExist.value shouldBe false
+ parsed.exportOperation.splitIndicator.value shouldBe false
+
+ parsed.customsOfficeOfExitActual.referenceNumber.value shouldBe "GB000001"
+
+ parsed.goodsShipment.isDefined shouldBe true
+ val shipment = parsed.goodsShipment.value
+ shipment.consignment.referenceNumberUCR.value shouldBe "6GB536187624189-S458"
+ shipment.goodsItem.isDefined shouldBe true
+ shipment.goodsItem.value.length shouldBe 1
+ }
+
+ "return Right with submissionId None when submissionId is absent" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |
+ | GB000001
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isRight shouldBe true
+ result.value.submissionId shouldBe None
+ }
+
+ "return Left when ExportOperation is missing" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ | GB000001
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: exportoperation"
+ }
+
+ "return Left when CustomsOfficeOfExitActual is missing" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: type"
+ }
+
+ "return Left when CustomsOfficeOfExitActual.referenceNumber is missing" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isLeft shouldBe true
+ result.left.value.toLowerCase shouldBe "missing required field: referencenumber"
+ }
+
+ "return Right with happy path when GoodsShipment is absent" in {
+ val xml: scala.xml.Elem =
+ XML.loadString(
+ """
+ |
+ |
+ | 1
+ | 23GB12345678901234
+ | 0
+ | 0
+ |
+ |
+ | GB000001
+ |
+ |
+ |""".stripMargin
+ )
+
+ val result = SubmissionRequestParser.fromXml(xml)
+
+ result.isRight shouldBe true
+ result.value.goodsShipment shouldBe None
+ }
+ }
+}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/services/SubmissionServiceSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/services/SubmissionServiceSpec.scala
index 55a96df..a434eb8 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/services/SubmissionServiceSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/services/SubmissionServiceSpec.scala
@@ -38,7 +38,7 @@ class SubmissionServiceSpec extends AnyFreeSpecLike, Matchers, EitherValues, Sca
val aesIE507Repository: AesIE507Repository = mock[AesIE507Repository]
- val submissionService: SubmissionService = SubmissionService(aesIE507Repository)
+ val submissionService: SubmissionService = SubmissionServiceImpl(aesIE507Repository)
"SubmissionService" - {
import helpers.GenHelpers.*
@@ -66,23 +66,20 @@ class SubmissionServiceSpec extends AnyFreeSpecLike, Matchers, EitherValues, Sca
val submissionSummaryList: List[SubmissionSummary] =
mongoAesIE507Messages.map(SubmissionSummary.fromMongoAesIE507Message).toList
+ val submissionSummariesNel: NonEmptyList[SubmissionSummary] =
+ NonEmptyList.fromListUnsafe(submissionSummaryList)
+
when(aesIE507Repository.getMessages(eoriNumber))
.thenReturn(
EitherT(
Future.successful(
- Right(
- NonEmptyList.of(
- mongoAesIE507Messages.head,
- mongoAesIE507Messages.tail*
- )
- )
+ Right(submissionSummariesNel)
)
)
)
val result: SubmissionSummaryList =
submissionService.getSubmissions(eoriNumber).value.futureValue.value
-
result.submissions shouldBe submissionSummaryList
}
}
diff --git a/test/uk/gov/hmrc/automatedexportsystem/services/XmlValidationServiceSpec.scala b/test/uk/gov/hmrc/automatedexportsystem/services/XmlValidationServiceSpec.scala
index 44439d1..228a009 100644
--- a/test/uk/gov/hmrc/automatedexportsystem/services/XmlValidationServiceSpec.scala
+++ b/test/uk/gov/hmrc/automatedexportsystem/services/XmlValidationServiceSpec.scala
@@ -158,11 +158,6 @@ class XmlValidationServiceSpec extends AnyFreeSpecLike, Matchers, EitherValues,
result.value.futureValue.left.value shouldBe XmlFailedValidationError(
NonEmptyList.of(
- XmlSchemaValidationError(
- 3,
- 26,
- "cvc-complex-type.2.4.a: Invalid content was found starting with element 'ExportOperation'. One of '{status}' is expected."
- ),
XmlSchemaValidationError(
5,
33,
@@ -197,45 +192,39 @@ class XmlValidationServiceSpec extends AnyFreeSpecLike, Matchers, EitherValues,
XmlSchemaValidationError(
2,
24,
- "cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{1,35}' for type 'UK_AlphaNumeric35Type'."
+ "cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{1,36}' for type 'UK_AlphaNumeric36Type'."
),
XmlSchemaValidationError(2, 24, "cvc-type.3.1.3: The value '' of element 'submissionId' is not valid."),
XmlSchemaValidationError(
- 3,
- 18,
- "cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '.{1,35}' for type 'UK_AlphaNumeric35Type'."
- ),
- XmlSchemaValidationError(3, 18, "cvc-type.3.1.3: The value '' of element 'status' is not valid."),
- XmlSchemaValidationError(
- 5,
+ 4,
20,
"cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '[1-3]{1}' for type 'UK_OneToThreeType'."
),
- XmlSchemaValidationError(5, 20, "cvc-type.3.1.3: The value '' of element 'type' is not valid."),
+ XmlSchemaValidationError(4, 20, "cvc-type.3.1.3: The value '' of element 'type' is not valid."),
XmlSchemaValidationError(
- 6,
+ 5,
19,
"cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '([2][4-9]|[3-9][0-9])[A-Z]{2}[A-Z0-9]{12}[A-E][0-9]' for type 'UK_MRNType'."
),
- XmlSchemaValidationError(6, 19, "cvc-type.3.1.3: The value '' of element 'MRN' is not valid."),
+ XmlSchemaValidationError(5, 19, "cvc-type.3.1.3: The value '' of element 'MRN' is not valid."),
XmlSchemaValidationError(
- 7,
+ 6,
34,
"cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration '[0, 1]'. It must be a value from the enumeration."
),
- XmlSchemaValidationError(7, 34, "cvc-type.3.1.3: The value '' of element 'discrepanciesExist' is not valid."),
+ XmlSchemaValidationError(6, 34, "cvc-type.3.1.3: The value '' of element 'discrepanciesExist' is not valid."),
XmlSchemaValidationError(
- 8,
+ 7,
30,
"cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration '[0, 1]'. It must be a value from the enumeration."
),
- XmlSchemaValidationError(8, 30, "cvc-type.3.1.3: The value '' of element 'splitIndicator' is not valid."),
+ XmlSchemaValidationError(7, 30, "cvc-type.3.1.3: The value '' of element 'splitIndicator' is not valid."),
XmlSchemaValidationError(
- 11,
+ 10,
31,
"cvc-pattern-valid: Value '' is not facet-valid with respect to pattern '[A-Z]{2}[A-Z0-9]{6}' for type 'UK_ReferenceNumberType'."
),
- XmlSchemaValidationError(11, 31, "cvc-type.3.1.3: The value '' of element 'referenceNumber' is not valid.")
+ XmlSchemaValidationError(10, 31, "cvc-type.3.1.3: The value '' of element 'referenceNumber' is not valid.")
)
)
}