diff --git a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs index f54feb791b..6eccc9c000 100644 --- a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs +++ b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs @@ -171,12 +171,9 @@ private async Task> CheckTransactionIdsAsyn { var transactionId = series.TransactionId; - var errorsForSeries = await CheckTransactionIdAsync( + var errorsForSeries = CheckTransactionId( transactionId, - message.SenderNumber, - transactionIdsToBeStored, - cancellationToken) - .ConfigureAwait(false); + transactionIdsToBeStored); if (errorsForSeries is null) { @@ -188,34 +185,28 @@ private async Task> CheckTransactionIdsAsyn } } + var duplicatedTransactionIds = await transactionIdRepository + .GetDuplicatedTransactionIdsAsync(message.SenderNumber, transactionIdsToBeStored, cancellationToken) + .ConfigureAwait(false); + foreach (var duplicatedTransactionId in duplicatedTransactionIds) + { + errors.Add(new DuplicateTransactionIdDetected(duplicatedTransactionId)); + } + return errors; } - private async Task CheckTransactionIdAsync( + private ValidationError? CheckTransactionId( string transactionId, - string senderNumber, - IReadOnlyCollection transactionIdsToBeStored, - CancellationToken cancellationToken) + IReadOnlyCollection transactionIdsToBeStored) { return transactionId switch { _ when string.IsNullOrEmpty(transactionId) => new EmptyTransactionId(), _ when transactionId.Length > MaxTransactionIdLength => new InvalidTransactionIdSize(transactionId), - _ when await TransactionIdIsDuplicatedAsync(senderNumber, transactionId, cancellationToken) - .ConfigureAwait(false) => new DuplicateTransactionIdDetected(transactionId), _ when transactionIdsToBeStored.Contains(transactionId) => new DuplicateTransactionIdDetected(transactionId), _ => null, }; } - - private async Task TransactionIdIsDuplicatedAsync( - string senderNumber, - string transactionId, - CancellationToken cancellationToken) - { - return await transactionIdRepository - .TransactionIdExistsAsync(senderNumber, transactionId, cancellationToken) - .ConfigureAwait(false); - } } diff --git a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs index 584da8d517..5c48bc8098 100644 --- a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs +++ b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs @@ -20,12 +20,12 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Repositories.Tra public interface ITransactionIdRepository { /// - /// Checks if is already registered by the sender + /// Returns a list of existing if they already is registered by the sender /// /// - /// + /// /// - Task TransactionIdExistsAsync(string senderId, string transactionId, CancellationToken cancellationToken); + Task> GetDuplicatedTransactionIdsAsync(string senderId, IReadOnlyCollection transactionIds, CancellationToken cancellationToken); /// /// Store transaction ids for the specified sender diff --git a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs index e5785c196c..5cb215e0f4 100644 --- a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs +++ b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs @@ -26,15 +26,17 @@ public TransactionIdRepository(IncomingMessagesContext incomingMessagesContext) _incomingMessagesContext = incomingMessagesContext; } - public async Task TransactionIdExistsAsync( + public async Task> GetDuplicatedTransactionIdsAsync( string senderId, - string transactionId, + IReadOnlyCollection transactionIds, CancellationToken cancellationToken) { - var transaction = await GetTransactionFromDbAsync(senderId, transactionId, cancellationToken).ConfigureAwait(false) - ?? GetTransactionFromInMemoryCollection(senderId, transactionId); + var duplicatedTransactionIdsForSender = await GetDuplicatedTransactionsFromDbAsync(senderId, transactionIds, cancellationToken) + .ConfigureAwait(false); + if (!transactionIds.Any()) + duplicatedTransactionIdsForSender = GetDuplicatedTransactionsFromInMemoryCollection(senderId, transactionIds); - return transaction != null; + return duplicatedTransactionIdsForSender.Select(x => x.TransactionId).ToList(); } public async Task AddAsync( @@ -50,19 +52,26 @@ public async Task AddAsync( } } - private TransactionIdForSender? GetTransactionFromInMemoryCollection(string senderId, string transactionId) + private IReadOnlyList GetDuplicatedTransactionsFromInMemoryCollection( + string senderId, + IReadOnlyCollection transactionIds) { return _incomingMessagesContext.TransactionIdForSenders.Local - .FirstOrDefault(x => x.TransactionId == transactionId && x.SenderId == senderId); + .Where( + transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId) + && transactionIdForSender.SenderId == senderId) + .ToList(); } - private async Task GetTransactionFromDbAsync(string senderId, string transactionId, CancellationToken cancellationToken) + private async Task> GetDuplicatedTransactionsFromDbAsync( + string senderId, + IReadOnlyCollection transactionIds, + CancellationToken cancellationToken) { return await _incomingMessagesContext.TransactionIdForSenders - .FirstOrDefaultAsync( - transactionIdForSender => transactionIdForSender.TransactionId == transactionId - && transactionIdForSender.SenderId == senderId, - cancellationToken) + .Where(transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId) + && transactionIdForSender.SenderId == senderId) + .ToListAsync(cancellationToken) .ConfigureAwait(false); } } diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs index 009ed970ec..b8a72fc628 100644 --- a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs @@ -157,6 +157,37 @@ public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExcepted result.Errors.Should().Contain(error => error is DuplicateTransactionIdDetected); } + [Fact] + public async Task When_MultipleTransactionsWithSameIdAsExisting_Then_ResultContainExceptedValidationError() + { + var documentFormat = DocumentFormat.Ebix; + var existingTransactionIdForSender = "123456"; + var newTransactionIdForSender = "654321"; + await StoreTransactionIdForActorAsync(existingTransactionIdForSender, _actorIdentity.ActorNumber.Value); + var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( + documentFormat, + _actorIdentity.ActorNumber, + [ + (existingTransactionIdForSender, + Instant.FromUtc(2024, 1, 1, 0, 0), + Instant.FromUtc(2024, 1, 2, 0, 0), + Resolution.QuarterHourly), + (newTransactionIdForSender, + Instant.FromUtc(2024, 1, 1, 0, 0), + Instant.FromUtc(2024, 1, 2, 0, 0), + Resolution.QuarterHourly), + ]); + + 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 DuplicateTransactionIdDetected); + } + [Fact] public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidationError() { @@ -618,6 +649,17 @@ public async Task When_BusinessTypeIsNotAllowed_Then_ExpectedValidationError() throw new NotSupportedException($"No message parser found for message format '{documentFormat}' and document type '{IncomingDocumentType.NotifyValidatedMeasureData}'"); } + private async Task StoreTransactionIdForActorAsync(string existingTransactionIdForSender, string senderActorNumber) + { + var databaseConnectionFactory = GetService(); + using var dbConnection = await databaseConnectionFactory.GetConnectionAndOpenAsync(CancellationToken.None).ConfigureAwait(false); + + await dbConnection.ExecuteAsync( + "INSERT INTO [dbo].[TransactionRegistry] ([TransactionId], [SenderId]) VALUES (@TransactionId, @SenderId)", + new { TransactionId = existingTransactionIdForSender, SenderId = senderActorNumber }) + .ConfigureAwait(false); + } + private async Task StoreMessageIdForActorAsync(string messageId, string senderActorNumber) { var databaseConnectionFactory = GetService(); diff --git a/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs b/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs index 8683f43b4b..a5ab09b00f 100644 --- a/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs +++ b/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics.CodeAnalysis; using System.Text; using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IntegrationTests.Fixtures; using Energinet.DataHub.EDI.OutgoingMessages.IntegrationTests.DocumentAsserters; using Energinet.DataHub.EDI.OutgoingMessages.Interfaces.Models.Peek; +using Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; using FluentAssertions; using FluentAssertions.Execution; using NodaTime; @@ -26,6 +28,10 @@ namespace Energinet.DataHub.EDI.IntegrationTests.Behaviours.IncomingRequests; +[SuppressMessage( + "StyleCop.CSharp.ReadabilityRules", + "SA1118:Parameter should not span multiple lines", + Justification = "Readability")] public sealed class GivenMeteredDataForMeasurementPointTests( IntegrationTestFixture integrationTestFixture, ITestOutputHelper testOutputHelper) @@ -50,15 +56,18 @@ public async Task When_ActorPeeksAllMessages_Then_ReceivesOneDocumentWithCorrect var transactionIdPrefix = Guid.NewGuid().ToString("N"); + var transactionId1 = $"{transactionIdPrefix}-1"; + var transactionId2 = $"{transactionIdPrefix}-2"; + await GivenReceivedMeteredDataForMeasurementPoint( documentFormat: DocumentFormat.Xml, senderActorNumber: currentActor.ActorNumber, [ - ($"{transactionIdPrefix}-1", + (transactionId1, InstantPattern.General.Parse("2024-11-28T13:51:42Z").Value, InstantPattern.General.Parse("2024-11-29T09:15:28Z").Value, Resolution.Hourly), - ($"{transactionIdPrefix}-2", + (transactionId2, InstantPattern.General.Parse("2024-11-24T18:51:58Z").Value, InstantPattern.General.Parse("2024-11-25T03:39:45Z").Value, Resolution.QuarterHourly), @@ -75,12 +84,111 @@ await GivenReceivedMeteredDataForMeasurementPoint( peekFormat); // Assert + peekResults.Should().HaveCount(2); + foreach (var peekResultDto in peekResults) { + // This is not pretty, but it works for now + var foo = new StreamReader(peekResultDto.Bundle); + var content = await foo.ReadToEndAsync(); + var isTransOne = content.Contains(transactionId1); + peekResultDto.Bundle.Position = 0; + await ThenNotifyValidatedMeasureDataDocumentIsCorrect( peekResultDto.Bundle, peekFormat, - new NotifyValidatedMeasureDataDocumentAssertionInput()); + new NotifyValidatedMeasureDataDocumentAssertionInput( + new RequiredHeaderDocumentFields( + "E23", + "8100000000115", + "A10", + "5790001330552", + "A10", + "DGL", + "DDQ", + "2024-07-01T14:57:09Z"), + new OptionalHeaderDocumentFields( + "23", + [ + isTransOne + ? new AssertSeriesDocumentFieldsInput( + 1, + new RequiredSeriesFields( + TransactionId.From(string.Join(string.Empty, transactionId1.Reverse())), + "579999993331812345", + "A10", + "E17", + "KWH", + new RequiredPeriodDocumentFields( + "PT1H", + "2024-11-28T13:51Z", + "2024-11-29T09:15Z", + Enumerable.Range(1, 24) + .Select( + i => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(i), + new OptionalPointDocumentFields("A03", 1000 + i))) + .ToList())), + new OptionalSeriesFields( + transactionId1, + "2022-12-17T09:30:47Z", + null, + null, + "8716867000030")) + : new AssertSeriesDocumentFieldsInput( + 1, + new RequiredSeriesFields( + TransactionId.From(string.Join(string.Empty, transactionId2.Reverse())), + "579999993331812345", + "A10", + "E17", + "KWH", + new RequiredPeriodDocumentFields( + "PT15M", + "2024-11-24T18:51Z", + "2024-11-25T03:39Z", + Enumerable.Range(1, 96) + .Select( + i => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(i), + new OptionalPointDocumentFields("A03", 1000 + i))) + .ToList())), + new OptionalSeriesFields( + transactionId2, + "2022-12-17T09:30:47Z", + null, + null, + "8716867000030")), + ]))); } } + + [Theory] + [MemberData(nameof(PeekFormats))] + public async Task AndGiven_MessageIsEmpty_When_ActorPeeksAllMessages_Then_ReceivesNoMessages( + DocumentFormat peekFormat) + { + // Arrange + var senderSpy = CreateServiceBusSenderSpy(); + var currentActor = (ActorNumber: ActorNumber.Create("1111111111111"), ActorRole: ActorRole.GridAccessProvider); + + GivenNowIs(Instant.FromUtc(2024, 7, 1, 14, 57, 09)); + GivenAuthenticatedActorIs(currentActor.ActorNumber, currentActor.ActorRole); + + await GivenReceivedMeteredDataForMeasurementPoint( + documentFormat: DocumentFormat.Xml, + senderActorNumber: currentActor.ActorNumber, + []); + + await WhenMeteredDataForMeasurementPointProcessIsInitialized(senderSpy.LatestMessage!); + + // Act + var peekResults = await WhenActorPeeksAllMessages( + ActorNumber.Create("8100000000115"), + ActorRole.EnergySupplier, + peekFormat); + + // Assert + peekResults.Should().BeEmpty(); + } } diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs index 29c186afc4..70112431d9 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs @@ -64,18 +64,18 @@ private Document ParseFrom(OutgoingMessageHeader header, IReadOnlyCollection(); + var meteredDataForMeasurementSeries = new Collection(); foreach (var activityRecord in transactions.Select(t => _parser.From(t))) { - meteredDataForMeasurementPoints.Add( - new MeteredDataForMeasurementPoint( + meteredDataForMeasurementSeries.Add( + new MeteredDataForMeasurementSeries( activityRecord.TransactionId.Value, activityRecord.MarketEvaluationPointNumber, activityRecord.MarketEvaluationPointType, activityRecord.OriginalTransactionIdReferenceId?.Value, activityRecord.Product, activityRecord.QuantityMeasureUnit.Code, - activityRecord.RegistrationDateTime.ToString(), + activityRecord.RegistrationDateTime?.ToString(), new Period( activityRecord.Resolution.Code, new TimeInterval( @@ -100,7 +100,7 @@ private Document ParseFrom(OutgoingMessageHeader header, IReadOnlyCollection meteredDataForMeasurementPoints) + IReadOnlyCollection meteredDataForMeasurementSeries) { [JsonPropertyName("mRID")] public string MessageId { get; init; } = messageId; @@ -150,17 +150,18 @@ internal class MeteredDateForMeasurementPoint( public ValueObject Type { get; init; } = ValueObject.Create(typeCode); [JsonPropertyName("Series")] - public IReadOnlyCollection MeteredDataForMeasurementPoints { get; init; } = meteredDataForMeasurementPoints; + public IReadOnlyCollection MeteredDataForMeasurementSeries { get; init; } = + meteredDataForMeasurementSeries; } -internal class MeteredDataForMeasurementPoint( +internal class MeteredDataForMeasurementSeries( string transactionId, string marketEvaluationPointNumber, string marketEvaluationPointType, string? originalTransactionIdReferenceId, - string product, + string? product, string quantityMeasureUnit, - string registrationDateTime, + string? registrationDateTime, Period period) { [JsonPropertyName("mRID")] @@ -177,13 +178,15 @@ internal class MeteredDataForMeasurementPoint( public string? OriginalTransactionIdReferenceId { get; init; } = originalTransactionIdReferenceId; //TODO: what does this field represent? [JsonPropertyName("product")] - public string Product { get; init; } = product; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Product { get; init; } = product; [JsonPropertyName("quantity_Measure_Unit.name")] public ValueObject QuantityMeasureUnit { get; init; } = ValueObject.Create(quantityMeasureUnit); [JsonPropertyName("registration_DateAndOrTime.dateTime")] - public string RegistrationDateTime { get; init; } = registrationDateTime; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RegistrationDateTime { get; init; } = registrationDateTime; [JsonPropertyName("Period")] public Period Period { get; init; } = period; diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs index 4de6b1db6f..f8f72049f3 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs @@ -40,20 +40,43 @@ protected override async Task WriteMarketActivityRecordsAsync( XNamespace @namespace = "urn:ediel.org:measure:notifyvalidatedmeasuredata:0:1"; foreach (var activityRecord in ParseFrom(marketActivityPayloads)) { - var seriesElement = new XElement( - @namespace + "Series", - new XElement(@namespace + "mRID", activityRecord.TransactionId.Value), - new XElement( + var seriesElement = new XElement(@namespace + "Series"); + seriesElement.Add(new XElement(@namespace + "mRID", activityRecord.TransactionId.Value)); + + if (activityRecord.OriginalTransactionIdReferenceId is not null) + { + seriesElement.Add( + new XElement( @namespace + "originalTransactionIDReference_Series.mRID", - activityRecord.OriginalTransactionIdReferenceId?.Value), + activityRecord.OriginalTransactionIdReferenceId?.Value)); + } + + seriesElement.Add( new XElement( @namespace + "marketEvaluationPoint.mRID", new XAttribute("codingScheme", "A10"), - activityRecord.MarketEvaluationPointNumber), - new XElement(@namespace + "marketEvaluationPoint.type", activityRecord.MarketEvaluationPointType), - new XElement(@namespace + "registration_DateAndOrTime.dateTime", activityRecord.RegistrationDateTime), - new XElement(@namespace + "product", activityRecord.Product), - new XElement(@namespace + "quantity_Measure_Unit.name", activityRecord.QuantityMeasureUnit.Code), + activityRecord.MarketEvaluationPointNumber)); + + seriesElement.Add( + new XElement(@namespace + "marketEvaluationPoint.type", activityRecord.MarketEvaluationPointType)); + + if (activityRecord.RegistrationDateTime is not null) + { + seriesElement.Add( + new XElement( + @namespace + "registration_DateAndOrTime.dateTime", + activityRecord.RegistrationDateTime)); + } + + if (activityRecord.Product is not null) + { + seriesElement.Add(new XElement(@namespace + "product", activityRecord.Product)); + } + + seriesElement.Add( + new XElement(@namespace + "quantity_Measure_Unit.name", activityRecord.QuantityMeasureUnit.Code)); + + seriesElement.Add( new XElement( @namespace + "Period", new XElement(@namespace + "resolution", activityRecord.Resolution.Code), diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs index 0497fc9770..60fa8acb05 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs @@ -22,9 +22,9 @@ public sealed record MeteredDateForMeasurementPointMarketActivityRecord( string MarketEvaluationPointNumber, string MarketEvaluationPointType, TransactionId? OriginalTransactionIdReferenceId, - string Product, + string? Product, MeasurementUnit QuantityMeasureUnit, - Instant RegistrationDateTime, + Instant? RegistrationDateTime, Resolution Resolution, Instant StartedDateTime, Instant EndedDateTime, diff --git a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs index fffd5a879a..10fc54eb03 100644 --- a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs +++ b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs @@ -52,6 +52,55 @@ public static async Task AssertCorrectDocumentAsync( _ => throw new ArgumentOutOfRangeException(nameof(documentFormat), documentFormat, null), }; + var requiredHeaderDocumentFields = assertionInput.RequiredHeaderDocumentFields; + + // Required fields + asserter + .MessageIdExists() + .HasBusinessReason(requiredHeaderDocumentFields.BusinessReasonCode) + .HasSenderId( + requiredHeaderDocumentFields.SenderId, + requiredHeaderDocumentFields.SenderScheme) + .HasSenderRole(requiredHeaderDocumentFields.SenderRole) + .HasReceiverId( + requiredHeaderDocumentFields.ReceiverId, + requiredHeaderDocumentFields.ReceiverScheme) + .HasReceiverRole(requiredHeaderDocumentFields.ReceiverRole) + .HasTimestamp(requiredHeaderDocumentFields.Timestamp); + + var optionalHeaderDocumentFields = assertionInput.OptionalHeaderDocumentFields; + + // Optional fields + asserter.HasBusinessSectorType(optionalHeaderDocumentFields.BusinessSectorType); + + foreach (var assertSeriesDocumentFieldsInput in optionalHeaderDocumentFields.AssertSeriesDocumentFieldsInput) + { + var (seriesIndex, requiredSeriesFields, optionalSeriesFields) = assertSeriesDocumentFieldsInput; + + // Required series fields + asserter + .HasTransactionId(seriesIndex, requiredSeriesFields.TransactionId) + .HasMeteringPointNumber( + seriesIndex, + requiredSeriesFields.MeteringPointNumber, + requiredSeriesFields.MeteringPointScheme) + .HasMeteringPointType(seriesIndex, requiredSeriesFields.MeteringPointType) + .HasQuantityMeasureUnit(seriesIndex, requiredSeriesFields.QuantityMeasureUnit) + // Required period fields + .HasResolution(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.Resolution) + .HasStartedDateTime(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.StartedDateTime) + .HasEndedDateTime(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.EndedDateTime) + .HasPoints(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.Points.ToList().AsReadOnly()); + + // Optional series fields + asserter + .HasOriginalTransactionIdReferenceId(seriesIndex, optionalSeriesFields.OriginalTransactionIdReferenceId) + .HasRegistrationDateTime(seriesIndex, optionalSeriesFields.RegistrationDateTime) + .HasProduct(seriesIndex, optionalSeriesFields.Product) + .HasInDomain(seriesIndex, optionalSeriesFields.InDomain) + .HasOutDomain(seriesIndex, optionalSeriesFields.OutDomain); + } + await asserter .DocumentIsValidAsync(); } diff --git a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs index 31fb059832..b29584be35 100644 --- a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs +++ b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs @@ -12,8 +12,51 @@ // 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.Tests.Infrastructure.OutgoingMessages.RSM012; + namespace Energinet.DataHub.EDI.OutgoingMessages.IntegrationTests.DocumentAsserters; -public class NotifyValidatedMeasureDataDocumentAssertionInput -{ -} +public readonly record struct RequiredHeaderDocumentFields( + string BusinessReasonCode, + string ReceiverId, + string ReceiverScheme, + string SenderId, + string SenderScheme, + string SenderRole, + string ReceiverRole, + string Timestamp); + +public sealed record OptionalHeaderDocumentFields( + string? BusinessSectorType, + IReadOnlyCollection AssertSeriesDocumentFieldsInput); + +public sealed record NotifyValidatedMeasureDataDocumentAssertionInput( + RequiredHeaderDocumentFields RequiredHeaderDocumentFields, + OptionalHeaderDocumentFields OptionalHeaderDocumentFields); + +public sealed record AssertSeriesDocumentFieldsInput( + int SeriesIndex, + RequiredSeriesFields RequiredSeriesFields, + OptionalSeriesFields OptionalSeriesFields); + +public sealed record RequiredSeriesFields( + TransactionId TransactionId, + string MeteringPointNumber, + string MeteringPointScheme, + string MeteringPointType, + string QuantityMeasureUnit, + RequiredPeriodDocumentFields RequiredPeriodDocumentFields); + +public sealed record OptionalSeriesFields( + string? OriginalTransactionIdReferenceId, + string? RegistrationDateTime, + string? InDomain, + string? OutDomain, + string? Product); + +public sealed record RequiredPeriodDocumentFields( + string Resolution, + string StartedDateTime, + string EndedDateTime, + IReadOnlyCollection Points); diff --git a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs index 95ffd47a76..ca92d02645 100644 --- a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs +++ b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs @@ -63,10 +63,10 @@ await _outgoingMessagesClient.EnqueueAndCommitAsync( BusinessReason.FromCode(marketMessage.BusinessReason), MessageId.Create(marketMessage.MessageId), new MeteredDataForMeasurementPointMessageSeriesDto( - TransactionId.From(series.TransactionId), + TransactionId.From(string.Join(string.Empty, series.TransactionId.Reverse())), series.MeteringPointLocationId!, series.MeteringPointType!, - null, + TransactionId.From(series.TransactionId), series.ProductNumber!, MeasurementUnit.FromCode(series.ProductUnitType!), InstantPattern.General.Parse(marketMessage.CreatedAt).Value, diff --git a/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs b/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs index a12626dded..46efe7bdfd 100644 --- a/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs +++ b/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs @@ -49,4 +49,18 @@ public MeteredDateForMeasurementPointMarketActivityRecord BuildMeteredDataForMea SampleData.EndedDateTime, SampleData.Points); } + + public MeteredDateForMeasurementPointMarketActivityRecord BuildMinimalMeteredDataForMeasurementPoint() => + new( + SampleData.TransactionId, + SampleData.MeteringPointNumber, + SampleData.MeteringPointType, + null, + null, + SampleData.QuantityMeasureUnit, + null, + SampleData.Resolution, + SampleData.StartedDateTime, + SampleData.EndedDateTime, + SampleData.MinimalPoints); } diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs index 6f68b9e5b3..99bb6666cc 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs @@ -15,7 +15,6 @@ using System.Text.Json; using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IncomingMessages.Domain.Schemas.Cim.Json; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; using FluentAssertions; using Json.Schema; using Xunit; @@ -36,9 +35,9 @@ public AssertMeteredDateForMeasurementPointJsonDocument(Stream documentStream) Assert.Equal("E66", _root.GetProperty("type").GetProperty("value").ToString()); } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId) + public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() { - Assert.Equal(expectedMessageId, _root.GetProperty("mRID").ToString()); + _root.TryGetProperty("mRID", out _).Should().BeTrue("property 'mRID' should be present"); return this; } @@ -80,93 +79,291 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType( + string? expectedBusinessSectorType) { - Assert.Equal(expectedTransactionId.Value, FirstTimeSeriesElement().GetProperty("mRID").GetString()); + if (expectedBusinessSectorType is null) + { + _root + .TryGetProperty("businessSector.type", out _) + .Should() + .BeFalse("property 'businessSector.type' should not be present"); + + return this; + } + + _root + .GetProperty("businessSector.type") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedBusinessSectorType); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId) + { + Assert.Equal(expectedTransactionId.Value, GetTimeSeriesElement(seriesIndex).GetProperty("mRID").GetString()); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, string expectedMeteringPointNumber, string expectedSchemeCode) { - Assert.Equal(expectedMeteringPointNumber, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.mRID").GetProperty("value").GetString()); - Assert.Equal(expectedSchemeCode, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.mRID").GetProperty("codingScheme").GetString()); + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.mRID") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedMeteringPointNumber); + + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.mRID") + .GetProperty("codingScheme") + .GetString() + .Should() + .Be(expectedSchemeCode); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType) { - Assert.Equal(expectedMeteringPointType, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.type").GetProperty("value").GetString()); + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.type") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedMeteringPointType); + return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, string? expectedOriginalTransactionIdReferenceId) { - Assert.Equal(expectedOriginalTransactionIdReferenceId, FirstTimeSeriesElement().GetProperty("originalTransactionIDReference_Series.mRID").GetString()); + if (expectedOriginalTransactionIdReferenceId is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("originalTransactionIDReference_Series.mRID", out _) + .Should() + .BeFalse("property 'originalTransactionIDReference_Series.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("originalTransactionIDReference_Series.mRID") + .GetString() + .Should() + .Be(expectedOriginalTransactionIdReferenceId); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct) + { + if (expectedProduct is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("product", out _) + .Should() + .BeFalse("property 'product' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex).GetProperty("product").GetString().Should().Be(expectedProduct); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit) + { + GetTimeSeriesElement(seriesIndex) + .GetProperty("quantity_Measure_Unit.name") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedQuantityMeasureUnit); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime) { - Assert.Equal(expectedProduct, FirstTimeSeriesElement().GetProperty("product").GetString()); + if (expectedRegistrationDateTime is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("registration_DateAndOrTime.dateTime", out _) + .Should() + .BeFalse("property 'registration_DateAndOrTime.dateTime' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("registration_DateAndOrTime.dateTime") + .GetString() + .Should() + .Be(expectedRegistrationDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution( + int seriesIndex, + string expectedResolution) { - Assert.Equal(expectedQuantityMeasureUnit, FirstTimeSeriesElement().GetProperty("quantity_Measure_Unit.name").GetProperty("value").GetString()); + GetTimeSeriesPeriodElement(seriesIndex).GetProperty("resolution").GetString().Should().Be(expectedResolution); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime) { - Assert.Equal(expectedRegistrationDateTime, FirstTimeSeriesElement().GetProperty("registration_DateAndOrTime.dateTime").GetString()); + GetTimeSeriesPeriodElement(seriesIndex) + .GetProperty("timeInterval") + .GetProperty("start") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedStartedDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime) { - Assert.Equal(expectedResolution, FirstTimeSeriesPeriodElement().GetProperty("resolution").GetString()); + GetTimeSeriesPeriodElement(seriesIndex) + .GetProperty("timeInterval") + .GetProperty("end") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedEndedDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain(int seriesIndex, string? expectedInDomain) { - Assert.Equal(expectedStartedDateTime, FirstTimeSeriesPeriodElement().GetProperty("timeInterval").GetProperty("start").GetProperty("value").GetString()); + if (expectedInDomain is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("in_Domain.mRID", out _) + .Should() + .BeFalse("property 'in_Domain.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("in_Domain.mRID") + .GetString() + .Should() + .Be(expectedInDomain); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain(int seriesIndex, string? expectedOutDomain) { - Assert.Equal(expectedEndedDateTime, FirstTimeSeriesPeriodElement().GetProperty("timeInterval").GetProperty("end").GetProperty("value").GetString()); + if (expectedOutDomain is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("out_Domain.mRID", out _) + .Should() + .BeFalse("property 'out_Domain.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("out_Domain.mRID") + .GetString() + .Should() + .Be(expectedOutDomain); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints) { - var points = FirstTimeSeriesPeriodElement().GetProperty("Point").EnumerateArray().ToList(); - Assert.Equal(expectedPoints.Count, points.Count); + var points = GetTimeSeriesPeriodElement(seriesIndex).GetProperty("Point").EnumerateArray().ToList(); + + points.Should().HaveCount(expectedPoints.Count); for (var i = 0; i < expectedPoints.Count; i++) { - var expectedPoint = expectedPoints[i]; + var (requiredPointDocumentFields, optionalPointDocumentFields) = expectedPoints[i]; var actualPoint = points[i]; - Assert.Equal(expectedPoint.Position, actualPoint.GetProperty("position").GetProperty("value").GetInt32()); - // Assert.Equal(expectedPoint.Quality, actualPoint.TryGetProperty("quality", out var quality) ? quality.GetProperty("value").GetString() : null); - Assert.Equal(expectedPoint.Quantity, actualPoint.TryGetProperty("quantity", out var quantity) ? quantity.GetInt32() : null); + actualPoint.GetProperty("position") + .GetProperty("value") + .GetInt32() + .Should() + .Be(requiredPointDocumentFields.Position); + + if (optionalPointDocumentFields.Quality != null) + { + actualPoint.GetProperty("quality") + .GetProperty("value") + .GetString() + .Should() + .Be(optionalPointDocumentFields.Quality); + } + else + { + AssertPropertyNotPresent(actualPoint, "quality"); + } + + if (optionalPointDocumentFields.Quantity != null) + { + actualPoint.GetProperty("quantity").GetDecimal().Should().Be(optionalPointDocumentFields.Quantity); + } + else + { + AssertPropertyNotPresent(actualPoint, "quantity"); + } } return this; + + void AssertPropertyNotPresent(JsonElement actualPoint, string propertyName) + { + actualPoint.TryGetProperty(propertyName, out _) + .Should() + .BeFalse($"property '{propertyName}' should not be present"); + } } public async Task DocumentIsValidAsync() { var schema = await _schemas.GetSchemaAsync("NOTIFYVALIDATEDMEASUREDATA", "0", CancellationToken.None).ConfigureAwait(false); var validationResult = IsValid(_document, schema!); - validationResult.IsValid.Should().BeTrue(string.Join("\n", validationResult.Errors)); + validationResult.IsValid.Should() + .BeTrue( + $"the following errors were unexpected:\n\n{string.Join("\n", validationResult.Errors)}\n\nfor the document\n\n{_document.RootElement}"); + return this; } @@ -176,40 +373,33 @@ public async Task Documen var result = schema.Evaluate(jsonDocument, new EvaluationOptions() { OutputFormat = OutputFormat.Hierarchical, }); if (result.IsValid == false) { - errors.Add(FindErrorsForInvalidEvaluation(result)); + errors.AddRange(FindErrorsForInvalidEvaluation(result).Where(e => !string.IsNullOrEmpty(e))); } return (result.IsValid, errors); } - private string FindErrorsForInvalidEvaluation(EvaluationResults result) + private IEnumerable FindErrorsForInvalidEvaluation(EvaluationResults result) { - if (!result.IsValid) + var errors = new List(); + + if (result is { IsValid: false, Errors: not null }) { - foreach (var detail in result.Details) - { - return FindErrorsForInvalidEvaluation(detail); - } + var propertyName = result.InstanceLocation.ToString(); + errors.AddRange(result.Errors.Select(error => $"{propertyName}: {error}")); } - if (!result.HasErrors || result.Errors == null) return string.Empty; - - var propertyName = result.InstanceLocation.ToString(); - foreach (var error in result.Errors) + foreach (var detail in result.Details) { - return $"{propertyName}: {error}"; + errors.AddRange(FindErrorsForInvalidEvaluation(detail)); } - return string.Empty; + return errors; } - private JsonElement FirstTimeSeriesElement() - { - return _root.GetProperty("Series").EnumerateArray().ToList()[0]; - } + private JsonElement GetTimeSeriesElement(int seriesIndex) => + _root.GetProperty("Series").EnumerateArray().ToList()[seriesIndex - 1]; - private JsonElement FirstTimeSeriesPeriodElement() - { - return _root.GetProperty("Series").EnumerateArray().ToList()[0].GetProperty("Period"); - } + private JsonElement GetTimeSeriesPeriodElement(int seriesIndex) => + _root.GetProperty("Series").EnumerateArray().ToList()[seriesIndex - 1].GetProperty("Period"); } diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs index eb2f320a97..110447d7a1 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs @@ -13,7 +13,6 @@ // limitations under the License. using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; using Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.Asserts; using FluentAssertions; @@ -29,7 +28,7 @@ public AssertMeteredDateForMeasurementPointXmlDocument(AssertXmlDocument documen _documentAsserter.HasValue("type", "E66"); } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId) + public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() { _documentAsserter.ElementExists("mRID"); return this; @@ -73,109 +72,203 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType( + string? expectedBusinessSectorType) { - _documentAsserter.HasValue("Series[1]/mRID", expectedTransactionId.Value); + if (expectedBusinessSectorType == null) + { + _documentAsserter.IsNotPresent("businessSector.type"); + } + else + { + _documentAsserter.HasValue("businessSector.type", expectedBusinessSectorType); + } + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId) + { + _documentAsserter.HasValue($"Series[{seriesIndex}]/mRID", expectedTransactionId.Value); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, string expectedMeteringPointNumber, string expectedSchemeCode) { - _documentAsserter.HasValue("Series[1]/marketEvaluationPoint.mRID", expectedMeteringPointNumber); + _documentAsserter.HasValue($"Series[{seriesIndex}]/marketEvaluationPoint.mRID", expectedMeteringPointNumber); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType) { - _documentAsserter.HasValue("Series[1]/marketEvaluationPoint.type", expectedMeteringPointType); + _documentAsserter.HasValue($"Series[{seriesIndex}]/marketEvaluationPoint.type", expectedMeteringPointType); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, string? expectedOriginalTransactionIdReferenceId) { if (expectedOriginalTransactionIdReferenceId == null) { - _documentAsserter.IsNotPresent("Series[1]/originalTransactionIDReference_Series.mRID"); + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/originalTransactionIDReference_Series.mRID"); } else { _documentAsserter.HasValue( - "Series[1]/originalTransactionIDReference_Series.mRID", + $"Series[{seriesIndex}]/originalTransactionIDReference_Series.mRID", expectedOriginalTransactionIdReferenceId); } return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct) { - _documentAsserter.HasValue("Series[1]/product", expectedProduct); + if (expectedProduct is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/product"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/product", expectedProduct); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit) { - _documentAsserter.HasValue("Series[1]/quantity_Measure_Unit.name", expectedQuantityMeasureUnit); + _documentAsserter.HasValue($"Series[{seriesIndex}]/quantity_Measure_Unit.name", expectedQuantityMeasureUnit); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime) { - _documentAsserter.HasValue("Series[1]/registration_DateAndOrTime.dateTime", expectedRegistrationDateTime); + if (expectedRegistrationDateTime is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/registration_DateAndOrTime.dateTime"); + return this; + } + + _documentAsserter.HasValue( + $"Series[{seriesIndex}]/registration_DateAndOrTime.dateTime", + expectedRegistrationDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution( + int seriesIndex, + string expectedResolution) { - _documentAsserter.HasValue("Series[1]/Period/resolution", expectedResolution); + _documentAsserter.HasValue($"Series[{seriesIndex}]/Period/resolution", expectedResolution); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime) { _documentAsserter - .HasValue("Series[1]/Period/timeInterval/start", expectedStartedDateTime); + .HasValue($"Series[{seriesIndex}]/Period/timeInterval/start", expectedStartedDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime) { _documentAsserter - .HasValue("Series[1]/Period/timeInterval/end", expectedEndedDateTime); + .HasValue($"Series[{seriesIndex}]/Period/timeInterval/end", expectedEndedDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain(int seriesIndex, string? expectedInDomain) + { + if (expectedInDomain is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/in_Domain.mRID"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/in_Domain.mRID", expectedInDomain); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain( + int seriesIndex, + string? expectedOutDomain) + { + if (expectedOutDomain is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/out_Domain.mRID"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/out_Domain.mRID", expectedOutDomain); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints) { var pointsInDocument = _documentAsserter - .GetElements("Series[1]/Period/Point")!; + .GetElements($"Series[{seriesIndex}]/Period/Point")!; pointsInDocument.Should().HaveSameCount(expectedPoints); for (var i = 0; i < expectedPoints.Count; i++) { - var expectedPoint = expectedPoints[i]; + var (requiredPointDocumentFields, optionalPointDocumentFields) = expectedPoints[i]; _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/position", expectedPoint.Position.ToString()); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/position", + requiredPointDocumentFields.Position.ToString()); - if (expectedPoint.Quantity != null) + if (optionalPointDocumentFields.Quantity != null) { _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/quantity", expectedPoint.Quantity!.ToString()!); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/quantity", + optionalPointDocumentFields.Quantity!.ToString()!); + } + else + { + AssertElementNotPresent($"Series[{seriesIndex}]/Period/Point[{i + 1}]/quantity"); } - if (expectedPoint.Quality != null) + if (optionalPointDocumentFields.Quality != null) { _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/quality", expectedPoint.Quality); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/quality", + optionalPointDocumentFields.Quality); + } + else + { + AssertElementNotPresent($"Series[{seriesIndex}]/Period/Point[{i + 1}]/quality"); } } return this; + + void AssertElementNotPresent(string xpath) + { + _documentAsserter.IsNotPresent(xpath); + } } public async Task DocumentIsValidAsync() diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs new file mode 100644 index 0000000000..0c08cb5a77 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs @@ -0,0 +1,19 @@ +// 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. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public sealed record AssertPointDocumentFieldsInput( + RequiredPointDocumentFields Required, + OptionalPointDocumentFields Optional); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs index c3768bc936..37c0d27591 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs @@ -13,13 +13,12 @@ // limitations under the License. using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; public interface IAssertMeteredDateForMeasurementPointDocumentDocument { - IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId); + IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists(); IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessReason(string expectedBusinessReasonCode); @@ -33,27 +32,56 @@ public interface IAssertMeteredDateForMeasurementPointDocumentDocument IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string expectedTimestamp); - IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId); + IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType(string? expectedBusinessSectorType); - IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber(string expectedMeteringPointNumber, string expectedSchemeCode); + IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId); - IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType); + IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, + string expectedMeteringPointNumber, + string expectedSchemeCode); - IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId(string? expectedOriginalTransactionIdReferenceId); + IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType); - IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct); + IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, + string? expectedOriginalTransactionIdReferenceId); - IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit); + IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct); - IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit); - IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution); + IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime); - IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(int seriesIndex, string expectedResolution); - IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime); - IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints); + IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain( + int seriesIndex, + string? expectedInDomain); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain( + int seriesIndex, + string? expectedOutDomain); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints); /// /// Asserts document validity diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs index 199aff0bd4..9172e38224 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs @@ -40,7 +40,7 @@ public class MeteredDateForMeasurementPointDocumentWriterTests(DocumentValidatio [Theory] [InlineData(nameof(DocumentFormat.Xml))] [InlineData(nameof(DocumentFormat.Json))] - public async Task Can_create_notifyValidatedMeasureData_document(string documentFormat) + public async Task Can_create_maximal_notifyValidatedMeasureData_document(string documentFormat) { // Arrange var messageBuilder = _meteredDateForMeasurementPointBuilder; @@ -54,44 +54,137 @@ public async Task Can_create_notifyValidatedMeasureData_document(string document // Assert using var assertionScope = new AssertionScope(); await AssertDocument(document, DocumentFormat.FromName(documentFormat)) - .HasMessageId(SampleData.MessageId) + .MessageIdExists() + .HasBusinessReason(SampleData.BusinessReason.Code) + .HasSenderId(SampleData.SenderActorNumber, "A10") + .HasSenderRole(SampleData.SenderActorRole) + .HasReceiverId(SampleData.ReceiverActorNumber, "A10") + .HasReceiverRole(SampleData.ReceiverActorRole) + .HasTimestamp(SampleData.TimeStamp.ToString()) + .HasTransactionId(1, SampleData.TransactionId) + .HasMeteringPointNumber(1, SampleData.MeteringPointNumber, "A10") + .HasMeteringPointType(1, SampleData.MeteringPointType) + .HasOriginalTransactionIdReferenceId(1, SampleData.OriginalTransactionIdReferenceId?.Value) + .HasProduct(1, SampleData.Product) + .HasQuantityMeasureUnit(1, SampleData.QuantityMeasureUnit.Code) + .HasRegistrationDateTime(1, SampleData.RegistrationDateTime.ToString()) + .HasResolution(1, SampleData.Resolution.Code) + .HasStartedDateTime( + 1, + SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasEndedDateTime( + 1, + SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasPoints( + 1, + SampleData.Points.Select( + p => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(p.Position), + new OptionalPointDocumentFields(p.Quality, p.Quantity))) + .ToList()) + .DocumentIsValidAsync(); + } + + [Theory] + [InlineData(nameof(DocumentFormat.Xml))] + [InlineData(nameof(DocumentFormat.Json))] + public async Task Can_create_minimal_series_notifyValidatedMeasureData_document(string documentFormat) + { + // Arrange + var messageBuilder = _meteredDateForMeasurementPointBuilder; + + // Act + var document = await WriteDocument( + messageBuilder.BuildHeader(), + messageBuilder.BuildMinimalMeteredDataForMeasurementPoint(), + DocumentFormat.FromName(documentFormat)); + + // Assert + using var assertionScope = new AssertionScope(); + await AssertDocument(document, DocumentFormat.FromName(documentFormat)) + .MessageIdExists() + .HasBusinessReason(SampleData.BusinessReason.Code) + .HasSenderId(SampleData.SenderActorNumber, "A10") + .HasSenderRole(SampleData.SenderActorRole) + .HasReceiverId(SampleData.ReceiverActorNumber, "A10") + .HasReceiverRole(SampleData.ReceiverActorRole) + .HasTimestamp(SampleData.TimeStamp.ToString()) + .HasTransactionId(1, SampleData.TransactionId) + .HasMeteringPointNumber(1, SampleData.MeteringPointNumber, "A10") + .HasMeteringPointType(1, SampleData.MeteringPointType) + .HasOriginalTransactionIdReferenceId(1, null) + .HasProduct(1, null) + .HasQuantityMeasureUnit(1, SampleData.QuantityMeasureUnit.Code) + .HasRegistrationDateTime(1, null) + .HasResolution(1, SampleData.Resolution.Code) + .HasStartedDateTime( + 1, + SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasEndedDateTime( + 1, + SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasPoints( + 1, + SampleData.MinimalPoints.Select( + p => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(p.Position), + OptionalPointDocumentFields.NoOptionalFields())) + .ToList()) + .DocumentIsValidAsync(); + } + + [Theory] + [InlineData(nameof(DocumentFormat.Xml))] + [InlineData(nameof(DocumentFormat.Json))] + /* + * We do not expect that we ever have to create any of these, + * but in case something goes wrong, we need to be able to create them, + * in order for the actors to empty their queues + */ + public async Task Can_create_no_series_notifyValidatedMeasureData_document(string documentFormat) + { + // Arrange + var messageBuilder = _meteredDateForMeasurementPointBuilder; + + // Act + var document = await WriteDocument( + messageBuilder.BuildHeader(), + null, + DocumentFormat.FromName(documentFormat)); + + // Assert + using var assertionScope = new AssertionScope(); + await AssertDocument(document, DocumentFormat.FromName(documentFormat)) + .MessageIdExists() .HasBusinessReason(SampleData.BusinessReason.Code) .HasSenderId(SampleData.SenderActorNumber, "A10") .HasSenderRole(SampleData.SenderActorRole) .HasReceiverId(SampleData.ReceiverActorNumber, "A10") .HasReceiverRole(SampleData.ReceiverActorRole) .HasTimestamp(SampleData.TimeStamp.ToString()) - .HasTransactionId(SampleData.TransactionId) - .HasMeteringPointNumber(SampleData.MeteringPointNumber, "A10") - .HasMeteringPointType(SampleData.MeteringPointType) - .HasOriginalTransactionIdReferenceId(SampleData.OriginalTransactionIdReferenceId?.Value) - .HasProduct(SampleData.Product) - .HasQuantityMeasureUnit(SampleData.QuantityMeasureUnit.Code) - .HasRegistrationDateTime(SampleData.RegistrationDateTime.ToString()) - .HasResolution(SampleData.Resolution.Code) - .HasStartedDateTime(SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) - .HasEndedDateTime(SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) - .HasPoints(SampleData.Points) .DocumentIsValidAsync(); } private Task WriteDocument( OutgoingMessageHeader header, - MeteredDateForMeasurementPointMarketActivityRecord meteredDateForMeasurementPointMarketActivityRecord, + MeteredDateForMeasurementPointMarketActivityRecord? meteredDateForMeasurementPointMarketActivityRecord, DocumentFormat documentFormat) { - var records = _parser.From(meteredDateForMeasurementPointMarketActivityRecord); + var records = meteredDateForMeasurementPointMarketActivityRecord is null + ? null + : _parser.From(meteredDateForMeasurementPointMarketActivityRecord); if (documentFormat == DocumentFormat.Xml) { - return new MeteredDateForMeasurementPointCimXmlDocumentWriter(_parser).WriteAsync(header, new[] { records }); + return new MeteredDateForMeasurementPointCimXmlDocumentWriter(_parser) + .WriteAsync(header, records is null ? [] : [records]); } var serviceProvider = new ServiceCollection().AddJavaScriptEncoder().BuildServiceProvider(); return new MeteredDateForMeasurementPointCimJsonDocumentWriter( _parser, serviceProvider.GetRequiredService()) - .WriteAsync(header, [records], CancellationToken.None); + .WriteAsync(header, records is null ? [] : [records], CancellationToken.None); } private IAssertMeteredDateForMeasurementPointDocumentDocument AssertDocument( diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs new file mode 100644 index 0000000000..294d093be5 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs @@ -0,0 +1,20 @@ +// 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. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public sealed record OptionalPointDocumentFields(string? Quality, decimal? Quantity) +{ + public static OptionalPointDocumentFields NoOptionalFields() => new(null, null); +} diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs new file mode 100644 index 0000000000..d511caa3c4 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public readonly record struct RequiredPointDocumentFields(int Position); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs index 140da37d15..06a118b446 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs @@ -71,5 +71,7 @@ internal static class SampleData new(6, "A02", null), }; + public static IReadOnlyList MinimalPoints => [new(2, null, null)]; + #endregion }