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.") ) ) }