Skip to content

Commit

Permalink
Merge 529ae5a into 2d4ae21
Browse files Browse the repository at this point in the history
  • Loading branch information
vbelinschi authored Dec 13, 2024
2 parents 2d4ae21 + 529ae5a commit 10e29c9
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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.

using MediatR;

namespace Energinet.DataHub.MarketParticipant.Application.Commands.GridAreas;

public sealed record GetRelevantGridAreasCommand(GetRelevantGridAreasRequestDto GetRelevantGridAreasRequest) : IRequest<GetGridAreasResponse>;
Original file line number Diff line number Diff line change
@@ -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.

using System;

namespace Energinet.DataHub.MarketParticipant.Application.Commands.GridAreas;

public record GetRelevantGridAreasRequestDto(DateTimeOffset StartDate, DateTimeOffset EndDate);
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Energinet.DataHub.Core.App.Common.Abstractions.Users;
using Energinet.DataHub.MarketParticipant.Application.Commands.GridAreas;
using Energinet.DataHub.MarketParticipant.Application.Security;
using Energinet.DataHub.MarketParticipant.Domain.Exception;
using Energinet.DataHub.MarketParticipant.Domain.Model;
using Energinet.DataHub.MarketParticipant.Domain.Repositories;
using MediatR;

namespace Energinet.DataHub.MarketParticipant.Application.Handlers.GridAreas;

public sealed class GetRelevantGridAreasHandler : IRequestHandler<GetRelevantGridAreasCommand, GetGridAreasResponse>
{
private readonly IActorRepository _actorRepository;
private readonly IGridAreaRepository _gridAreaRepository;
private readonly IUserContext<FrontendUser> _userContext;

public GetRelevantGridAreasHandler(IActorRepository actorRepository, IGridAreaRepository gridAreaRepository, IUserContext<FrontendUser> userContext)
{
_actorRepository = actorRepository;
_gridAreaRepository = gridAreaRepository;
_userContext = userContext;
}

public async Task<GetGridAreasResponse> Handle(GetRelevantGridAreasCommand request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request, nameof(request));

var gridAreas = await _gridAreaRepository.GetAsync().ConfigureAwait(false);
var filteredByDateGridAreas = gridAreas.Where(ga => DoDatesOverlap(ga, request.GetRelevantGridAreasRequest.StartDate, request.GetRelevantGridAreasRequest.EndDate));

var actor = await _actorRepository.GetAsync(new ActorId(_userContext.CurrentUser.ActorId)).ConfigureAwait(false);
NotFoundValidationException.ThrowIfNull(actor, _userContext.CurrentUser.ActorId);
var actorGridAreaIds = actor.MarketRole.GridAreas.Select(ga => ga.Id);
var relevantGridAreas = filteredByDateGridAreas.Where(ga => actorGridAreaIds.Contains(ga.Id));

return new GetGridAreasResponse(relevantGridAreas.Select(gridArea => new GridAreaDto(
gridArea.Id.Value,
gridArea.Code.Value,
gridArea.Name.Value,
gridArea.PriceAreaCode.ToString(),
gridArea.Type,
gridArea.ValidFrom,
gridArea.ValidTo)));
}

private static bool DoDatesOverlap(GridArea gridArea, DateTimeOffset startDate, DateTimeOffset endDate)
{
var convertedStartDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(startDate, "Romance Standard Time");
var convertedEndDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(endDate.AddMilliseconds(-1), "Romance Standard Time");

if (!gridArea.ValidTo.HasValue)
{
return gridArea.ValidFrom <= convertedEndDate;
}

// formula from https://www.baeldung.com/java-check-two-date-ranges-overlap
var overlap = Math.Min(gridArea.ValidTo.Value.Ticks, convertedEndDate.Ticks) - Math.Max(gridArea.ValidFrom.Ticks, convertedStartDate.Ticks);
return overlap >= 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public async Task<ActionResult<IEnumerable<GridAreaDto>>> GetGridAreasAsync()
return Ok(response.GridAreas);
}

[HttpPost("relevant")]
[EnableRevision(RevisionActivities.RelevantGridAreasRetrieved, typeof(GridArea))]
public async Task<ActionResult<IEnumerable<GridAreaDto>>> GetRelevantGridAreasAsync(GetRelevantGridAreasRequestDto getRelevantGridAreasRequest)
{
ArgumentNullException.ThrowIfNull(getRelevantGridAreasRequest);

var command = new GetRelevantGridAreasCommand(getRelevantGridAreasRequest);
var response = await _mediator.Send(command).ConfigureAwait(false);
return Ok(response.GridAreas);
}

[HttpGet("{gridAreaId:guid}")]
[EnableRevision(RevisionActivities.PublicGridAreasRetrieved, typeof(GridArea), "gridAreaId")]
public async Task<ActionResult<GridAreaDto>> GetGridAreaAsync(Guid gridAreaId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static class RevisionActivities
public const string UserAuditLogViewed = "UserAuditLogViewed";

public const string PublicGridAreasRetrieved = "PublicGridAreasRetrieved";
public const string RelevantGridAreasRetrieved = "RelevantGridAreasRetrieved";
public const string GridAreaCreated = "GridAreaCreated";
public const string GridAreaEdited = "GridAreaEdited";
public const string GridAreaAuditLogViewed = "GridAreaAuditLogViewed";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// 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;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Energinet.DataHub.Core.App.Common.Abstractions.Users;
using Energinet.DataHub.MarketParticipant.Application.Commands.GridAreas;
using Energinet.DataHub.MarketParticipant.Application.Handlers.GridAreas;
using Energinet.DataHub.MarketParticipant.Application.Security;
using Energinet.DataHub.MarketParticipant.Domain.Model;
using Energinet.DataHub.MarketParticipant.Domain.Repositories;
using Energinet.DataHub.MarketParticipant.Tests.Common;
using Moq;
using Xunit;
using Xunit.Categories;

namespace Energinet.DataHub.MarketParticipant.Tests.Handlers;

[UnitTest]
public sealed class GetRelevantGridAreasHandlerTests
{
[Fact]
public async Task Handle_ActorWithGridAreas_ReturnsGridAreas()
{
// arrange
var gridArea = new GridArea(
new GridAreaId(Guid.NewGuid()),
new GridAreaName("name"),
new GridAreaCode("code"),
PriceAreaCode.Dk1,
GridAreaType.Distribution,
DateTimeOffset.MinValue,
DateTimeOffset.MaxValue);
var gridAreaRepositoryMock = new Mock<IGridAreaRepository>();
gridAreaRepositoryMock
.Setup(x => x.GetAsync())
.ReturnsAsync([gridArea]);

var mockedActor = TestPreparationModels.MockedActor(Guid.NewGuid(), Guid.NewGuid());
mockedActor.UpdateMarketRole(new ActorMarketRole(mockedActor.MarketRole.Function, [new ActorGridArea(gridArea.Id, [])]));
var actorRepositoryMock = new Mock<IActorRepository>();
actorRepositoryMock.Setup(x => x.GetAsync(It.IsAny<ActorId>()))
.ReturnsAsync(mockedActor);

var userContextMock = new Mock<IUserContext<FrontendUser>>();
userContextMock
.Setup(x => x.CurrentUser)
.Returns(new FrontendUser(Guid.NewGuid(), mockedActor.OrganizationId.Value, mockedActor.Id.Value, true));

var relevantGridAreasRequest = new GetRelevantGridAreasRequestDto(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), new DateTimeOffset(2024, 1, 31, 23, 59, 59, TimeSpan.Zero));

var target = new GetRelevantGridAreasHandler(actorRepositoryMock.Object, gridAreaRepositoryMock.Object, userContextMock.Object);

// act
var actual = await target.Handle(new GetRelevantGridAreasCommand(relevantGridAreasRequest), CancellationToken.None);

// assert
Assert.NotEmpty(actual.GridAreas);
}

[Fact]
public async Task Handle_ActorHasNoGridAreas_ReturnsEmpty()
{
// arrange
var gridArea = new GridArea(
new GridAreaId(Guid.NewGuid()),
new GridAreaName("name"),
new GridAreaCode("code"),
PriceAreaCode.Dk1,
GridAreaType.Distribution,
DateTimeOffset.MinValue,
DateTimeOffset.MaxValue);
var gridAreaRepositoryMock = new Mock<IGridAreaRepository>();
gridAreaRepositoryMock
.Setup(x => x.GetAsync())
.ReturnsAsync([gridArea]);

var mockedActor = TestPreparationModels.MockedActor(Guid.NewGuid(), Guid.NewGuid());
var actorRepositoryMock = new Mock<IActorRepository>();
actorRepositoryMock.Setup(x => x.GetAsync(It.IsAny<ActorId>()))
.ReturnsAsync(mockedActor);

var userContextMock = new Mock<IUserContext<FrontendUser>>();
userContextMock
.Setup(x => x.CurrentUser)
.Returns(new FrontendUser(Guid.NewGuid(), mockedActor.OrganizationId.Value, mockedActor.Id.Value, true));

var relevantGridAreasRequest = new GetRelevantGridAreasRequestDto(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), new DateTimeOffset(2024, 1, 31, 23, 59, 59, TimeSpan.Zero));

var target = new GetRelevantGridAreasHandler(actorRepositoryMock.Object, gridAreaRepositoryMock.Object, userContextMock.Object);

// act
var actual = await target.Handle(new GetRelevantGridAreasCommand(relevantGridAreasRequest), CancellationToken.None);

// assert
Assert.Empty(actual.GridAreas);
}

[Theory]
[InlineData("01/01/2023 00:00:00", "31/12/2025 23:59:59", "01/01/2024 00:00:00", "31/01/2024 23:59:59")]
[InlineData("01/01/2023 00:00:00", "31/12/2025 23:59:59", "01/01/2022 00:00:00", "31/01/2023 23:59:59")]
[InlineData("01/01/2023 00:00:00", "31/12/2025 23:59:59", "01/01/2022 00:00:00", "31/01/2026 23:59:59")]
[InlineData("01/01/2023 00:00:00", null, "01/01/2022 00:00:00", "31/01/2026 23:59:59")]
[InlineData("01/01/2023 00:00:00", null, "31/12/2025 23:59:59", "31/01/2026 23:59:59")]
public async Task Handle_ValidDates_ReturnsGridAreas(string validFrom, string? validTo, string periodStart, string periodEnd)
{
// arrange
var gridArea = new GridArea(
new GridAreaId(Guid.NewGuid()),
new GridAreaName("name"),
new GridAreaCode("code"),
PriceAreaCode.Dk1,
GridAreaType.Distribution,
DateTimeOffset.Parse(validFrom, new CultureInfo("da-dk")),
!string.IsNullOrEmpty(validTo) ? DateTimeOffset.Parse(validTo, new CultureInfo("da-dk")) : null);
var gridAreaRepositoryMock = new Mock<IGridAreaRepository>();
gridAreaRepositoryMock
.Setup(x => x.GetAsync())
.ReturnsAsync([gridArea]);

var mockedActor = TestPreparationModels.MockedActor(Guid.NewGuid(), Guid.NewGuid());
mockedActor.UpdateMarketRole(new ActorMarketRole(mockedActor.MarketRole.Function, [new ActorGridArea(gridArea.Id, [])]));
var actorRepositoryMock = new Mock<IActorRepository>();
actorRepositoryMock.Setup(x => x.GetAsync(It.IsAny<ActorId>()))
.ReturnsAsync(mockedActor);

var userContextMock = new Mock<IUserContext<FrontendUser>>();
userContextMock
.Setup(x => x.CurrentUser)
.Returns(new FrontendUser(Guid.NewGuid(), mockedActor.OrganizationId.Value, mockedActor.Id.Value, true));

var relevantGridAreasRequest = new GetRelevantGridAreasRequestDto(DateTimeOffset.Parse(periodStart, new CultureInfo("da-dk")), DateTimeOffset.Parse(periodEnd, new CultureInfo("da-dk")));

var target = new GetRelevantGridAreasHandler(actorRepositoryMock.Object, gridAreaRepositoryMock.Object, userContextMock.Object);

// act
var actual = await target.Handle(new GetRelevantGridAreasCommand(relevantGridAreasRequest), CancellationToken.None);

// assert
Assert.NotEmpty(actual.GridAreas);
}

[Theory]
[InlineData("01/01/2023 00:00:00", "31/12/2025 23:59:59", "01/01/2022 00:00:00", "31/01/2022 23:59:59")]
[InlineData("01/01/2023 00:00:00", "31/12/2025 23:59:59", "01/01/2026 00:00:00", "31/01/2026 23:59:59")]
[InlineData("01/01/2027 00:00:00", null, "31/12/2025 23:59:59", "31/01/2026 23:59:59")]
public async Task Handle_InvalidDates_ReturnsEmpty(string validFrom, string? validTo, string periodStart, string periodEnd)
{
// arrange
var gridArea = new GridArea(
new GridAreaId(Guid.NewGuid()),
new GridAreaName("name"),
new GridAreaCode("code"),
PriceAreaCode.Dk1,
GridAreaType.Distribution,
DateTimeOffset.Parse(validFrom, new CultureInfo("da-dk")),
!string.IsNullOrEmpty(validTo) ? DateTimeOffset.Parse(validTo, new CultureInfo("da-dk")) : null);
var gridAreaRepositoryMock = new Mock<IGridAreaRepository>();
gridAreaRepositoryMock
.Setup(x => x.GetAsync())
.ReturnsAsync([gridArea]);

var mockedActor = TestPreparationModels.MockedActor(Guid.NewGuid(), Guid.NewGuid());
mockedActor.UpdateMarketRole(new ActorMarketRole(mockedActor.MarketRole.Function, [new ActorGridArea(gridArea.Id, [])]));
var actorRepositoryMock = new Mock<IActorRepository>();
actorRepositoryMock.Setup(x => x.GetAsync(It.IsAny<ActorId>()))
.ReturnsAsync(mockedActor);

var userContextMock = new Mock<IUserContext<FrontendUser>>();
userContextMock
.Setup(x => x.CurrentUser)
.Returns(new FrontendUser(Guid.NewGuid(), mockedActor.OrganizationId.Value, mockedActor.Id.Value, true));

var relevantGridAreasRequest = new GetRelevantGridAreasRequestDto(DateTimeOffset.Parse(periodStart, new CultureInfo("da-dk")), DateTimeOffset.Parse(periodEnd, new CultureInfo("da-dk")));

var target = new GetRelevantGridAreasHandler(actorRepositoryMock.Object, gridAreaRepositoryMock.Object, userContextMock.Object);

// act
var actual = await target.Handle(new GetRelevantGridAreasCommand(relevantGridAreasRequest), CancellationToken.None);

// assert
Assert.Empty(actual.GridAreas);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public void ControllerEndpoint_Exists_MustHaveAuthorizationAttribute()
$"{nameof(ActorQueryController)}.{nameof(ActorQueryController.GetSelectionActorsAsync)}",
$"{nameof(GridAreaController)}.{nameof(GridAreaController.GetGridAreasAsync)}",
$"{nameof(GridAreaController)}.{nameof(GridAreaController.GetGridAreaAsync)}",
$"{nameof(GridAreaController)}.{nameof(GridAreaController.GetRelevantGridAreasAsync)}",
$"{nameof(GridAreaController)}.{nameof(GridAreaController.GetAuditAsync)}",
$"{nameof(GridAreaOverviewController)}.{nameof(GridAreaOverviewController.GetGridAreaOverviewAsync)}",
$"{nameof(OrganizationController)}.{nameof(OrganizationController.ListAllAsync)}",
Expand Down

0 comments on commit 10e29c9

Please sign in to comment.