diff --git a/source/IncomingMessages.Application/Extensions/DependencyInjection/IncomingMessagesExtensions.cs b/source/IncomingMessages.Application/Extensions/DependencyInjection/IncomingMessagesExtensions.cs index 7cb0ab755d..cd7ffa4ffb 100644 --- a/source/IncomingMessages.Application/Extensions/DependencyInjection/IncomingMessagesExtensions.cs +++ b/source/IncomingMessages.Application/Extensions/DependencyInjection/IncomingMessagesExtensions.cs @@ -137,11 +137,14 @@ public static IServiceCollection AddIncomingMessagesModule(this IServiceCollecti .AddSingleton(); services - .AddTransient(); + .AddTransient(); + services + .AddTransient(); services.AddTransient>(provider => new Dictionary<(IncomingDocumentType, DocumentFormat), IMessageParser> { - { (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Ebix), provider.GetRequiredService() }, + { (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Ebix), provider.GetRequiredService() }, + { (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Xml), provider.GetRequiredService() }, }); return services; diff --git a/source/IncomingMessages.Application/UseCases/ReceiveIncomingMarketMessage.cs b/source/IncomingMessages.Application/UseCases/ReceiveIncomingMarketMessage.cs index b35f3b6932..0aff41da01 100644 --- a/source/IncomingMessages.Application/UseCases/ReceiveIncomingMarketMessage.cs +++ b/source/IncomingMessages.Application/UseCases/ReceiveIncomingMarketMessage.cs @@ -112,7 +112,7 @@ await _delegateIncomingMessage .ConfigureAwait(false); var validationResult = await _validateIncomingMessage - .ValidateAsync(incomingMarketMessageParserResult.IncomingMessage, cancellationToken) + .ValidateAsync(incomingMarketMessageParserResult.IncomingMessage, incomingDocumentFormat, cancellationToken) .ConfigureAwait(false); if (!validationResult.Success) diff --git a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs index 121c39b377..37f0b3e9a3 100644 --- a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs +++ b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions; using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation; using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors; @@ -53,6 +54,7 @@ public ValidateIncomingMessage( public async Task ValidateAsync( IIncomingMessage incomingMessage, + DocumentFormat documentFormat, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(incomingMessage); @@ -62,7 +64,7 @@ public async Task ValidateAsync( VerifyReceiverAsync(incomingMessage), CheckMessageIdAsync(incomingMessage, cancellationToken), CheckMessageTypeAsync(incomingMessage, cancellationToken), - CheckBusinessReasonAsync(incomingMessage, cancellationToken), + CheckBusinessReasonAsync(incomingMessage, documentFormat, cancellationToken), CheckBusinessTypeAsync(incomingMessage, cancellationToken)) .ConfigureAwait(false)) .SelectMany(errs => errs); @@ -135,9 +137,10 @@ private async Task> CheckMessageTypeAsync( private async Task> CheckBusinessReasonAsync( IIncomingMessage message, + DocumentFormat documentFormat, CancellationToken cancellationToken) { - var result = await _processTypeValidator.ValidateAsync(message, cancellationToken) + var result = await _processTypeValidator.ValidateAsync(message, documentFormat, cancellationToken) .ConfigureAwait(false); return result.Errors; } diff --git a/source/IncomingMessages.Domain/Validation/IProcessTypeValidator.cs b/source/IncomingMessages.Domain/Validation/IProcessTypeValidator.cs index 8763418fff..c047d9b505 100644 --- a/source/IncomingMessages.Domain/Validation/IProcessTypeValidator.cs +++ b/source/IncomingMessages.Domain/Validation/IProcessTypeValidator.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions; namespace Energinet.DataHub.EDI.IncomingMessages.Domain.Validation; @@ -24,7 +25,5 @@ public interface IProcessTypeValidator /// /// Validates Process Type /// - /// - /// - public Task ValidateAsync(IIncomingMessage message, CancellationToken cancellationToken); + public Task ValidateAsync(IIncomingMessage message, DocumentFormat documentFormat, CancellationToken cancellationToken); } diff --git a/source/IncomingMessages.Domain/Validation/ProcessTypeValidator.cs b/source/IncomingMessages.Domain/Validation/ProcessTypeValidator.cs index 3576228281..151b9af0fe 100644 --- a/source/IncomingMessages.Domain/Validation/ProcessTypeValidator.cs +++ b/source/IncomingMessages.Domain/Validation/ProcessTypeValidator.cs @@ -21,29 +21,35 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Domain.Validation; public class ProcessTypeValidator : IProcessTypeValidator { private static readonly IReadOnlyCollection _aggregatedMeasureDataWhitelist = - new[] - { - BusinessReason.PreliminaryAggregation.Code, - BusinessReason.BalanceFixing.Code, - BusinessReason.WholesaleFixing.Code, - BusinessReason.Correction.Code, - }; + [ + BusinessReason.PreliminaryAggregation.Code, + BusinessReason.BalanceFixing.Code, + BusinessReason.WholesaleFixing.Code, + BusinessReason.Correction.Code, + ]; private static readonly IReadOnlyCollection _wholesaleServicesWhitelist = - new[] - { - BusinessReason.WholesaleFixing.Code, - BusinessReason.Correction.Code, - }; + [ + BusinessReason.WholesaleFixing.Code, + BusinessReason.Correction.Code, + ]; + + private static readonly IReadOnlyCollection _meteredDataForMeasurementPointEbixWhiteList = + [ + BusinessReason.PeriodicMetering.Code, + BusinessReason.PeriodicFlexMetering + .Code, // Flex metering is only supported for Ebix and should be rejected when used for CIM + ]; private static readonly IReadOnlyCollection _meteredDataForMeasurementPointWhiteList = - new[] - { - BusinessReason.PeriodicMetering.Code, - BusinessReason.PeriodicFlexMetering.Code, // Flex metering is only supported for Ebix and should be rejected when used for CIM - }; + [ + BusinessReason.PeriodicMetering.Code, + ]; - public async Task ValidateAsync(IIncomingMessage message, CancellationToken cancellationToken) + public async Task ValidateAsync( + IIncomingMessage message, + DocumentFormat documentFormat, + CancellationToken cancellationToken) { return await Task.FromResult( message switch @@ -57,9 +63,13 @@ public async Task ValidateAsync(IIncomingMessage message, CancellationTo ? Result.Succeeded() : Result.Failure(new NotSupportedProcessType(rwsm.BusinessReason)), MeteredDataForMeasurementPointMessage mdfmpm => - _meteredDataForMeasurementPointWhiteList.Contains(mdfmpm.BusinessReason) - ? Result.Succeeded() - : Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason)), + documentFormat == DocumentFormat.Ebix + ? _meteredDataForMeasurementPointEbixWhiteList.Contains(mdfmpm.BusinessReason) + ? Result.Succeeded() + : Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason)) + : _meteredDataForMeasurementPointWhiteList.Contains(mdfmpm.BusinessReason) + ? Result.Succeeded() + : Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason)), _ => throw new InvalidOperationException($"The baw's on the slates! {message.GetType().Name}"), }) .ConfigureAwait(false); diff --git a/source/IncomingMessages.Infrastructure/IncomingMessages.Infrastructure.csproj b/source/IncomingMessages.Infrastructure/IncomingMessages.Infrastructure.csproj index 25eb422742..2f086227ea 100644 --- a/source/IncomingMessages.Infrastructure/IncomingMessages.Infrastructure.csproj +++ b/source/IncomingMessages.Infrastructure/IncomingMessages.Infrastructure.csproj @@ -977,6 +977,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/MessageHeaderExtractor.cs b/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/MessageHeaderExtractor.cs index c588f67486..66ded4bd3b 100644 --- a/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/MessageHeaderExtractor.cs +++ b/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/MessageHeaderExtractor.cs @@ -1,4 +1,4 @@ -// Copyright 2020 Energinet DataHub A/S +// Copyright 2020 Energinet DataHub A/S // // Licensed under the Apache License, Version 2.0 (the "License2"); // you may not use this file except in compliance with the License. diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/SchemaExtractor.cs b/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/SchemaExtractor.cs new file mode 100644 index 0000000000..2f848bd918 --- /dev/null +++ b/source/IncomingMessages.Infrastructure/MessageParsers/BaseParsers/Xml/SchemaExtractor.cs @@ -0,0 +1,124 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// 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. + +using System.Xml; +using System.Xml.Schema; +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; +using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Cim.Xml; + +namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.BaseParsers.Xml; + +public static class SchemaExtractor +{ + public static async Task<(XmlSchema? Schema, string? Namespace, IncomingMarketMessageParserResult? ParserResult)> GetXmlSchemaAsync( + IIncomingMarketMessageStream incomingMarketMessageStream, + string rootElement, + CimXmlSchemaProvider schemaProvider, + CancellationToken cancellationToken) + { + string? @namespace = null; + IncomingMarketMessageParserResult? parserResult = null; + XmlSchema? xmlSchema = null; + try + { + @namespace = GetNamespace(incomingMarketMessageStream, rootElement); + var version = GetVersion(@namespace); + var businessProcessType = BusinessProcessType(@namespace); + xmlSchema = await schemaProvider.GetSchemaAsync(businessProcessType, version, cancellationToken) + .ConfigureAwait(true); + + if (xmlSchema is null) + { + parserResult = new IncomingMarketMessageParserResult( + new InvalidBusinessReasonOrVersion(businessProcessType, version)); + } + } + catch (XmlException exception) + { + parserResult = InvalidXmlFailure(exception); + } + catch (ObjectDisposedException objectDisposedException) + { + parserResult = InvalidXmlFailure(objectDisposedException); + } + catch (IndexOutOfRangeException indexOutOfRangeException) + { + parserResult = InvalidXmlFailure(indexOutOfRangeException); + } + + return (xmlSchema, @namespace, parserResult); + } + + private static IncomingMarketMessageParserResult InvalidXmlFailure( + Exception exception) + { + return new IncomingMarketMessageParserResult( + InvalidMessageStructure.From(exception)); + } + + private static string GetNamespace(IIncomingMarketMessageStream marketMessage, string rootElement) + { + ArgumentNullException.ThrowIfNull(marketMessage); + + var settings = new XmlReaderSettings + { + Async = true, + IgnoreWhitespace = true, + IgnoreComments = true, + }; + + using var reader = XmlReader.Create(marketMessage.Stream, settings); + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name.Contains(rootElement)) + { + return reader.NamespaceURI; + } + } + + throw new XmlException($"Namespace for element '{rootElement}' not found."); + } + + private static string BusinessProcessType(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + var split = SplitNamespace(@namespace); + if (split.Length < 6) + { + throw new XmlException($"Invalid namespace format"); + } + + return split[3]; + } + + private static string GetVersion(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + var split = SplitNamespace(@namespace); + if (split.Length < 5) + { + throw new XmlException($"Invalid namespace format"); + } + + var version = split[4] + "." + split[5]; + return version; + } + + private static string[] SplitNamespace(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + return @namespace.Split(':'); + } +} diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/EbixMessageParserBase.cs b/source/IncomingMessages.Infrastructure/MessageParsers/EbixMessageParserBase.cs index d7a2509ddb..6449fd509b 100644 --- a/source/IncomingMessages.Infrastructure/MessageParsers/EbixMessageParserBase.cs +++ b/source/IncomingMessages.Infrastructure/MessageParsers/EbixMessageParserBase.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.ObjectModel; using System.Xml; using System.Xml.Linq; using System.Xml.Schema; @@ -39,40 +40,35 @@ public abstract class EbixMessageParserBase(EbixSchemaProvider schemaProvider) : protected abstract string RootPayloadElementName { get; } + private Collection ValidationErrors { get; } = []; + protected override async Task ParseMessageAsync( IIncomingMarketMessageStream marketMessage, XmlSchema schemaResult, CancellationToken cancellationToken) { using var reader = XmlReader.Create(marketMessage.Stream, CreateXmlReaderSettings(schemaResult)); - if (Errors.Count > 0) + var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false); + if (ValidationErrors.Count > 0) { - return new IncomingMarketMessageParserResult(Errors.ToArray()); + return new IncomingMarketMessageParserResult(ValidationErrors.ToArray()); } - var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false); var @namespace = GetNamespace(marketMessage); var ns = XNamespace.Get(@namespace); var header = ParseHeader(document, ns); var transactions = ParseTransactions(document, ns, header.SenderId); - if (Errors.Count != 0) - { - return new IncomingMarketMessageParserResult(Errors.ToArray()); - } - return CreateResult(header, transactions); } - protected override async Task<(XmlSchema? Schema, IncomingMarketMessageParserResult? Result)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken) + protected override async Task<(XmlSchema? Schema, ValidationError? ValidationError)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken) { - string? @namespace = null; - IncomingMarketMessageParserResult? parserResult = null; XmlSchema? xmlSchema = default; try { - @namespace = GetNamespace(marketMessage); + var @namespace = GetNamespace(marketMessage); var version = GetVersion(@namespace); var businessProcessType = BusinessProcessType(@namespace); xmlSchema = await _schemaProvider.GetSchemaAsync(businessProcessType, version, cancellationToken) @@ -80,24 +76,23 @@ protected override async Task ParseMessageAsy if (xmlSchema is null) { - parserResult = new IncomingMarketMessageParserResult( - new InvalidBusinessReasonOrVersion(businessProcessType, version)); + return (xmlSchema, new InvalidBusinessReasonOrVersion(businessProcessType, version)); } } catch (XmlException exception) { - parserResult = Invalid(exception); + return (xmlSchema, InvalidMessageStructure.From(exception)); } catch (ObjectDisposedException objectDisposedException) { - parserResult = Invalid(objectDisposedException); + return (xmlSchema, InvalidMessageStructure.From(objectDisposedException)); } catch (IndexOutOfRangeException indexOutOfRangeException) { - parserResult = Invalid(indexOutOfRangeException); + return (xmlSchema, InvalidMessageStructure.From(indexOutOfRangeException)); } - return (xmlSchema, parserResult); + return (xmlSchema, null); } protected abstract IReadOnlyCollection ParseTransactions(XDocument document, XNamespace ns, string senderNumber); @@ -210,6 +205,6 @@ private void OnValidationError(object? sender, ValidationEventArgs arguments) { var message = $"XML schema validation error at line {arguments.Exception.LineNumber}, position {arguments.Exception.LinePosition}: {arguments.Message}."; - Errors.Add(InvalidMessageStructure.From(message)); + ValidationErrors.Add(InvalidMessageStructure.From(message)); } } diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/MessageParserBase.cs b/source/IncomingMessages.Infrastructure/MessageParsers/MessageParserBase.cs index 2eb594349d..2b0963646b 100644 --- a/source/IncomingMessages.Infrastructure/MessageParsers/MessageParserBase.cs +++ b/source/IncomingMessages.Infrastructure/MessageParsers/MessageParserBase.cs @@ -22,30 +22,26 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers; public abstract class MessageParserBase() : IMessageParser { - protected Collection Errors { get; } = []; - public async Task ParseAsync( IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken) { var schemaResult = await GetSchemaAsync(marketMessage, cancellationToken).ConfigureAwait(false); + if (schemaResult.ValidationError is not null) + { + return new IncomingMarketMessageParserResult(schemaResult.ValidationError); + } + if (schemaResult.Schema == null) { - return schemaResult.Result ?? new IncomingMarketMessageParserResult(new InvalidSchemaOrNamespace()); + return new IncomingMarketMessageParserResult(new InvalidSchemaOrNamespace()); } return await ParseMessageAsync(marketMessage, schemaResult.Schema, cancellationToken) .ConfigureAwait(false); } - protected static IncomingMarketMessageParserResult Invalid( - Exception exception) - { - return new IncomingMarketMessageParserResult( - InvalidMessageStructure.From(exception)); - } - - protected abstract Task<(TSchema? Schema, IncomingMarketMessageParserResult? Result)> + protected abstract Task<(TSchema? Schema, ValidationError? ValidationError)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken); protected abstract Task ParseMessageAsync( diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/Ebix/MeteredDataForMeasurementPointEbixMessageParser.cs b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/Ebix/MeteredDataForMeasurementPointEbixMessageParser.cs index 6eea4b4dcf..3133e85990 100644 --- a/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/Ebix/MeteredDataForMeasurementPointEbixMessageParser.cs +++ b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/Ebix/MeteredDataForMeasurementPointEbixMessageParser.cs @@ -16,9 +16,9 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.Ebix; -public class MeteredDataForMeasurementPointEbixMessageParser(EbixMessageParser messageParser) : IMarketMessageParser +public class MeteredDataForMeasurementPointEbixMessageParser(MeteredDateForMeasurementPointEbixMessageParser messageParser) : IMarketMessageParser { - private readonly EbixMessageParser _messageParser = messageParser; + private readonly MeteredDateForMeasurementPointEbixMessageParser _messageParser = messageParser; public DocumentFormat HandledFormat => DocumentFormat.Ebix; diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/EbixMessageParser.cs b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointEbixMessageParser.cs similarity index 83% rename from source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/EbixMessageParser.cs rename to source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointEbixMessageParser.cs index c49bf01843..7839c7de0a 100644 --- a/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/EbixMessageParser.cs +++ b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointEbixMessageParser.cs @@ -20,7 +20,7 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers; -public class EbixMessageParser(EbixSchemaProvider schemaProvider) : EbixMessageParserBase(schemaProvider) +public class MeteredDateForMeasurementPointEbixMessageParser(EbixSchemaProvider schemaProvider) : EbixMessageParserBase(schemaProvider) { private const string SeriesElementName = "PayloadEnergyTimeSeries"; private const string Identification = "Identification"; @@ -47,11 +47,13 @@ protected override IReadOnlyCollection ParseTransactions foreach (var transactionElement in transactionElements) { var id = transactionElement.Element(ns + Identification)?.Value ?? string.Empty; - var resolution = transactionElement.Element(ns + ObservationTimeSeriesPeriod)?.Element(ns + ResolutionDuration)?.Value; - var startDateAndOrTimeDateTime = transactionElement.Element(ns + ObservationTimeSeriesPeriod)?.Element(ns + Start)?.Value ?? string.Empty; - var endDateAndOrTimeDateTime = transactionElement.Element(ns + ObservationTimeSeriesPeriod)?.Element(ns + End)?.Value; - var productNumber = transactionElement.Element(ns + IncludedProductCharacteristic)?.Element(ns + Identification)?.Value; - var productUnitType = transactionElement.Element(ns + IncludedProductCharacteristic)?.Element(ns + UnitType)?.Value; + var observationElement = transactionElement.Element(ns + ObservationTimeSeriesPeriod); + var resolution = observationElement?.Element(ns + ResolutionDuration)?.Value; + var startDateAndOrTimeDateTime = observationElement?.Element(ns + Start)?.Value ?? string.Empty; + var endDateAndOrTimeDateTime = observationElement?.Element(ns + End)?.Value; + var includedProductCharacteristicElement = transactionElement.Element(ns + IncludedProductCharacteristic); + var productNumber = includedProductCharacteristicElement?.Element(ns + Identification)?.Value; + var productUnitType = includedProductCharacteristicElement?.Element(ns + UnitType)?.Value; var meteringPointType = transactionElement.Element(ns + DetailMeasurementMeteringPointCharacteristic)?.Element(ns + MeteringPointType)?.Value; var meteringPointLocationId = transactionElement.Element(ns + MeteringPointDomainLocation)?.Element(ns + Identification)?.Value; diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointXmlMessageParser.cs b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointXmlMessageParser.cs new file mode 100644 index 0000000000..2612e4bc2e --- /dev/null +++ b/source/IncomingMessages.Infrastructure/MessageParsers/MeteredDateForMeasurementPointParsers/MeteredDateForMeasurementPointXmlMessageParser.cs @@ -0,0 +1,109 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// 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. + +using System.Xml.Linq; +using Energinet.DataHub.EDI.IncomingMessages.Domain; +using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.BaseParsers; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Cim.Xml; + +namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers; + +public class MeteredDateForMeasurementPointXmlMessageParser(CimXmlSchemaProvider schemaProvider) : XmlMessageParserBase(schemaProvider) +{ + private const string SeriesElementName = "Series"; + private const string MridElementName = "mRID"; + private const string UnitTypeElementName = "quantity_Measure_Unit.name"; + private const string ProductElementName = "product"; + private const string TypeOfMeteringPointElementName = "marketEvaluationPoint.type"; + private const string MeteringPointDomainLocationElementName = "marketEvaluationPoint.mRID"; + private const string PeriodElementName = "Period"; + private const string ResolutionElementName = "resolution"; + private const string TimeIntervalElementName = "timeInterval"; + private const string StartElementName = "start"; + private const string EndElementName = "end"; + private const string PointElementName = "Point"; + private const string PositionElementName = "position"; + private const string QuantityElementName = "quantity"; + private const string QualityElementName = "quality"; + + protected override string RootPayloadElementName => "NotifyValidatedMeasureData_MarketDocument"; + + protected override IReadOnlyCollection ParseTransactions(XDocument document, XNamespace ns, string senderNumber) + { + var seriesElements = document.Descendants(ns + SeriesElementName); + var result = new List(); + + foreach (var seriesElement in seriesElements) + { + var id = seriesElement.Element(ns + MridElementName)?.Value ?? string.Empty; + var meteringPointLocationId = seriesElement.Element(ns + MeteringPointDomainLocationElementName)?.Value; + var meteringPointType = seriesElement.Element(ns + TypeOfMeteringPointElementName)?.Value; + var productNumber = seriesElement.Element(ns + ProductElementName)?.Value; + var productUnitType = seriesElement.Element(ns + UnitTypeElementName)?.Value; + + var periodElement = seriesElement.Element(ns + PeriodElementName); + var resolution = periodElement?.Element(ns + ResolutionElementName)?.Value; + var startDateAndOrTimeDateTime = + periodElement + ?.Element(ns + TimeIntervalElementName) + ?.Element(ns + StartElementName) + ?.Value ?? string.Empty; + var endDateAndOrTimeDateTime = periodElement + ?.Element(ns + TimeIntervalElementName) + ?.Element(ns + EndElementName) + ?.Value; + + var energyObservations = seriesElement + //.Element(ns + Period)? + .Descendants(ns + PointElementName) + .Select( + e => new EnergyObservation( + e.Element(ns + PositionElementName)?.Value, + e.Element(ns + QuantityElementName)?.Value, + e.Element(ns + QualityElementName)?.Value)) + .ToList(); + + result.Add( + new MeteredDataForMeasurementPointSeries( + id, + resolution, + startDateAndOrTimeDateTime, + endDateAndOrTimeDateTime, + productNumber, + productUnitType, + meteringPointType, + meteringPointLocationId, + senderNumber, + energyObservations)); + } + + return result.AsReadOnly(); + } + + protected override IncomingMarketMessageParserResult CreateResult(MessageHeader header, IReadOnlyCollection transactions) + { + return new IncomingMarketMessageParserResult(new MeteredDataForMeasurementPointMessage( + header.MessageId, + header.MessageType, + header.CreatedAt, + header.SenderId, + header.ReceiverId, + header.SenderRole, + header.BusinessReason, + header.ReceiverRole, + header.BusinessType, + transactions)); + } +} diff --git a/source/IncomingMessages.Infrastructure/MessageParsers/XmlMessageParserBase.cs b/source/IncomingMessages.Infrastructure/MessageParsers/XmlMessageParserBase.cs new file mode 100644 index 0000000000..03cb05e6a9 --- /dev/null +++ b/source/IncomingMessages.Infrastructure/MessageParsers/XmlMessageParserBase.cs @@ -0,0 +1,203 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// 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. + +using System.Collections.ObjectModel; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Schema; +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; +using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions; +using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.BaseParsers; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Cim.Xml; + +namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers; + +public abstract class XmlMessageParserBase(CimXmlSchemaProvider schemaProvider) : MessageParserBase +{ + private const string MridElementName = "mRID"; + private const string TypeElementName = "type"; + private const string ProcessTypeElementName = "process.processType"; + private const string SenderMridElementName = "sender_MarketParticipant.mRID"; + private const string SenderRoleElementName = "sender_MarketParticipant.marketRole.type"; + private const string ReceiverMridElementName = "receiver_MarketParticipant.mRID"; + private const string ReceiverRoleElementName = "receiver_MarketParticipant.marketRole.type"; + private const string CreatedDateTimeElementName = "createdDateTime"; + private const string BusinessSectorTypeElementName = "businessSector.type"; + private readonly CimXmlSchemaProvider _schemaProvider = schemaProvider; + + protected abstract string RootPayloadElementName { get; } + + private Collection ValidationErrors { get; } = []; + + protected override async Task ParseMessageAsync( + IIncomingMarketMessageStream marketMessage, + XmlSchema schemaResult, + CancellationToken cancellationToken) + { + using var reader = XmlReader.Create(marketMessage.Stream, CreateXmlReaderSettings(schemaResult)); + var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false); + if (ValidationErrors.Count > 0) + { + return new IncomingMarketMessageParserResult(ValidationErrors.ToArray()); + } + + var @namespace = GetNamespace(marketMessage); + var ns = XNamespace.Get(@namespace); + + var header = ParseHeader(document, ns); + var transactions = ParseTransactions(document, ns, header.SenderId); + + return CreateResult(header, transactions); + } + + protected override async Task<(XmlSchema? Schema, ValidationError? ValidationError)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken) + { + XmlSchema? xmlSchema = default; + try + { + var @namespace = GetNamespace(marketMessage); + var version = GetVersion(@namespace); + var businessProcessType = BusinessProcessType(@namespace); + xmlSchema = await _schemaProvider.GetSchemaAsync(businessProcessType, version, cancellationToken) + .ConfigureAwait(true); + + if (xmlSchema is null) + { + return (xmlSchema, new InvalidBusinessReasonOrVersion(businessProcessType, version)); + } + } + catch (XmlException exception) + { + return (xmlSchema, InvalidMessageStructure.From(exception)); + } + catch (ObjectDisposedException objectDisposedException) + { + return (xmlSchema, InvalidMessageStructure.From(objectDisposedException)); + } + catch (IndexOutOfRangeException indexOutOfRangeException) + { + return (xmlSchema, InvalidMessageStructure.From(indexOutOfRangeException)); + } + + return (xmlSchema, null); + } + + protected abstract IReadOnlyCollection ParseTransactions(XDocument document, XNamespace ns, string senderNumber); + + protected abstract IncomingMarketMessageParserResult CreateResult(MessageHeader header, IReadOnlyCollection transactions); + + private static string[] SplitNamespace(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + return @namespace.Split(':'); + } + + private string BusinessProcessType(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + var split = SplitNamespace(@namespace); + if (split.Length < 6) + { + throw new XmlException($"Invalid namespace format"); + } + + return split[3]; + } + + private string GetVersion(string @namespace) + { + ArgumentNullException.ThrowIfNull(@namespace); + var split = SplitNamespace(@namespace); + if (split.Length < 5) + { + throw new XmlException($"Invalid namespace format"); + } + + var version = split[4] + "." + split[5]; + return version; + } + + private string GetNamespace(IIncomingMarketMessageStream marketMessage) + { + ArgumentNullException.ThrowIfNull(marketMessage); + + var settings = new XmlReaderSettings + { + Async = true, + IgnoreWhitespace = true, + IgnoreComments = true, + }; + + using var reader = XmlReader.Create(marketMessage.Stream, settings); + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name.Contains(RootPayloadElementName)) + { + return reader.NamespaceURI; + } + } + + throw new XmlException($"Namespace for element '{RootPayloadElementName}' not found."); + } + + private MessageHeader ParseHeader(XDocument document, XNamespace ns) + { + var headerElement = document.Descendants(ns + RootPayloadElementName).SingleOrDefault(); + if (headerElement == null) throw new InvalidOperationException("Header element not found"); + + var messageId = headerElement.Element(ns + MridElementName)?.Value ?? string.Empty; + var messageType = headerElement.Element(ns + TypeElementName)?.Value ?? string.Empty; + var processType = headerElement.Element(ns + ProcessTypeElementName)?.Value ?? string.Empty; + var senderId = headerElement.Element(ns + SenderMridElementName)?.Value ?? string.Empty; + var senderRole = headerElement.Element(ns + SenderRoleElementName)?.Value ?? string.Empty; + var receiverId = headerElement.Element(ns + ReceiverMridElementName)?.Value ?? string.Empty; + var receiverRole = headerElement.Element(ns + ReceiverRoleElementName)?.Value ?? string.Empty; + var createdAt = headerElement.Element(ns + CreatedDateTimeElementName)?.Value ?? string.Empty; + var businessType = headerElement.Element(ns + BusinessSectorTypeElementName)?.Value; + + return new MessageHeader( + messageId, + messageType, + processType, + senderId, + senderRole, + receiverId, + receiverRole, + createdAt, + businessType); + } + + private XmlReaderSettings CreateXmlReaderSettings(XmlSchema xmlSchema) + { + var settings = new XmlReaderSettings + { + Async = true, + ValidationType = ValidationType.Schema, + ValidationFlags = XmlSchemaValidationFlags.ProcessInlineSchema | + XmlSchemaValidationFlags.ReportValidationWarnings, + }; + + settings.Schemas.Add(xmlSchema); + settings.ValidationEventHandler += OnValidationError; + return settings; + } + + private void OnValidationError(object? sender, ValidationEventArgs arguments) + { + var message = + $"XML schema validation error at line {arguments.Exception.LineNumber}, position {arguments.Exception.LinePosition}: {arguments.Message}."; + ValidationErrors.Add(InvalidMessageStructure.From(message)); + } +} diff --git a/source/IncomingMessages.Infrastructure/Schemas/Cim/Xml/Schemas/urn-ediel-org-measure-notifyvalidatedmeasuredata-0-1.xsd b/source/IncomingMessages.Infrastructure/Schemas/Cim/Xml/Schemas/urn-ediel-org-measure-notifyvalidatedmeasuredata-0-1.xsd new file mode 100644 index 0000000000..589bea2952 --- /dev/null +++ b/source/IncomingMessages.Infrastructure/Schemas/Cim/Xml/Schemas/urn-ediel-org-measure-notifyvalidatedmeasuredata-0-1.xsd @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/IncomingMessages.IntegrationTests/Builders/MeteredDataForMeasurementPointBuilder.cs b/source/IncomingMessages.IntegrationTests/Builders/MeteredDataForMeasurementPointBuilder.cs index 6df203a196..d87e9ee107 100644 --- a/source/IncomingMessages.IntegrationTests/Builders/MeteredDataForMeasurementPointBuilder.cs +++ b/source/IncomingMessages.IntegrationTests/Builders/MeteredDataForMeasurementPointBuilder.cs @@ -33,7 +33,7 @@ public static IncomingMarketMessageStream CreateIncomingMessage( string messageId = "111131835", string senderRole = "MDR", string receiverNumber = "5790001330552", - string schema = "un:unece:260:data:EEM-DK_MeteredDataTimeSeries:v3") + string? schema = null) { string content; if (format == DocumentFormat.Ebix) @@ -47,7 +47,20 @@ public static IncomingMarketMessageStream CreateIncomingMessage( messageId, senderRole, receiverNumber, - schema); + schema ?? "un:unece:260:data:EEM-DK_MeteredDataTimeSeries:v3"); + } + else if (format == DocumentFormat.Xml) + { + content = GetXml( + senderActorNumber, + series, + messageType, + processType, + businessType, + messageId, + senderRole, + receiverNumber, + schema ?? "urn:ediel.org:measure:notifyvalidatedmeasuredata:0:1"); } else { @@ -119,6 +132,64 @@ private static string GetEbix( return doc.OuterXml; } + private static string GetXml( + ActorNumber senderActorNumber, + IReadOnlyCollection<(string TransactionId, Instant PeriodStart, Instant PeriodEnd, Resolution Resolution)> series, + string messageType, + string processType, + string businessType, + string messageId, + string senderRole, + string receiverNumber, + string schema) + { + var doc = new XmlDocument(); + doc.LoadXml($@" + + {messageId} + {messageType} + {processType} + {businessType} + {senderActorNumber.Value} + {senderRole} + {receiverNumber} + DGL + 2022-12-17T09:30:47Z + + {string.Join("\n", series.Select(s => $@" + + {s.TransactionId} + C1875000 + + 579999993331812345 + + E17 + + 2022-12-17T07:30:00Z + + 8716867000030 + KWH + + {s.Resolution.Code} + + {s.PeriodStart.ToString("yyyy-MM-ddTHH:mm'Z'", null)} + {s.PeriodEnd.ToString("yyyy-MM-ddTHH:mm'Z'", null)} + + + {string.Join("\n", GetEnergyObservations(s.Resolution).Select(e => $@" + + {e.Position} + {e.Quantity} + A03 + + "))} + + + "))} +"); + return doc.OuterXml; + } + private static ReadOnlyCollection<(int Position, int Quantity)> GetEnergyObservations(Resolution resolution) { var observations = new List<(int Position, int Quantity)>(); diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages.IntegrationTests.csproj b/source/IncomingMessages.IntegrationTests/IncomingMessages.IntegrationTests.csproj index c115a18ab2..70bf61a390 100644 --- a/source/IncomingMessages.IntegrationTests/IncomingMessages.IntegrationTests.csproj +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages.IntegrationTests.csproj @@ -63,6 +63,10 @@ limitations under the License. PreserveNewest + + + PreserveNewest + diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMessagesTests.cs b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMessagesTests.cs index 6922f98c6a..8d12364447 100644 --- a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMessagesTests.cs +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMessagesTests.cs @@ -66,9 +66,10 @@ public static TheoryData { - { DocumentFormat.Json, IncomingDocumentType.RequestAggregatedMeasureData, ActorRole.BalanceResponsibleParty, ReadJsonFile(@"IncomingMessages\RequestAggregatedMeasureDataAsDdk.json") }, - { DocumentFormat.Json, IncomingDocumentType.RequestWholesaleSettlement, ActorRole.EnergySupplier, ReadJsonFile(@"IncomingMessages\RequestWholesaleSettlement.json") }, - { DocumentFormat.Ebix, IncomingDocumentType.MeteredDataForMeasurementPoint, ActorRole.MeteredDataResponsible, ReadJsonFile(@"IncomingMessages\EbixMeteredDataForMeasurementPoint.xml") }, + { DocumentFormat.Json, IncomingDocumentType.RequestAggregatedMeasureData, ActorRole.BalanceResponsibleParty, ReadFile(@"IncomingMessages\RequestAggregatedMeasureDataAsDdk.json") }, + { DocumentFormat.Json, IncomingDocumentType.RequestWholesaleSettlement, ActorRole.EnergySupplier, ReadFile(@"IncomingMessages\RequestWholesaleSettlement.json") }, + { DocumentFormat.Ebix, IncomingDocumentType.MeteredDataForMeasurementPoint, ActorRole.GridAccessProvider, ReadFile(@"IncomingMessages\EbixMeteredDataForMeasurementPoint.xml") }, + { DocumentFormat.Xml, IncomingDocumentType.MeteredDataForMeasurementPoint, ActorRole.GridAccessProvider, ReadFile(@"IncomingMessages\MeteredDataForMeasurementPoint.xml") }, }; return data; @@ -81,17 +82,17 @@ public static IEnumerable InvalidIncomingRequestMessages() [ DocumentFormat.Json, IncomingDocumentType.RequestAggregatedMeasureData, - ReadJsonFile(@"IncomingMessages\FailSchemeValidationAggregatedMeasureData.json"), + ReadFile(@"IncomingMessages\FailSchemeValidationAggregatedMeasureData.json"), ], [ DocumentFormat.Json, IncomingDocumentType.RequestWholesaleSettlement, - ReadJsonFile(@"IncomingMessages\FailSchemeValidationRequestWholesaleSettlement.json"), + ReadFile(@"IncomingMessages\FailSchemeValidationRequestWholesaleSettlement.json"), ], [ DocumentFormat.Json, IncomingDocumentType.RequestWholesaleSettlement, - ReadJsonFile( + ReadFile( @"IncomingMessages\RequestWholesaleSettlementWithUnusedBusinessReason.json"), ], ]; @@ -146,7 +147,7 @@ public async Task AndGiven_DdmMdrHackIsApplicable_When_MessageIsReceived_Then_Bo // Act await _incomingMessagesRequest.ReceiveIncomingMarketMessageAsync( - ReadJsonFile(@"IncomingMessages\RequestAggregatedMeasureDataAsMdr.json"), + ReadFile(@"IncomingMessages\RequestAggregatedMeasureDataAsMdr.json"), DocumentFormat.Json, IncomingDocumentType.RequestAggregatedMeasureData, DocumentFormat.Json, @@ -365,7 +366,7 @@ public async Task When_MessageIsReceived_Then_IncomingMessageIsArchivedWithCorre authenticatedActor.SetAuthenticatedActor( new ActorIdentity(senderActorNumber, Restriction.Owned, ActorRole.BalanceResponsibleParty)); - var messageStream = ReadJsonFile(@"IncomingMessages\RequestAggregatedMeasureDataAsDdk.json"); + var messageStream = ReadFile(@"IncomingMessages\RequestAggregatedMeasureDataAsDdk.json"); // Act await _incomingMessagesRequest.ReceiveIncomingMarketMessageAsync( @@ -401,7 +402,7 @@ public async Task When_MeteredDataForMeasurementPointMessageIsReceived_Then_Inco authenticatedActor.SetAuthenticatedActor( new ActorIdentity(senderActorNumber, Restriction.Owned, ActorRole.MeteredDataResponsible)); - var messageStream = ReadJsonFile(@"IncomingMessages\EbixMeteredDataForMeasurementPoint.xml"); + var messageStream = ReadFile(@"IncomingMessages\EbixMeteredDataForMeasurementPoint.xml"); // Act await _incomingMessagesRequest.ReceiveIncomingMarketMessageAsync( @@ -445,7 +446,7 @@ public async Task When_MessageIsReceived_Then_IncomingMessageIsArchivedWithCorre authenticatedActor.SetAuthenticatedActor( new ActorIdentity(senderActorNumber, Restriction.Owned, ActorRole.BalanceResponsibleParty)); - var messageStream = ReadJsonFile(path); + var messageStream = ReadFile(path); // Act await _incomingMessagesRequest.ReceiveIncomingMarketMessageAsync( @@ -505,7 +506,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private static IncomingMarketMessageStream ReadJsonFile(string path) + private static IncomingMarketMessageStream ReadFile(string path) { var jsonDoc = File.ReadAllText(path); diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs index cb06d702b6..032304df7c 100644 --- a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs @@ -33,6 +33,7 @@ namespace Energinet.DataHub.EDI.IncomingMessages.IntegrationTests.IncomingMessag public class GivenIncomingMeteredDataForMeasurementMessageTests : IncomingMessagesTestBase { private readonly MarketMessageParser _marketMessageParser; + private readonly IDictionary<(IncomingDocumentType, DocumentFormat), IMessageParser> _messageParsers; private readonly ValidateIncomingMessage _validateIncomingMessage; private readonly ActorIdentity _actorIdentity; @@ -42,6 +43,7 @@ public GivenIncomingMeteredDataForMeasurementMessageTests( : base(incomingMessagesTestFixture, testOutputHelper) { _marketMessageParser = GetService(); + _messageParsers = GetService>(); var authenticatedActor = GetService(); _actorIdentity = new ActorIdentity(ActorNumber.Create("1234567890123"), restriction: Restriction.None, ActorRole.FromCode("DDM")); @@ -54,8 +56,9 @@ public GivenIncomingMeteredDataForMeasurementMessageTests( public async Task When_ReceiverIdIsDatahub_Then_ValidationSucceed() { var validDataHubReceiverId = "5790001330552"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("123456", @@ -65,9 +68,10 @@ public async Task When_ReceiverIdIsDatahub_Then_ValidationSucceed() ], receiverNumber: validDataHubReceiverId); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeTrue(); @@ -78,8 +82,9 @@ public async Task When_ReceiverIdIsDatahub_Then_ValidationSucceed() public async Task When_ReceiverIdIsNotDatahub_Then_ResultContainExceptedValidationError() { var invalidDataHubReceiverId = "5790001330052"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("123456", @@ -89,9 +94,10 @@ public async Task When_ReceiverIdIsNotDatahub_Then_ResultContainExceptedValidati ], receiverNumber: invalidDataHubReceiverId); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -101,9 +107,10 @@ public async Task When_ReceiverIdIsNotDatahub_Then_ResultContainExceptedValidati [Fact] public async Task When_SenderIdDoesNotMatchTheAuthenticatedUser_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var invalidSenderId = ActorNumber.Create("5790001330550"); var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, invalidSenderId, [ ("123456", @@ -112,9 +119,10 @@ public async Task When_SenderIdDoesNotMatchTheAuthenticatedUser_Then_ResultConta Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -124,9 +132,10 @@ public async Task When_SenderIdDoesNotMatchTheAuthenticatedUser_Then_ResultConta [Fact] public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var duplicatedTransactionId = "123456"; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ (duplicatedTransactionId, @@ -139,9 +148,10 @@ public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExcepted Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -151,9 +161,10 @@ public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExcepted [Fact] public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var emptyTransactionId = string.Empty; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ (emptyTransactionId, @@ -162,9 +173,10 @@ public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidation Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -174,9 +186,10 @@ public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidation [Fact] public async Task When_MessageIdIsEmpty_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var emptyMessageId = string.Empty; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("123456789", @@ -186,9 +199,10 @@ public async Task When_MessageIdIsEmpty_Then_ResultContainExceptedValidationErro ], messageId: emptyMessageId); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -198,9 +212,10 @@ public async Task When_MessageIdIsEmpty_Then_ResultContainExceptedValidationErro [Fact] public async Task When_MessageIdAlreadyExists_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var existingMessageId = "123564789"; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), @@ -209,9 +224,10 @@ public async Task When_MessageIdAlreadyExists_Then_ResultContainExceptedValidati await StoreMessageIdForActorAsync(existingMessageId, _actorIdentity.ActorNumber.Value); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -221,18 +237,20 @@ public async Task When_MessageIdAlreadyExists_Then_ResultContainExceptedValidati [Fact] public async Task When_SenderRoleInMessageIsMeteredDataResponsible_Then_ValidationSucceed() { + var documentFormat = DocumentFormat.Ebix; var validSenderRoleInMessage = ActorRole.MeteredDataResponsible; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], senderRole: validSenderRoleInMessage.Code); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeTrue(); @@ -242,20 +260,22 @@ public async Task When_SenderRoleInMessageIsMeteredDataResponsible_Then_Validati [Fact] public async Task When_AuthenticatedSenderRoleIsIncorrect_Then_ResultContainExceptedValidationError() { + var documentFormat = DocumentFormat.Ebix; var authenticatedActor = GetService(); var invalidSenderRole = ActorRole.EnergySupplier; var actorIdentity = new ActorIdentity(ActorNumber.Create("1234567890123"), restriction: Restriction.None, invalidSenderRole); authenticatedActor.SetAuthenticatedActor(actorIdentity); var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeFalse(); @@ -266,15 +286,16 @@ public async Task When_AuthenticatedSenderRoleIsIncorrect_Then_ResultContainExce public async Task When_BusinessProcessIsIncorrect_Then_ResultContainExceptedValidationError() { var invalidBusinessProcess = "un:unece:260:data:EEM-DK_DataTimeSeries:v3"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], schema: invalidBusinessProcess); - var (_, result) = await ParseMessageAsync(message.Stream); + var (_, result) = await ParseMessageAsync(message.Stream, documentFormat); result.Success.Should().BeFalse(); result.Errors.Should().Contain(error => error is InvalidBusinessReasonOrVersion); @@ -284,15 +305,16 @@ public async Task When_BusinessProcessIsIncorrect_Then_ResultContainExceptedVali public async Task When_SchemaIsIncorrect_Then_ResultContainExceptedValidationError() { var invalidSchema = "EEM-DK_MeteredDataTimeSeries:v3"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], schema: invalidSchema); - var (_, result) = await ParseMessageAsync(message.Stream); + var (_, result) = await ParseMessageAsync(message.Stream, documentFormat); result.Success.Should().BeFalse(); result.Errors.Should().Contain(error => error is InvalidMessageStructure); @@ -302,15 +324,16 @@ public async Task When_SchemaIsIncorrect_Then_ResultContainExceptedValidationErr public async Task When_ProcessTypeIsNotAllowed_Then_ResultContainExceptedValidationError() { var notAllowedProcessType = "1880"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], processType: notAllowedProcessType); - var (_, result) = await ParseMessageAsync(message.Stream); + var (_, result) = await ParseMessageAsync(message.Stream, documentFormat); result.Success.Should().BeFalse(); result.Errors.Should().Contain(error => error is InvalidMessageStructure); @@ -318,39 +341,88 @@ public async Task When_ProcessTypeIsNotAllowed_Then_ResultContainExceptedValidat [Theory] [InlineData("E23")] - [InlineData("D42")] //Only for Ebix - public async Task When_ProcessTypeIsAllowed_Then_ValidationSucceed(string allowedProcessType) + [InlineData("D42")] + public async Task When_ProcessTypeIsAllowedForEbix_Then_ValidationSucceed(string allowedProcessType) { + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], processType: allowedProcessType); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Success.Should().BeTrue(); result.Errors.Should().BeEmpty(); } + [Fact] + public async Task When_ProcessTypeIsAllowedForCim_Then_ValidationSucceed() + { + var allowedProcessType = "E23"; + var documentFormat = DocumentFormat.Xml; + var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( + documentFormat, + _actorIdentity.ActorNumber, + [ + ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), + ], + processType: allowedProcessType); + + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); + var result = await _validateIncomingMessage.ValidateAsync( + incomingMessage!, + documentFormat, + CancellationToken.None); + + result.Success.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } + + [Fact] + public async Task When_ProcessTypeIsNotAllowedForCim_Then_ResultContainExceptedValidationError() + { + var notAllowedProcessType = "D42"; + var documentFormat = DocumentFormat.Xml; + var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( + documentFormat, + _actorIdentity.ActorNumber, + [ + ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), + ], + processType: notAllowedProcessType); + + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); + var result = await _validateIncomingMessage.ValidateAsync( + incomingMessage!, + documentFormat, + CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Errors.Should().Contain(error => error is NotSupportedProcessType); + } + [Fact] public async Task When_MessageTypeIsNotAllowed_Then_ResultContainExceptedValidationError() { var notAllowedMessageType = "1880"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], messageType: notAllowedMessageType); - var (_, result) = await ParseMessageAsync(message.Stream); + var (_, result) = await ParseMessageAsync(message.Stream, documentFormat); result.Success.Should().BeFalse(); result.Errors.Should().Contain(error => error is InvalidMessageStructure); @@ -363,15 +435,16 @@ public async Task When_MessageTypeIsNotAllowed_Then_ResultContainExceptedValidat public async Task When_MessageTypeIsAllowed_Then_ResultContainExceptedValidationError() { var allowedMessageType = "E66"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 31, 0, 0), Resolution.QuarterHourly), ], messageType: allowedMessageType); - var (_, result) = await ParseMessageAsync(message.Stream); + var (_, result) = await ParseMessageAsync(message.Stream, documentFormat); result.Success.Should().BeTrue(); result.Errors.Should().BeEmpty(); @@ -380,9 +453,10 @@ public async Task When_MessageTypeIsAllowed_Then_ResultContainExceptedValidation [Fact] public async Task When_MessageIdIs35Characters_Then_ValidationSucceed() { + var documentFormat = DocumentFormat.Ebix; var validMessageId = "12356478912356478912356478912356478"; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("123456798", @@ -390,9 +464,10 @@ public async Task When_MessageIdIs35Characters_Then_ValidationSucceed() ], messageId: validMessageId); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Errors.Should().BeEmpty(); @@ -403,8 +478,9 @@ public async Task When_MessageIdIs35Characters_Then_ValidationSucceed() public async Task When_MessageIdExceed35Characters_Then_ExpectedValidationError() { var invalidMessageId = "123564789123564789123564789123564789_123564789123564789123564789123564789"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("123456789", @@ -414,7 +490,7 @@ public async Task When_MessageIdExceed35Characters_Then_ExpectedValidationError( ], messageId: invalidMessageId); - var messageParser = await ParseMessageAsync(message.Stream); + var messageParser = await ParseMessageAsync(message.Stream, documentFormat); messageParser.ParserResult.Errors.Should().Contain(error => error is InvalidMessageStructure); messageParser.ParserResult.Success.Should().BeFalse(); @@ -423,19 +499,21 @@ public async Task When_MessageIdExceed35Characters_Then_ExpectedValidationError( [Fact] public async Task When_TransactionIdIsLessThen35Characters_Then_ValidationSucceed() { + var documentFormat = DocumentFormat.Ebix; var validTransactionId = "1235647891235647891235647891"; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ (validTransactionId, Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 2, 0, 0), Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Errors.Should().BeEmpty(); @@ -445,18 +523,20 @@ public async Task When_TransactionIdIsLessThen35Characters_Then_ValidationSuccee [Fact] public async Task When_TransactionIdIs35Characters_Then_ValidationSucceed() { + var documentFormat = DocumentFormat.Ebix; var validTransactionId = "12356478912356478912356478912356478"; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ (validTransactionId, Instant.FromUtc(2024, 1, 1, 0, 0), Instant.FromUtc(2024, 1, 2, 0, 0), Resolution.QuarterHourly), ]); - var (incomingMessage, _) = await ParseMessageAsync(message.Stream); + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + documentFormat, CancellationToken.None); result.Errors.Should().BeEmpty(); @@ -467,8 +547,9 @@ public async Task When_TransactionIdIs35Characters_Then_ValidationSucceed() public async Task When_TransactionIdExceed35Characters_Then_ExpectedValidationError() { var invalidTransactionId = "123564789123564789123564789123564789_123564789123564789123564789123564789"; + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ (invalidTransactionId, @@ -477,7 +558,7 @@ public async Task When_TransactionIdExceed35Characters_Then_ExpectedValidationEr Resolution.QuarterHourly), ]); - var messageParser = await ParseMessageAsync(message.Stream); + var messageParser = await ParseMessageAsync(message.Stream, documentFormat); messageParser.ParserResult.Errors.Should().Contain(error => error is InvalidMessageStructure); messageParser.ParserResult.Success.Should().BeFalse(); @@ -486,8 +567,9 @@ public async Task When_TransactionIdExceed35Characters_Then_ExpectedValidationEr [Fact] public async Task When_BusinessTypeIsAllowed_Then_ValidationSucceed() { + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", @@ -495,9 +577,10 @@ public async Task When_BusinessTypeIsAllowed_Then_ValidationSucceed() ], businessType: "23"); - var messageParser = await ParseMessageAsync(message.Stream); + var messageParser = await ParseMessageAsync(message.Stream, documentFormat); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + documentFormat, CancellationToken.None); result.Should().NotBeNull(); @@ -507,8 +590,9 @@ public async Task When_BusinessTypeIsAllowed_Then_ValidationSucceed() [Fact] public async Task When_BusinessTypeIsNotAllowed_Then_ExpectedValidationError() { + var documentFormat = DocumentFormat.Ebix; var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( - DocumentFormat.Ebix, + documentFormat, _actorIdentity.ActorNumber, [ ("555555555", @@ -516,20 +600,28 @@ public async Task When_BusinessTypeIsNotAllowed_Then_ExpectedValidationError() ], businessType: "27"); - var messageParser = await ParseMessageAsync(message.Stream); + var messageParser = await ParseMessageAsync(message.Stream, documentFormat); messageParser.ParserResult.Errors.Should().Contain(error => error is InvalidMessageStructure); messageParser.ParserResult.Success.Should().BeFalse(); } private async Task<(MeteredDataForMeasurementPointMessage? IncomingMessage, IncomingMarketMessageParserResult ParserResult)> ParseMessageAsync( - Stream message) + Stream message, + DocumentFormat documentFormat) { - var messageParser = await _marketMessageParser.ParseAsync( - new IncomingMarketMessageStream(message), - DocumentFormat.Ebix, + var incomingMarketMessageStream = new IncomingMarketMessageStream(message); + if (_messageParsers.TryGetValue((IncomingDocumentType.MeteredDataForMeasurementPoint, documentFormat), out var messageParser)) + { + var result = await messageParser.ParseAsync(incomingMarketMessageStream, CancellationToken.None).ConfigureAwait(false); + return (IncomingMessage: (MeteredDataForMeasurementPointMessage?)result.IncomingMessage, ParserResult: result); + } + + var messageMarketParser = await _marketMessageParser.ParseAsync( + incomingMarketMessageStream, + documentFormat, IncomingDocumentType.MeteredDataForMeasurementPoint, CancellationToken.None); - return (IncomingMessage: (MeteredDataForMeasurementPointMessage?)messageParser.IncomingMessage, ParserResult: messageParser); + return (IncomingMessage: (MeteredDataForMeasurementPointMessage?)messageMarketParser.IncomingMessage, ParserResult: messageMarketParser); } private async Task StoreMessageIdForActorAsync(string messageId, string senderActorNumber) diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages/MeteredDataForMeasurementPoint.xml b/source/IncomingMessages.IntegrationTests/IncomingMessages/MeteredDataForMeasurementPoint.xml new file mode 100644 index 0000000000..ebd125fa46 --- /dev/null +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages/MeteredDataForMeasurementPoint.xml @@ -0,0 +1,53 @@ + + + C1876453 + E66 + E23 + 23 + 5799999933318 + MDR + 5790001330552 + DGL + 2022-12-17T09:30:47Z + + C1876456 + C1875000 + 579999993331812345 + E17 + 2022-12-17T07:30:00Z + 8716867000030 + KWH + + PT1H + + 2022-08-15T22:00Z + 2022-08-15T04:00Z + + + 1 + 242 + A03 + + + 2 + 242 + + + 3 + 222 + + + 4 + 202 + + + 5 + 191 + + + 6 + A02 + + + + \ No newline at end of file diff --git a/source/IncomingMessages.IntegrationTests/MessageParsers/GivenNewDocumentTypeTests.cs b/source/IncomingMessages.IntegrationTests/MessageParsers/GivenNewDocumentTypeTests.cs index 48f250d1fe..0123fc4e3e 100644 --- a/source/IncomingMessages.IntegrationTests/MessageParsers/GivenNewDocumentTypeTests.cs +++ b/source/IncomingMessages.IntegrationTests/MessageParsers/GivenNewDocumentTypeTests.cs @@ -33,7 +33,6 @@ public class GivenNewIncomingDocumentTypeTests : IncomingMessagesTestBase (IncomingDocumentType.B2CRequestWholesaleSettlement, DocumentFormat.Ebix), (IncomingDocumentType.B2CRequestAggregatedMeasureData, DocumentFormat.Ebix), // TODO: Remove when implementing parsers for CIM - (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Xml), (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Json), }; @@ -73,13 +72,23 @@ public async Task When_ParsingMessageOfDocumentTypeAndFormat_Then_ExpectedMessag { // Arrange var marketMessageParser = GetService(); + var messageParsers = GetService>(); // Act - var act = () => marketMessageParser.ParseAsync( - new IncomingMarketMessageStream(new MemoryStream()), - documentFormat, - incomingDocumentType, - CancellationToken.None); + var act = async () => + { + var incomingMarketMessageStream = new IncomingMarketMessageStream(new MemoryStream()); + if (messageParsers.TryGetValue((incomingDocumentType, documentFormat), out var messageParser)) + { + return await messageParser.ParseAsync(incomingMarketMessageStream, CancellationToken.None).ConfigureAwait(false); + } + + return await marketMessageParser.ParseAsync( + incomingMarketMessageStream, + documentFormat, + incomingDocumentType, + CancellationToken.None); + }; // Assert if (_unsupportedCombinationsOfIncomingDocumentTypeAndDocumentFormat.Contains((incomingDocumentType, documentFormat))) diff --git a/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/RequestAggregatedMeasureData/IncomingMessageReceiverTests.cs b/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/RequestAggregatedMeasureData/IncomingMessageReceiverTests.cs index 26ce9d58d5..b0aa58a122 100644 --- a/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/RequestAggregatedMeasureData/IncomingMessageReceiverTests.cs +++ b/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/RequestAggregatedMeasureData/IncomingMessageReceiverTests.cs @@ -96,6 +96,7 @@ public async Task Receiver_id_must_be_unknown() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is InvalidReceiverId); @@ -113,6 +114,7 @@ public async Task Receiver_id_must_be_Datahub() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(result.Errors, error => error is InvalidReceiverId); @@ -129,6 +131,7 @@ public async Task Receiver_role_must_be_calculation_responsible() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(result.Errors, error => error is InvalidReceiverRole); @@ -165,6 +168,7 @@ public async Task Sender_id_must_match_the_organization_of_the_current_authentic var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(result.Errors, error => error is AuthenticatedUserDoesNotMatchSenderId); @@ -182,6 +186,7 @@ public async Task Sender_id_does_not_match_the_current_authenticated_user() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is AuthenticatedUserDoesNotMatchSenderId); @@ -198,6 +203,7 @@ public async Task Authenticated_user_must_hold_the_role_type_as_specified_in_mes var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is AuthenticatedUserDoesNotHoldRequiredRoleType); @@ -224,6 +230,7 @@ public async Task var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); // Assert @@ -250,6 +257,7 @@ public async Task Series_must_have_unique_transaction_ids() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is DuplicateTransactionIdDetected); @@ -269,6 +277,7 @@ public async Task Series_can_have_same_transaction_ids_across_senders() var messageParser = await ParseMessageAsync(message); var resultFromFirstMessage = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); // Request from a second sender. @@ -282,6 +291,7 @@ public async Task Series_can_have_same_transaction_ids_across_senders() var messageParser2 = await ParseMessageAsync(message02); var resultFromSecondMessage = await _validateIncomingMessage.ValidateAsync( messageParser2.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(resultFromFirstMessage.Errors, error => error is DuplicateTransactionIdDetected); @@ -303,6 +313,7 @@ public async Task Series_must_have_none_empty_transaction_ids() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is EmptyTransactionId); @@ -319,6 +330,7 @@ public async Task Message_id_must_not_be_empty() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is EmptyMessageId); @@ -338,11 +350,13 @@ public async Task Message_id_may_be_reused_across_senders() var messageParser01 = await ParseMessageAsync(message01); var result01 = await _validateIncomingMessage.ValidateAsync( messageParser01.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); var messageParser02 = await ParseMessageAsync(message02); var result02 = await _validateIncomingMessage.ValidateAsync( messageParser02.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(result01.Errors, error => error is DuplicateMessageIdDetected); @@ -365,6 +379,7 @@ public async Task Message_ids_must_not_exists_for_sender() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.False(result.Success); @@ -383,6 +398,7 @@ public async Task Sender_role_type_for_aggregated_measure_data_must_be_the_role_ var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.DoesNotContain(result.Errors, error => error is SenderRoleTypeIsNotAuthorized); @@ -480,6 +496,7 @@ public async Task Message_id_can_be_shorter_than_36_chars() var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); result.Errors.Should().NotContainItemsAssignableTo(); @@ -503,6 +520,7 @@ public async Task Message_id_cannot_be_longer_than_36_chars() var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); result.Errors.Should().ContainItemsAssignableTo(); @@ -546,6 +564,7 @@ public async Task Transaction_id_can_be_less_than_36_characters() var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); result.Errors.Should().NotContainItemsAssignableTo(); @@ -562,6 +581,7 @@ public async Task Transaction_id_can_be_36_characters() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); result.Errors.Should().NotContainItemsAssignableTo(); @@ -578,6 +598,7 @@ public async Task Transaction_id_must_not_be_more_than_36_characters() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); result.Errors.Should().ContainItemsAssignableTo(); @@ -594,6 +615,7 @@ public async Task Business_type_is_allowed() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.NotNull(result); @@ -611,6 +633,7 @@ public async Task Business_type_is_not_allowed() var messageParser = await ParseMessageAsync(message); var result = await _validateIncomingMessage.ValidateAsync( messageParser.IncomingMessage!, + DocumentFormat.Xml, CancellationToken.None); Assert.Contains(result.Errors, error => error is NotSupportedBusinessType); diff --git a/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/WholesaleServices/IncomingWholesaleServiceTests.cs b/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/WholesaleServices/IncomingWholesaleServiceTests.cs index 24496be079..4909a9c6a5 100644 --- a/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/WholesaleServices/IncomingWholesaleServiceTests.cs +++ b/source/IntegrationTests/Infrastructure.CimMessageAdapter/Messages/WholesaleServices/IncomingWholesaleServiceTests.cs @@ -120,6 +120,7 @@ public async Task Given_AllowedActorRoles_When_Validation_Then_ReturnNoErrors(st // Act var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); // Assert @@ -142,6 +143,7 @@ public async Task Given_RequestWithMessageTypeNotD21_When_Validating_Then_Return // Act var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); // Assert @@ -166,6 +168,7 @@ public async Task Given_RequestWithMessageTypeD11_When_Validating_Then_ReturnNoE // Act var result = await _validateIncomingMessage.ValidateAsync( incomingMessage!, + DocumentFormat.Xml, CancellationToken.None); // Assert diff --git a/source/Tests/CimMessageAdapter/Messages/MeteredDataForMeasurementPointMessageParserTests/MessageParserTests.cs b/source/Tests/CimMessageAdapter/Messages/MeteredDataForMeasurementPointMessageParserTests/MessageParserTests.cs index eefc14d39a..2a07fcfe78 100644 --- a/source/Tests/CimMessageAdapter/Messages/MeteredDataForMeasurementPointMessageParserTests/MessageParserTests.cs +++ b/source/Tests/CimMessageAdapter/Messages/MeteredDataForMeasurementPointMessageParserTests/MessageParserTests.cs @@ -18,6 +18,7 @@ using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors; using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers; using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers; +using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Cim.Xml; using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Ebix; using Energinet.DataHub.EDI.IncomingMessages.Interfaces.Models; using FluentAssertions; @@ -36,7 +37,8 @@ public sealed class MessageParserTests private readonly Dictionary _marketMessageParser = new() { - { DocumentFormat.Ebix, new EbixMessageParser(new EbixSchemaProvider()) }, + { DocumentFormat.Ebix, new MeteredDateForMeasurementPointEbixMessageParser(new EbixSchemaProvider()) }, + { DocumentFormat.Xml, new MeteredDateForMeasurementPointXmlMessageParser(new CimXmlSchemaProvider(new CimXmlSchemas())) }, }; public static TheoryData CreateMessagesWithSingleAndMultipleTransactions() @@ -46,6 +48,8 @@ public static TheoryData CreateMessagesWithSingleAndMult { DocumentFormat.Ebix, CreateBaseEbixMessage("ValidMeteredDataForMeasurementPoint.xml") }, { DocumentFormat.Ebix, CreateBaseEbixMessage("ValidMeteredDataForMeasurementPointWithTwoTransactions.xml") }, { DocumentFormat.Ebix, CreateBaseEbixMessage("ValidPT1HMeteredDataForMeasurementPoint.xml") }, + { DocumentFormat.Xml, CreateBaseXmlMessage("ValidMeteredDataForMeasurementPoint.xml") }, + { DocumentFormat.Xml, CreateBaseXmlMessage("ValidMeteredDataForMeasurementPointWithTwoTransactions.xml") }, }; return data; @@ -57,6 +61,8 @@ public static TheoryData CreateBadMessages() { { DocumentFormat.Ebix, CreateBaseEbixMessage("BadVersionMeteredDataForMeasurementPoint.xml"), nameof(InvalidBusinessReasonOrVersion) }, { DocumentFormat.Ebix, CreateBaseEbixMessage("InvalidMeteredDataForMeasurementPoint.xml"), nameof(InvalidMessageStructure) }, + { DocumentFormat.Xml, CreateBaseXmlMessage("BadVersionMeteredDataForMeasurementPoint.xml"), nameof(InvalidBusinessReasonOrVersion) }, + { DocumentFormat.Xml, CreateBaseXmlMessage("InvalidMeteredDataForMeasurementPoint.xml"), nameof(InvalidMessageStructure) }, }; return data; @@ -93,9 +99,11 @@ public async Task Successfully_parsed(DocumentFormat format, Stream message) series.Resolution.Should() .Match(resolution => resolution == "PT15M" || resolution == "PT1H"); series.StartDateTime.Should() - .Match(startDate => startDate == "2024-06-28T22:00:00Z" || startDate == "2024-06-29T22:00:00Z"); + .Match(startDate => startDate == "2024-06-28T22:00:00Z" || startDate == "2024-06-29T22:00:00Z" + || startDate == "2024-06-28T22:00Z" || startDate == "2024-06-29T22:00Z"); series.EndDateTime.Should() - .Match(endDate => endDate == "2024-06-29T22:00:00Z" || endDate == "2024-06-30T22:00:00Z"); + .Match(endDate => endDate == "2024-06-29T22:00:00Z" || endDate == "2024-06-30T22:00:00Z" + || endDate == "2024-06-29T22:00Z" || endDate == "2024-06-30T22:00Z"); series.ProductNumber.Should().Be("8716867000030"); series.ProductUnitType.Should().Be("KWH"); series.MeteringPointType.Should().Be("E18"); @@ -109,7 +117,7 @@ public async Task Successfully_parsed(DocumentFormat format, Stream message) energyObservation.Position.Should().Be(position.ToString()); energyObservation.EnergyQuantity.Should().NotBeEmpty(); energyObservation.QuantityQuality.Should() - .Match(quality => quality == "E01" || quality == "56"); + .Match(quality => quality == "E01" || quality == "56" || quality == "A03"); position++; } } @@ -139,4 +147,15 @@ private static MemoryStream CreateBaseEbixMessage(string fileName) return stream; } + + private static MemoryStream CreateBaseXmlMessage(string fileName) + { + var xmlDocument = XDocument.Load( + $"{PathToMessages}xml{SubPath}{fileName}"); + + var stream = new MemoryStream(); + xmlDocument.Save(stream); + + return stream; + } } diff --git a/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/BadVersionMeteredDataForMeasurementPoint.xml b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/BadVersionMeteredDataForMeasurementPoint.xml new file mode 100644 index 0000000000..a1507a6f6c --- /dev/null +++ b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/BadVersionMeteredDataForMeasurementPoint.xml @@ -0,0 +1,60 @@ + + + C1876453 + E66 + E23 + 23 + 5799999933318 + MDR + 5790001330552 + DGL + 2022-12-17T09:30:47Z + + C1876456 + C1875000 + + 579999993331812345 + + E17 + + 2022-12-17T07:30:00Z + + 8716867000030 + KWH + + PT1H + + 2022-08-15T22:00Z + 2022-08-15T04:00Z + + + 1 + 242 + A03 + + + + 2 + 242 + + + + 3 + 222 + + + 4 + 202 + + + 5 + 191 + + + 6 + A02 + + + + + \ No newline at end of file diff --git a/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/InvalidMeteredDataForMeasurementPoint.xml b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/InvalidMeteredDataForMeasurementPoint.xml new file mode 100644 index 0000000000..d90133fd11 --- /dev/null +++ b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/InvalidMeteredDataForMeasurementPoint.xml @@ -0,0 +1,60 @@ + + + C1876453 + E66 + E23 + 23 + 5799999933318 + MDR + 5790001330552 + DGL + 2022-12-17T09:30:47Z + + C1876456 + C1875000 + + 579999993331812345 + + E17 + + 2022-12-17T07:30:00Z + + 8716867000030 + KWH + + PT1H + + 2022-08-15T22:00Z + 2022-08-15T04:00Z + + + 1 + 242 + A03 + + + + 2 + 242 + + + + 3 + 222 + + + 4 + 202 + + + 5 + 191 + + + 6 + A02 + + + + + \ No newline at end of file diff --git a/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPoint.xml b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPoint.xml new file mode 100644 index 0000000000..d61e2ef95d --- /dev/null +++ b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPoint.xml @@ -0,0 +1,58 @@ + + + 111131835 + E66 + E23 + 23 + 5790001330552 + MDR + 5790000432752 + DGL + 2024-07-30T07:30:54Z + + 4413675032_5080574373 + C1875000 + 571313000000002000 + E18 + 2022-12-17T07:30:00Z + 8716867000030 + KWH + + PT1H + + 2024-06-28T22:00Z + 2024-06-29T22:00Z + + + 1 + 56 + A03 + + + 2 + 56 + A03 + + + 3 + 56 + A03 + + + 4 + 56 + A03 + + + 5 + 56 + A03 + + + 6 + 56 + A03 + + + + \ No newline at end of file diff --git a/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPointWithTwoTransactions.xml b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPointWithTwoTransactions.xml new file mode 100644 index 0000000000..8d21737de0 --- /dev/null +++ b/source/Tests/CimMessageAdapter/Messages/xml/MeteredDataForMeasurementPoint/ValidMeteredDataForMeasurementPointWithTwoTransactions.xml @@ -0,0 +1,104 @@ + + + 111131835 + E66 + E23 + 23 + 5790001330552 + MDR + 5790000432752 + DGL + 2024-07-30T07:30:54Z + + 4413675032_5080574373 + C1875000 + 571313000000002000 + E18 + 2022-12-17T07:30:00Z + 8716867000030 + KWH + + PT1H + + 2024-06-28T22:00Z + 2024-06-29T22:00Z + + + 1 + 56 + A03 + + + 2 + 56 + A03 + + + 3 + 56 + A03 + + + 4 + 56 + A03 + + + 5 + 56 + A03 + + + 6 + 56 + A03 + + + + + 4413675032_5080574374 + C1875000 + 571313000000002000 + E18 + 2022-12-17T07:30:00Z + 8716867000030 + KWH + + PT1H + + 2024-06-29T22:00Z + 2024-06-30T22:00Z + + + 1 + 56 + A03 + + + 2 + 56 + A03 + + + 3 + 56 + A03 + + + 4 + 56 + A03 + + + 5 + 56 + A03 + + + 6 + 56 + A03 + + + + \ No newline at end of file diff --git a/source/Tests/Tests.csproj b/source/Tests/Tests.csproj index 8824e4ce63..313b267439 100644 --- a/source/Tests/Tests.csproj +++ b/source/Tests/Tests.csproj @@ -91,6 +91,18 @@ limitations under the License. Always + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -123,7 +135,7 @@ limitations under the License. PreserveNewest - + PreserveNewest