diff --git a/source/settlement-report/Orchestration.SettlementReports.IntegrationTests/Fixtures/OrchestrationSettlementReportsAppFixture.cs b/source/settlement-report/Orchestration.SettlementReports.IntegrationTests/Fixtures/OrchestrationSettlementReportsAppFixture.cs index 8c17818..a59ca71 100644 --- a/source/settlement-report/Orchestration.SettlementReports.IntegrationTests/Fixtures/OrchestrationSettlementReportsAppFixture.cs +++ b/source/settlement-report/Orchestration.SettlementReports.IntegrationTests/Fixtures/OrchestrationSettlementReportsAppFixture.cs @@ -206,6 +206,13 @@ private FunctionAppHostSettings CreateAppHostSettings(ref int port) $"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageAccountUri)}", AzuriteManager.BlobStorageServiceUri + "/"); + appHostSettings.ProcessEnvironmentVariables.Add( + $"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageContainerForJobsName)}", + "settlement-report-container-jobs"); + appHostSettings.ProcessEnvironmentVariables.Add( + $"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageAccountForJobsUri)}", + AzuriteManager.BlobStorageServiceUri + "/"); + return appHostSettings; } diff --git a/source/settlement-report/Orchestration.SettlementReports/Functions/SettlementReports/SettlementReportListHttpTrigger.cs b/source/settlement-report/Orchestration.SettlementReports/Functions/SettlementReports/SettlementReportListHttpTrigger.cs index 109c6e8..d9ab0c0 100644 --- a/source/settlement-report/Orchestration.SettlementReports/Functions/SettlementReports/SettlementReportListHttpTrigger.cs +++ b/source/settlement-report/Orchestration.SettlementReports/Functions/SettlementReports/SettlementReportListHttpTrigger.cs @@ -81,7 +81,7 @@ private async Task> CheckStatusOfSettl { var updatedReport = settlementReport; - if (settlementReport.Status == SettlementReportStatus.InProgress && settlementReport.RequestId != null) + if (settlementReport.Status == SettlementReportStatus.InProgress && settlementReport.JobId == null) { var instanceInfo = await durableTaskClient .GetInstanceAsync(settlementReport.RequestId.Id, getInputsAndOutputs: true) diff --git a/source/settlement-report/SettlementReports.Application/Handlers/ISettlementReportJobsDownloadHandler.cs b/source/settlement-report/SettlementReports.Application/Handlers/ISettlementReportJobsDownloadHandler.cs new file mode 100644 index 0000000..6ed9241 --- /dev/null +++ b/source/settlement-report/SettlementReports.Application/Handlers/ISettlementReportJobsDownloadHandler.cs @@ -0,0 +1,22 @@ +// 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 Energinet.DataHub.SettlementReport.Interfaces.SettlementReports_v2.Models; + +namespace Energinet.DataHub.SettlementReport.Application.Handlers; + +public interface ISettlementReportJobsDownloadHandler +{ + Task DownloadReportAsync(SettlementReportRequestId requestId, Func outputStreamProvider, Guid actorId, bool isMultitenancy); +} diff --git a/source/settlement-report/SettlementReports.Application/Handlers/ListSettlementReportJobsHandler.cs b/source/settlement-report/SettlementReports.Application/Handlers/ListSettlementReportJobsHandler.cs index 126913c..ad9a8d5 100644 --- a/source/settlement-report/SettlementReports.Application/Handlers/ListSettlementReportJobsHandler.cs +++ b/source/settlement-report/SettlementReports.Application/Handlers/ListSettlementReportJobsHandler.cs @@ -60,9 +60,14 @@ private async Task> GetSettlementRepor { var jobStatus = await _jobHelper.GetSettlementReportsJobStatusAsync(settlementReportDto.JobId!.Id) .ConfigureAwait(false); - if (jobStatus == JobRunStatus.Completed) + switch (jobStatus) { - await MarkAsCompletedAsync(settlementReportDto).ConfigureAwait(false); + case JobRunStatus.Completed: + await MarkAsCompletedAsync(settlementReportDto).ConfigureAwait(false); + break; + case JobRunStatus.Canceled or JobRunStatus.Failed: + await MarkAsFailedAsync(settlementReportDto).ConfigureAwait(false); + break; } results.Add(settlementReportDto with { Status = MapFromJobStatus(jobStatus) }); @@ -85,7 +90,23 @@ private async Task MarkAsCompletedAsync(RequestedSettlementReportDto settlementR .GetAsync(settlementReportDto.JobId.Id) .ConfigureAwait(false); - request.MarkAsCompleted(_clock, settlementReportDto.JobId); + request.MarkAsCompleted(_clock, settlementReportDto.RequestId); + + await _repository + .AddOrUpdateAsync(request) + .ConfigureAwait(false); + } + + private async Task MarkAsFailedAsync(RequestedSettlementReportDto settlementReportDto) + { + ArgumentNullException.ThrowIfNull(settlementReportDto); + ArgumentNullException.ThrowIfNull(settlementReportDto.JobId); + + var request = await _repository + .GetAsync(settlementReportDto.JobId.Id) + .ConfigureAwait(false); + + request.MarkAsFailed(); await _repository .AddOrUpdateAsync(request) diff --git a/source/settlement-report/SettlementReports.Application/Handlers/SettlementReportJobsDownloadHandler.cs b/source/settlement-report/SettlementReports.Application/Handlers/SettlementReportJobsDownloadHandler.cs new file mode 100644 index 0000000..3f12e4f --- /dev/null +++ b/source/settlement-report/SettlementReports.Application/Handlers/SettlementReportJobsDownloadHandler.cs @@ -0,0 +1,55 @@ +// 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 Energinet.DataHub.SettlementReport.Application.SettlementReports_v2; +using Energinet.DataHub.SettlementReport.Interfaces.SettlementReports_v2.Models; + +namespace Energinet.DataHub.SettlementReport.Application.Handlers; + +public sealed class SettlementReportJobsDownloadHandler : ISettlementReportJobsDownloadHandler +{ + private readonly ISettlementReportJobsFileRepository _fileRepository; + private readonly ISettlementReportRepository _repository; + + public SettlementReportJobsDownloadHandler( + ISettlementReportJobsFileRepository fileRepository, + ISettlementReportRepository repository) + { + _fileRepository = fileRepository; + _repository = repository; + } + + public async Task DownloadReportAsync( + SettlementReportRequestId requestId, + Func outputStreamProvider, + Guid actorId, + bool isMultitenancy) + { + var report = await _repository + .GetAsync(requestId.Id) + .ConfigureAwait(false) ?? throw new InvalidOperationException("Report not found."); + + if (!isMultitenancy && (report.ActorId != actorId || report.IsHiddenFromActor)) + { + throw new InvalidOperationException("User does not have access to the report."); + } + + if (string.IsNullOrEmpty(report.BlobFileName)) + throw new InvalidOperationException("Report does not have a Blob file name."); + + await _fileRepository + .DownloadAsync(report.BlobFileName, outputStreamProvider()) + .ConfigureAwait(false); + } +} diff --git a/source/settlement-report/SettlementReports.Application/SettlementReports_v2/ISettlementReportJobsFileRepository.cs b/source/settlement-report/SettlementReports.Application/SettlementReports_v2/ISettlementReportJobsFileRepository.cs index 47d4b62..b6610d0 100644 --- a/source/settlement-report/SettlementReports.Application/SettlementReports_v2/ISettlementReportJobsFileRepository.cs +++ b/source/settlement-report/SettlementReports.Application/SettlementReports_v2/ISettlementReportJobsFileRepository.cs @@ -18,7 +18,5 @@ namespace Energinet.DataHub.SettlementReport.Application.SettlementReports_v2; public interface ISettlementReportJobsFileRepository { - Task DownloadAsync(JobRunId jobRunId, string fileName, Stream downloadStream); - - Task DeleteAsync(JobRunId reportRequestId, string fileName); + Task DownloadAsync(string blobFileName, Stream downloadStream); } diff --git a/source/settlement-report/SettlementReports.Application/SettlementReports_v2/SettlementReport.cs b/source/settlement-report/SettlementReports.Application/SettlementReports_v2/SettlementReport.cs index 01377cd..4afa811 100644 --- a/source/settlement-report/SettlementReports.Application/SettlementReports_v2/SettlementReport.cs +++ b/source/settlement-report/SettlementReports.Application/SettlementReports_v2/SettlementReport.cs @@ -121,10 +121,10 @@ public void MarkAsCompleted(IClock clock, GeneratedSettlementReportDto generated EndedDateTime = clock.GetCurrentInstant(); } - public void MarkAsCompleted(IClock clock, JobRunId jobRunId) + public void MarkAsCompleted(IClock clock, SettlementReportRequestId requestId) { Status = SettlementReportStatus.Completed; - BlobFileName = jobRunId.Id.ToString(); + BlobFileName = requestId.Id + ".zip"; EndedDateTime = clock.GetCurrentInstant(); } diff --git a/source/settlement-report/SettlementReports.Infrastructure/Extensions/DependencyInjection/StorageExtensions.cs b/source/settlement-report/SettlementReports.Infrastructure/Extensions/DependencyInjection/StorageExtensions.cs index a1c1fc0..592de29 100644 --- a/source/settlement-report/SettlementReports.Infrastructure/Extensions/DependencyInjection/StorageExtensions.cs +++ b/source/settlement-report/SettlementReports.Infrastructure/Extensions/DependencyInjection/StorageExtensions.cs @@ -45,25 +45,43 @@ public static IServiceCollection AddSettlementReportBlobStorage(this IServiceCol { var blobSettings = serviceProvider.GetRequiredService>().Value; - var blobContainerUri = new Uri(blobSettings.StorageAccountUri, blobSettings.StorageContainerName); + var blobContainerUri = new Uri(blobSettings.StorageAccountForJobsUri, blobSettings.StorageContainerForJobsName); var blobContainerClient = new BlobContainerClient(blobContainerUri, new DefaultAzureCredential()); return new SettlementReportJobsFileBlobStorage(blobContainerClient); }); // Health checks - services.AddHealthChecks().AddAzureBlobStorage( - serviceProvider => - { - var blobSettings = serviceProvider.GetRequiredService>().Value; - return new BlobServiceClient(blobSettings.StorageAccountUri, new DefaultAzureCredential()); - }, - (serviceProvider, options) => - { - var blobSettings = serviceProvider.GetRequiredService>().Value; - options.ContainerName = blobSettings.StorageContainerName; - }, - "SettlementReportBlobStorage"); + services + .AddHealthChecks() + .AddAzureBlobStorage( + serviceProvider => + { + var blobSettings = serviceProvider.GetRequiredService>() + .Value; + return new BlobServiceClient(blobSettings.StorageAccountUri, new DefaultAzureCredential()); + }, + (serviceProvider, options) => + { + var blobSettings = serviceProvider.GetRequiredService>() + .Value; + options.ContainerName = blobSettings.StorageContainerName; + }, + "SettlementReportBlobStorage") + .AddAzureBlobStorage( + serviceProvider => + { + var blobSettings = serviceProvider.GetRequiredService>() + .Value; + return new BlobServiceClient(blobSettings.StorageAccountForJobsUri, new DefaultAzureCredential()); + }, + (serviceProvider, options) => + { + var blobSettings = serviceProvider.GetRequiredService>() + .Value; + options.ContainerName = blobSettings.StorageContainerForJobsName; + }, + "SettlementReportBlobStorageJobs"); return services; } diff --git a/source/settlement-report/SettlementReports.Infrastructure/Extensions/Options/SettlementReportStorageOptions.cs b/source/settlement-report/SettlementReports.Infrastructure/Extensions/Options/SettlementReportStorageOptions.cs index fa83d67..6dd3652 100644 --- a/source/settlement-report/SettlementReports.Infrastructure/Extensions/Options/SettlementReportStorageOptions.cs +++ b/source/settlement-report/SettlementReports.Infrastructure/Extensions/Options/SettlementReportStorageOptions.cs @@ -25,4 +25,10 @@ public class SettlementReportStorageOptions [Required] public string StorageContainerName { get; set; } = string.Empty; + + [Required] + public Uri StorageAccountForJobsUri { get; set; } = null!; + + [Required] + public string StorageContainerForJobsName { get; set; } = string.Empty; } diff --git a/source/settlement-report/SettlementReports.Infrastructure/SettlementReports_v2/RemoveExpiredSettlementReports.cs b/source/settlement-report/SettlementReports.Infrastructure/SettlementReports_v2/RemoveExpiredSettlementReports.cs index 3c0d3eb..7a35dc0 100644 --- a/source/settlement-report/SettlementReports.Infrastructure/SettlementReports_v2/RemoveExpiredSettlementReports.cs +++ b/source/settlement-report/SettlementReports.Infrastructure/SettlementReports_v2/RemoveExpiredSettlementReports.cs @@ -47,24 +47,14 @@ public async Task RemoveExpiredAsync(IList _userContext; public SettlementReportsController( IRequestSettlementReportJobHandler requestSettlementReportJobHandler, IUserContext userContext, - IListSettlementReportJobsHandler listSettlementReportJobsHandler) + IListSettlementReportJobsHandler listSettlementReportJobsHandler, + ISettlementReportJobsDownloadHandler downloadHandler) { _requestSettlementReportJobHandler = requestSettlementReportJobHandler; _userContext = userContext; _listSettlementReportJobsHandler = listSettlementReportJobsHandler; + _downloadHandler = downloadHandler; } [HttpPost] @@ -107,6 +113,32 @@ public async Task> ListSettlementRepor return await _listSettlementReportJobsHandler.HandleAsync(_userContext.CurrentUser.Actor.ActorId).ConfigureAwait(false); } + [HttpGet] + [Route("download")] + [Authorize] + [Produces("application/octet-stream")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + public async Task DownloadFileAsync(SettlementReportRequestId requestId) + { + try + { + var stream = new MemoryStream(); + await _downloadHandler + .DownloadReportAsync( + requestId, + () => stream, + _userContext.CurrentUser.Actor.ActorId, + _userContext.CurrentUser.MultiTenancy) + .ConfigureAwait(false); + + return File(stream.GetBuffer(), MediaTypeNames.Application.Zip); + } + catch (Exception ex) when (ex is InvalidOperationException or RequestFailedException) + { + return NotFound(); + } + } + private bool IsValid(SettlementReportRequestDto req) { if (_userContext.CurrentUser.MultiTenancy) diff --git a/source/settlement-report/SettlementReports.WebAPI/Extensions/DependencyInjection/SettlementReportModuleExtensions.cs b/source/settlement-report/SettlementReports.WebAPI/Extensions/DependencyInjection/SettlementReportModuleExtensions.cs index 01d5e89..7a25dd3 100644 --- a/source/settlement-report/SettlementReports.WebAPI/Extensions/DependencyInjection/SettlementReportModuleExtensions.cs +++ b/source/settlement-report/SettlementReports.WebAPI/Extensions/DependencyInjection/SettlementReportModuleExtensions.cs @@ -44,6 +44,7 @@ public static IServiceCollection AddSettlementReportApiModule(this IServiceColle services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSettlementReportBlobStorage(); // Database Health check