diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs index 29c186afc..5dc60740f 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs @@ -75,7 +75,7 @@ private Document ParseFrom(OutgoingMessageHeader header, IReadOnlyCollection meteredDataForMeasurementPoints) + IReadOnlyCollection? meteredDataForMeasurementPoints) { [JsonPropertyName("mRID")] public string MessageId { get; init; } = messageId; @@ -150,7 +150,9 @@ internal class MeteredDateForMeasurementPoint( public ValueObject Type { get; init; } = ValueObject.Create(typeCode); [JsonPropertyName("Series")] - public IReadOnlyCollection MeteredDataForMeasurementPoints { get; init; } = meteredDataForMeasurementPoints; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyCollection? MeteredDataForMeasurementPoints { get; init; } = + meteredDataForMeasurementPoints; } internal class MeteredDataForMeasurementPoint( @@ -158,9 +160,9 @@ internal class MeteredDataForMeasurementPoint( string marketEvaluationPointNumber, string marketEvaluationPointType, string? originalTransactionIdReferenceId, - string product, + string? product, string quantityMeasureUnit, - string registrationDateTime, + string? registrationDateTime, Period period) { [JsonPropertyName("mRID")] @@ -177,13 +179,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 4de6b1db6..f8f72049f 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 0497fc977..60fa8acb0 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/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs b/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs index a12626dde..46efe7bdf 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 b219a894e..21ee05013 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs @@ -41,6 +41,12 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() return this; } + public IAssertMeteredDateForMeasurementPointDocumentDocument HasNoSeriesElements() + { + _root.TryGetProperty("Series", out _).Should().BeFalse("property 'Series' should not be present"); + return this; + } + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessReason(string expectedBusinessReasonCode) { Assert.Equal(expectedBusinessReasonCode, _root.GetProperty("process.processType").GetProperty("value").ToString()); @@ -360,7 +366,10 @@ public async Task Documen { 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; } @@ -370,31 +379,28 @@ 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 GetTimeSeriesElement(int seriesIndex) => diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs index 110447d7a..d6c8e290d 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs @@ -34,6 +34,12 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() return this; } + public IAssertMeteredDateForMeasurementPointDocumentDocument HasNoSeriesElements() + { + _documentAsserter.IsNotPresent("Series[1]"); + return this; + } + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessReason(string expectedBusinessReasonCode) { _documentAsserter.HasValue("process.processType", expectedBusinessReasonCode); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs index 37c0d2759..6f241d595 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs @@ -20,6 +20,8 @@ public interface IAssertMeteredDateForMeasurementPointDocumentDocument { IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists(); + IAssertMeteredDateForMeasurementPointDocumentDocument HasNoSeriesElements(); + IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessReason(string expectedBusinessReasonCode); IAssertMeteredDateForMeasurementPointDocumentDocument HasSenderId(string expectedSenderId, string expectedSchemeCode); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs index 81a6f91d5..591bc259a 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; @@ -85,23 +85,102 @@ await AssertDocument(document, DocumentFormat.FromName(documentFormat)) .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))] + 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()) + .HasNoSeriesElements() + .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/SampleData.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs index 140da37d1..06a118b44 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 }