From 6bccb57de7701aabe745a6ee047c06260248e973 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:45:29 +0200 Subject: [PATCH 1/9] Add _departmentFilter to sender --- .../Functions/WeeklyDepartmentSummarySender.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs index b97d76f9e..b9f46cc6a 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs @@ -22,6 +22,7 @@ public class WeeklyDepartmentSummarySender private readonly IConfiguration configuration; private int _maxDegreeOfParallelism; + private readonly string[] _departmentFilter; public WeeklyDepartmentSummarySender(ISummaryApiClient summaryApiClient, INotificationApiClient notificationApiClient, ILogger logger, IConfiguration configuration) @@ -32,12 +33,15 @@ public WeeklyDepartmentSummarySender(ISummaryApiClient summaryApiClient, INotifi this.configuration = configuration; _maxDegreeOfParallelism = int.TryParse(configuration["weekly-department-summary-sender-parallelism"], out var result) ? result : 2; + _departmentFilter = configuration["departmentFilter"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? ["PRD"]; } [FunctionName("weekly-department-summary-sender")] public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] TimerInfo timerInfo) { - var departments = await summaryApiClient.GetDepartmentsAsync(); + var departments = (await summaryApiClient.GetDepartmentsAsync()) + ?.Where(d => _departmentFilter.Any(df => d.FullDepartmentName.Contains(df))) + .ToArray(); if (departments is null || !departments.Any()) { From 2e5e7bd5896ac33cdd9bf53ac7530e63c7658def Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:32:49 +0200 Subject: [PATCH 2/9] Added more logging, changed log levels for some existing logs. Allow DepartmentsFilter to be empty --- .../Functions/DepartmentResourceOwnerSync.cs | 21 +++++++++++---- .../WeeklyDepartmentSummarySender.cs | 26 ++++++++++++------- .../WeeklyDepartmentSummaryWorker.cs | 4 ++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs index 35b26e1dc..48a552f60 100644 --- a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs @@ -52,7 +52,7 @@ public DepartmentResourceOwnerSync( { _totalBatchTime = TimeSpan.FromHours(4.5); - logger.LogWarning("Configuration variable 'total_batch_time_in_minutes' not found, batching messages over {BatchTime}", _totalBatchTime); + logger.LogInformation("Configuration variable 'total_batch_time_in_minutes' not found, batching messages over {BatchTime}", _totalBatchTime); } } @@ -72,13 +72,19 @@ public async Task RunAsync( var client = new ServiceBusClient(_serviceBusConnectionString); var sender = client.CreateSender(_weeklySummaryQueueName); + logger.LogInformation("weekly-department-recipients-sync fetching departments with department filter {DepartmentFilter}", JsonConvert.SerializeObject(_departmentFilter, Formatting.Indented)); + // Fetch all departments var departments = (await lineOrgApiClient.GetOrgUnitDepartmentsAsync()) .DistinctBy(d => d.SapId) .Where(d => d.FullDepartment != null && d.SapId != null) - .Where(d => _departmentFilter.Any(df => d.FullDepartment.Contains(df))) - .Where(d => d.Management.Persons.Length > 0); + .Where(d => d.Management.Persons.Length > 0) + .ToArray(); + + if (_departmentFilter.Length != 0) + departments = departments.Where(d => _departmentFilter.Any(df => d.FullDepartment!.Contains(df))).ToArray(); + logger.LogInformation("Found departments {Departments}", JsonConvert.SerializeObject(departments, Formatting.Indented)); var apiDepartments = new List(); @@ -113,12 +119,13 @@ public async Task RunAsync( { try { + //TODO: Do one batch update instead of individual updates // Update the database await summaryApiClient.PutDepartmentAsync(department, cancellationToken); } catch (Exception e) { - logger.LogError(e, "Failed to PUT department {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); + logger.LogCritical(e, "Failed to PUT department {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); continue; } @@ -129,9 +136,11 @@ public async Task RunAsync( } catch (Exception e) { - logger.LogError(e, "Failed to send department to queue {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); + logger.LogCritical(e, "Failed to send department to queue {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); } } + + logger.LogInformation("weekly-department-recipients-sync completed"); } @@ -156,6 +165,8 @@ private Dictionary CalculateDepartme var currentTime = DateTimeOffset.UtcNow; var minutesPerReportSlice = _totalBatchTime.TotalMinutes / apiDepartments.Count; + logger.LogInformation("Minutes allocated for each worker: {MinutesPerReportSlice}", minutesPerReportSlice); + var departmentDelayMapping = new Dictionary(); foreach (var department in apiDepartments) { diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs index b9f46cc6a..8bfb2b49d 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs @@ -39,11 +39,17 @@ public WeeklyDepartmentSummarySender(ISummaryApiClient summaryApiClient, INotifi [FunctionName("weekly-department-summary-sender")] public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] TimerInfo timerInfo) { - var departments = (await summaryApiClient.GetDepartmentsAsync()) - ?.Where(d => _departmentFilter.Any(df => d.FullDepartmentName.Contains(df))) - .ToArray(); + logger.LogInformation("weekly-department-summary-sender started with department filter {DepartmentFilter}", JsonConvert.SerializeObject(_departmentFilter, Formatting.Indented)); - if (departments is null || !departments.Any()) + // TODO: Use OData query to filter departments + var departments = await summaryApiClient.GetDepartmentsAsync(); + + if (_departmentFilter.Length != 0) + { + departments = departments?.Where(d => _departmentFilter.Contains(d.DepartmentSapId)).ToArray(); + } + + if (departments is null || departments.Count == 0) { logger.LogCritical("No departments found. Exiting"); return; @@ -56,6 +62,8 @@ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] // Use Parallel.ForEachAsync to easily limit the number of parallel requests await Parallel.ForEachAsync(departments, options, async (department, _) => await CreateAndSendNotificationsAsync(department)); + + logger.LogInformation("weekly-department-summary-sender completed"); } private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment department) @@ -68,7 +76,7 @@ private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment de if (summaryReport is null) { - logger.LogCritical( + logger.LogInformation( "No summary report found for department {Department}. Unable to send report notification", JsonConvert.SerializeObject(department, Formatting.Indented)); return; @@ -76,7 +84,7 @@ private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment de } catch (Exception e) { - logger.LogError(e, "Failed to get summary report for department {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); + logger.LogCritical(e, "Failed to get summary report for department {Department}", JsonConvert.SerializeObject(department, Formatting.Indented)); return; } @@ -87,7 +95,7 @@ private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment de } catch (Exception e) { - logger.LogError(e, "Failed to create notification for department {DepartmentSapId} | Report {Report}", department.DepartmentSapId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); + logger.LogCritical(e, "Failed to create notification for department {DepartmentSapId} | Report {Report}", department.DepartmentSapId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); return; } @@ -99,11 +107,11 @@ private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment de { var result = await notificationApiClient.SendNotification(notification, azureId); if (!result) - logger.LogError("Failed to send notification to user with AzureId {AzureId} | Report {Report}", azureId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); + logger.LogCritical("Failed to send notification to user with AzureId {AzureId} | Report {Report}", azureId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); } catch (Exception e) { - logger.LogError(e, "Failed to send notification to user with AzureId {AzureId} | Report {Report}", azureId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); + logger.LogCritical(e, "Failed to send notification to user with AzureId {AzureId} | Report {Report}", azureId, JsonConvert.SerializeObject(summaryReport, Formatting.Indented)); } } } diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummaryWorker.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummaryWorker.cs index fcc322d92..b9b118d28 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummaryWorker.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummaryWorker.cs @@ -32,6 +32,7 @@ public async Task RunAsync( [ServiceBusTrigger("%department_summary_weekly_queue%", Connection = "AzureWebJobsServiceBus")] ServiceBusReceivedMessage message, ServiceBusMessageActions messageReceiver) { + _logger.LogInformation("weekly-department-summary-worker started with message: {MessageBody}", message.Body); try { var dto = await JsonSerializer.DeserializeAsync(message.Body.ToStream()); @@ -50,6 +51,7 @@ public async Task RunAsync( { // Complete the message regardless of outcome. await messageReceiver.CompleteMessageAsync(message); + _logger.LogInformation("weekly-department-summary-worker completed"); } } @@ -66,7 +68,7 @@ private async Task CreateAndStoreReportAsync(ApiResourceOwnerDepartment message) // Check if the department has personnel, abort if not if (departmentPersonnel.Count == 0) { - _logger.LogInformation("Department contains no personnel, no need to store report"); + _logger.LogInformation("Department {Department} contains no valid personnel, no need to store report", message.FullDepartmentName); return; } From 79f93f2900bd0257f0dfe42ea9190c5cf9315825 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:48:40 +0200 Subject: [PATCH 3/9] Copied old notification tests to new summary notification tests --- src/Fusion.Resources.sln | 7 + .../Fusion.Summary.Functions.Tests.csproj | 28 ++ .../Mock/NotificationReportApiResponseMock.cs | 110 ++++++++ ...ScheduledReportNotificationBuilderTests.cs | 246 ++++++++++++++++++ 4 files changed, 391 insertions(+) create mode 100644 src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj create mode 100644 src/tests/Fusion.Summary.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs create mode 100644 src/tests/Fusion.Summary.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs diff --git a/src/Fusion.Resources.sln b/src/Fusion.Resources.sln index e99eb061c..416a6f9ef 100644 --- a/src/Fusion.Resources.sln +++ b/src/Fusion.Resources.sln @@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fusion.Resources.Infrastruc EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{3C253096-BCFA-40D7-8C3F-F3F3B3BA4F1C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fusion.Summary.Functions.Tests", "tests\Fusion.Summary.Functions.Tests\Fusion.Summary.Functions.Tests.csproj", "{F819CC16-861C-4E8E-97FB-4B0928BD8704}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -163,6 +165,10 @@ Global {53F9FAF1-EFA4-4951-9693-1E350F6428A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {53F9FAF1-EFA4-4951-9693-1E350F6428A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {53F9FAF1-EFA4-4951-9693-1E350F6428A8}.Release|Any CPU.Build.0 = Release|Any CPU + {F819CC16-861C-4E8E-97FB-4B0928BD8704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F819CC16-861C-4E8E-97FB-4B0928BD8704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F819CC16-861C-4E8E-97FB-4B0928BD8704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F819CC16-861C-4E8E-97FB-4B0928BD8704}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +199,7 @@ Global {0F9CC682-2DD6-4422-A321-D7100780A739} = {286E3C76-C860-4661-9AEF-817458D1231B} {3C253096-BCFA-40D7-8C3F-F3F3B3BA4F1C} = {996B233A-1FC5-4CF5-97AD-B9C09F3E0A53} {CC1EF38A-2741-4D92-A33A-3970E6F73797} = {3C253096-BCFA-40D7-8C3F-F3F3B3BA4F1C} + {F819CC16-861C-4E8E-97FB-4B0928BD8704} = {D12DC33D-BB83-40E4-9F36-18F59351A21D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F47D8FDB-3919-45C8-8C08-486405ECEC01} diff --git a/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj new file mode 100644 index 000000000..16ca89022 --- /dev/null +++ b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs new file mode 100644 index 000000000..75afae2b0 --- /dev/null +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs @@ -0,0 +1,110 @@ +using Fusion.Resources.Functions.Common.ApiClients; + +namespace Fusion.Summary.Functions.Tests.Notifications.Mock; + +public abstract class NotificationReportApiResponseMock +{ + public static List GetMockedInternalPersonnel( + double personnelCount, + double workload, + double otherTasks, + double vacationLeave, + double absenceLeave) + { + var personnel = new List(); + for (var i = 0; i < personnelCount; i++) + { + personnel.Add(new IResourcesApiClient.InternalPersonnelPerson() + { + EmploymentStatuses = new List + { + new() + { + Type = IResourcesApiClient.ApiAbsenceType.Vacation, + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AbsencePercentage = vacationLeave + }, + new() + { + Type = IResourcesApiClient.ApiAbsenceType.OtherTasks, + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AbsencePercentage = otherTasks + }, + new() + { + Type = IResourcesApiClient.ApiAbsenceType.Absence, + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AbsencePercentage = absenceLeave + } + }, + PositionInstances = new List + { + new() + { + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + Workload = workload + } + } + } + ); + ; + } + + return personnel; + } + + public static List GetMockedInternalPersonnelWithInstancesWithAndWithoutChanges(double personnelCount) + { + var personnel = new List(); + for (var i = 0; i < personnelCount; i++) + { + personnel.Add(new IResourcesApiClient.InternalPersonnelPerson() + { + // Should return 4 instances for each person + PositionInstances = new List + { + new() + { + // One active instance without any changes + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AllocationState = null, + AllocationUpdated = null + }, + new() + { + // One active instance that contains changes done within the last week + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AllocationState = "ChangeByTaskOwner", + AllocationUpdated = DateTime.UtcNow + }, + new() + { + // One active instance that contains changes done more than a week ago + AppliesFrom = DateTime.UtcNow.AddDays(-1 - i), + AppliesTo = DateTime.UtcNow.AddDays(1 + i * 10), + AllocationState = "ChangeByTaskOwner", + AllocationUpdated = DateTime.UtcNow.AddDays(-8) + }, + new() + { + // One instance that will become active in more than 3 months that contains changes + AppliesFrom = DateTime.UtcNow.AddMonths(4), + AppliesTo = DateTime.UtcNow.AddMonths(4 + i), + AllocationState = "ChangeByTaskOwner", + AllocationUpdated = DateTime.UtcNow + } + } + } + ); + ; + } + + return personnel; + } +} \ No newline at end of file diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs new file mode 100644 index 000000000..c4ea51a1c --- /dev/null +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs @@ -0,0 +1,246 @@ +using FluentAssertions; +using Fusion.ApiClients.Org; +using Fusion.Resources.Functions.Common.ApiClients; +using Fusion.Summary.Functions.ReportCreator; +using Fusion.Summary.Functions.Tests.Notifications.Mock; + +namespace Fusion.Summary.Functions.Tests.Notifications; + +public class ScheduledReportNotificationBuilderTests +{ + [Fact] + public void GetChangesForDepartment_Should_ResultInNumberOfChanges() + { + // Arrange + const int personnelCount = 4; + // Returns 4 different instances pr personnel + var personnel = NotificationReportApiResponseMock.GetMockedInternalPersonnelWithInstancesWithAndWithoutChanges( + personnelCount); + + // Act + var changes = ResourceOwnerReportDataCreator.CalculateDepartmentChangesLastWeek(personnel); + + // Assert + changes.Should().Be(8); + } + + [Fact] + public void GetCapacityInUse_ShouldReturnCorrectCapacityInUse() + { + // Arrange + const int personnelCount = 4; + const int workload = 80; + const int otherTasks = 4; + const int vacationLeave = 2; + const int absenceLeave = 3; + var personnel = NotificationReportApiResponseMock.GetMockedInternalPersonnel( + personnelCount, + workload, + otherTasks, + vacationLeave, + absenceLeave); + + // Act + var capacityInUse = ResourceOwnerReportDataCreator.GetCapacityInUse(personnel); + var capacityInUseCalculated = (int)Math.Round((double)(workload + otherTasks) / + (100 - (vacationLeave + absenceLeave)) * 100); + + // Assert + capacityInUse.Should().Be(capacityInUseCalculated); + } + + [Fact] + public void GetNumberOfRequestsLastWeek_ShouldReturnCorrectNumberOfRequests() + { + // Arrange + var requests = new List + { + // Will pass + new() + { + Type = RequestType.Allocation.ToString(), + IsDraft = false, + Created = DateTimeOffset.UtcNow.AddDays(-1) + }, + new() + { + Type = RequestType.Allocation.ToString(), + IsDraft = false, + Created = DateTimeOffset.UtcNow.AddDays(-8) + }, + new() + { + Type = RequestType.Allocation.ToString(), + IsDraft = true, + Created = DateTimeOffset.UtcNow.AddDays(-1) + }, + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + IsDraft = false, + Created = DateTimeOffset.UtcNow.AddDays(-1) + } + }; + + // Act + var numberOfRequests = ResourceOwnerReportDataCreator.GetNumberOfRequestsLastWeek(requests); + + // Assert + numberOfRequests.Should().Be(1); + } + + [Fact] + public void GetNumberOfOpenRequests_ShouldReturnCorrectNumberOfOpenRequests() + { + // Arrange + var requests = new List + { + // Will pass + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString() + }, + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + State = RequestState.Created.ToString(), + ProposedPerson = new IResourcesApiClient.ProposedPerson() + { Person = new IResourcesApiClient.InternalPersonnelPerson() { AzureUniquePersonId = new Guid() } } + }, + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + State = RequestState.Created.ToString() + }, + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Completed.ToString() + } + }; + + + // Act + var numberOfOpenRequests = ResourceOwnerReportDataCreator.GetNumberOfOpenRequests(requests); + + // Assert + numberOfOpenRequests.Should().Be(1); + } + + [Fact] + public void GetNumberOfRequestsStartingInMoreThanThreeMonths_ShouldReturnCorrectNumberOfRequests() + { + // Arrange + var requests = new List + { + // Will pass + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(4) } + }, + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(2) } + }, + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + State = RequestState.Completed.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(4) } + } + }; + + + // Act + var numberOfRequests = + ResourceOwnerReportDataCreator.GetNumberOfRequestsStartingInMoreThanThreeMonths(requests); + + // Assert + numberOfRequests.Should().Be(1); + } + + [Fact] + public void GetTotalNumberOfPersonnel_ShouldReturnCorrectNumberOfPersonnel() + { + // Arrange + var personnel = NotificationReportApiResponseMock.GetMockedInternalPersonnel(5, 100, 0, 0, 0); + + // Act + var totalNumberOfPersonnel = ResourceOwnerReportDataCreator.GetTotalNumberOfPersonnel(personnel); + + // Assert + totalNumberOfPersonnel.Should().Be(5); + } + + [Fact] + public void GetNumberOfRequestsStartingInLessThanThreeMonths_ShouldReturnCorrectNumberOfRequests() + { + // Arrange + var requests = new List + { + // Will pass + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(2) } + }, + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(4) } + }, + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + State = RequestState.Completed.ToString(), + OrgPositionInstance = new ApiPositionInstanceV2 { AppliesFrom = DateTime.UtcNow.AddMonths(2) } + } + }; + + // Act + var numberOfRequests = + ResourceOwnerReportDataCreator.GetNumberOfRequestsStartingInLessThanThreeMonths(requests); + + // Assert + numberOfRequests.Should().Be(1); + } + + [Fact] + public void GetAllocationChangesAwaitingTaskOwnerAction_ShouldReturnCorrectNumberOfRequests() + { + // Arrange + var requests = new List + { + // Will pass + new() + { + Type = RequestType.ResourceOwnerChange.ToString(), + State = RequestState.Created.ToString() + }, + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Completed.ToString() + }, + new() + { + Type = RequestType.Allocation.ToString(), + State = RequestState.Created.ToString() + } + }; + + // Act + var numberOfRequests = + ResourceOwnerReportDataCreator.GetAllocationChangesAwaitingTaskOwnerAction(requests); + + // Assert + numberOfRequests.Should().Be(1); + } +} \ No newline at end of file From fde794abfb4af97f01b29a916551f969d9f9d454 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:51:29 +0200 Subject: [PATCH 4/9] Removed ToArray calls --- .../Functions/DepartmentResourceOwnerSync.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs index 48a552f60..99f735602 100644 --- a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs @@ -78,11 +78,10 @@ public async Task RunAsync( var departments = (await lineOrgApiClient.GetOrgUnitDepartmentsAsync()) .DistinctBy(d => d.SapId) .Where(d => d.FullDepartment != null && d.SapId != null) - .Where(d => d.Management.Persons.Length > 0) - .ToArray(); + .Where(d => d.Management.Persons.Length > 0); if (_departmentFilter.Length != 0) - departments = departments.Where(d => _departmentFilter.Any(df => d.FullDepartment!.Contains(df))).ToArray(); + departments = departments.Where(d => _departmentFilter.Any(df => d.FullDepartment!.Contains(df))); logger.LogInformation("Found departments {Departments}", JsonConvert.SerializeObject(departments, Formatting.Indented)); From bdd50941b494d118f702543abefc61735ea86d97 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:26:05 +0200 Subject: [PATCH 5/9] Added nuget.config to test project --- .../Fusion.Summary.Functions.Tests.csproj | 8 ++++++++ src/tests/Fusion.Summary.Functions.Tests/nuget.config | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/tests/Fusion.Summary.Functions.Tests/nuget.config diff --git a/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj index 16ca89022..5b0d6a63c 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj +++ b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj @@ -25,4 +25,12 @@ + + + PreserveNewest + true + PreserveNewest + + + diff --git a/src/tests/Fusion.Summary.Functions.Tests/nuget.config b/src/tests/Fusion.Summary.Functions.Tests/nuget.config new file mode 100644 index 000000000..84f4b4aa5 --- /dev/null +++ b/src/tests/Fusion.Summary.Functions.Tests/nuget.config @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file From 55d8285b9354025e0aed5f0fa50c68b50f0185d8 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:03:33 +0200 Subject: [PATCH 6/9] Update new test project --- .../Fusion.Summary.Functions.Tests.csproj | 23 +++++++++++++++---- .../xunit.runner.json | 3 +++ 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 src/tests/Fusion.Summary.Functions.Tests/xunit.runner.json diff --git a/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj index 5b0d6a63c..cffc36c25 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj +++ b/src/tests/Fusion.Summary.Functions.Tests/Fusion.Summary.Functions.Tests.csproj @@ -10,11 +10,21 @@ - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -25,6 +35,9 @@ + + + PreserveNewest diff --git a/src/tests/Fusion.Summary.Functions.Tests/xunit.runner.json b/src/tests/Fusion.Summary.Functions.Tests/xunit.runner.json new file mode 100644 index 000000000..f78bc2f0c --- /dev/null +++ b/src/tests/Fusion.Summary.Functions.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} \ No newline at end of file From 6c9e1d59f374487e8193d284208599480cf3c165 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:20:50 +0200 Subject: [PATCH 7/9] Changed log level --- .../Functions/WeeklyDepartmentSummarySender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs index 8bfb2b49d..4ccfe69ee 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs @@ -76,7 +76,7 @@ private async Task CreateAndSendNotificationsAsync(ApiResourceOwnerDepartment de if (summaryReport is null) { - logger.LogInformation( + logger.LogWarning( "No summary report found for department {Department}. Unable to send report notification", JsonConvert.SerializeObject(department, Formatting.Indented)); return; From 34f41b046cf0244ec6ae81d6c1a963faaf4ac62d Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:49:38 +0200 Subject: [PATCH 8/9] Fixed wrong "where" filter --- .../Functions/WeeklyDepartmentSummarySender.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs index 4ccfe69ee..7e78090fb 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs @@ -45,9 +45,7 @@ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] var departments = await summaryApiClient.GetDepartmentsAsync(); if (_departmentFilter.Length != 0) - { - departments = departments?.Where(d => _departmentFilter.Contains(d.DepartmentSapId)).ToArray(); - } + departments = departments?.Where(d => _departmentFilter.Any(df => d.FullDepartmentName!.Contains(df))).ToArray(); if (departments is null || departments.Count == 0) { From 33af4e5e503fd397c43cd12386420e306f762fe7 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:51:22 +0200 Subject: [PATCH 9/9] Updated check for checking if there are any recipients for a notification --- .../Functions/DepartmentResourceOwnerSync.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs index 99f735602..7f3deff67 100644 --- a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs @@ -77,8 +77,7 @@ public async Task RunAsync( // Fetch all departments var departments = (await lineOrgApiClient.GetOrgUnitDepartmentsAsync()) .DistinctBy(d => d.SapId) - .Where(d => d.FullDepartment != null && d.SapId != null) - .Where(d => d.Management.Persons.Length > 0); + .Where(d => d.FullDepartment != null && d.SapId != null); if (_departmentFilter.Length != 0) departments = departments.Where(d => _departmentFilter.Any(df => d.FullDepartment!.Contains(df))); @@ -100,6 +99,13 @@ public async Task RunAsync( .Distinct() .ToArray(); + var recipients = resourceOwners.Concat(delegatedResponsibles).ToArray(); + if (recipients.Length == 0) + { + logger.LogInformation("Skipping department {Department} as it has no resource owners or delegated responsibles", orgUnit.FullDepartment); + continue; + } + apiDepartments.Add(new ApiResourceOwnerDepartment() { DepartmentSapId = orgUnit.SapId!,