diff --git a/src/backend/function/Fusion.Resources.Functions/Deployment/disabled-functions.json b/src/backend/function/Fusion.Resources.Functions/Deployment/disabled-functions.json index 0ffa84289..0cb329692 100644 --- a/src/backend/function/Fusion.Resources.Functions/Deployment/disabled-functions.json +++ b/src/backend/function/Fusion.Resources.Functions/Deployment/disabled-functions.json @@ -5,23 +5,14 @@ }, { "environment": "ci", - "disabledFunctions": [ - "scheduled-report-content-Builder-function", - "scheduled-report-timer-trigger-function" - ] + "disabledFunctions": [] }, { "environment": "fqa", - "disabledFunctions": [ - "scheduled-report-content-Builder-function", - "scheduled-report-timer-trigger-function" - ] + "disabledFunctions": [] }, { "environment": "fprd", - "disabledFunctions": [ - "scheduled-report-content-Builder-function", - "scheduled-report-timer-trigger-function" - ] + "disabledFunctions": [] } ] diff --git a/src/backend/function/Fusion.Resources.Functions/Deployment/function.template.json b/src/backend/function/Fusion.Resources.Functions/Deployment/function.template.json index 5155b2892..0aea73c18 100644 --- a/src/backend/function/Fusion.Resources.Functions/Deployment/function.template.json +++ b/src/backend/function/Fusion.Resources.Functions/Deployment/function.template.json @@ -18,7 +18,6 @@ "org": "https://org.ci.api.fusion-dev.net", "people": "https://people.ci.api.fusion-dev.net", "resources": "[concat('https://fra-resources.', parameters('env-name'), '.api.fusion-dev.net')]", - "notifications": "https://notification.ci.api.fusion-dev.net", "context": "https://context.ci.api.fusion-dev.net", "portal": "https://fusion.ci.fusion-dev.net" }, @@ -27,7 +26,6 @@ }, "queues": { "provisionPosition": "provision-position", - "scheduledNotificationReportQueue": "scheduled-notification" } } }, @@ -141,10 +139,6 @@ "name": "Endpoints_people", "value": "[parameters('settings').endpoints.people]" }, - { - "name": "Endpoints_notifications", - "value": "[parameters('settings').endpoints.notifications]" - }, { "name": "Endpoints_context", "value": "[parameters('settings').endpoints.context]" @@ -161,17 +155,9 @@ "name": "Endpoints_Resources_Fusion", "value": "[parameters('settings').resources.fusion]" }, - { - "name": "scheduled_notification_report_queue", - "value": "[parameters('settings').queues.scheduledNotificationReportQueue]" - }, { "name": "provision_position_queue", "value": "[parameters('settings').queues.provisionPosition]" - }, - { - "name": "total_batch_time_in_minutes", - "value": "270" } ] } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/AdaptiveCardBuilder.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/AdaptiveCardBuilder.cs deleted file mode 100644 index 746af9381..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/AdaptiveCardBuilder.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AdaptiveCards; - -namespace Fusion.Resources.Functions.Functions.Notifications; - -public class AdaptiveCardBuilder -{ - private readonly AdaptiveCard _adaptiveCard = new(new AdaptiveSchemaVersion(1, 2)); - - public AdaptiveCardBuilder AddHeading(string text) - { - var heading = new AdaptiveTextBlock - { - Text = text, - Wrap = true, - HorizontalAlignment = AdaptiveHorizontalAlignment.Center, - Separator = true, - Size = AdaptiveTextSize.Large, - Weight = AdaptiveTextWeight.Bolder - }; - _adaptiveCard.Body.Add(heading); - return this; - } - - public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, string customText = "") - { - var container = new AdaptiveContainer() - { - Separator = true, - Items = new List() - { - new AdaptiveTextBlock - { - Text = $"{valueText} {customText}", - Wrap = true, - HorizontalAlignment = AdaptiveHorizontalAlignment.Center, - Size = AdaptiveTextSize.ExtraLarge - }, - new AdaptiveTextBlock - { - Text = headerText, - Wrap = true, - HorizontalAlignment = AdaptiveHorizontalAlignment.Center - } - } - }; - - _adaptiveCard.Body.Add(container); - return this; - } - - public AdaptiveCardBuilder AddListContainer(string headerText, - List> objectLists) - { - var listContainer = new AdaptiveContainer - { - Separator = true, - }; - - var header = new AdaptiveTextBlock - { - Weight = AdaptiveTextWeight.Bolder, - Text = headerText, - Wrap = true, - Size = AdaptiveTextSize.Large, - HorizontalAlignment = AdaptiveHorizontalAlignment.Center - }; - - var rows = new List(); - - foreach (var listObject in objectLists) - { - var row = new AdaptiveColumnSet() - { - Columns = listObject.Select(o => new AdaptiveColumn() - { - Width = AdaptiveColumnWidth.Stretch, - Items = new List - { - new AdaptiveTextBlock - { - Text = o.Value, - Wrap = true, - HorizontalAlignment = o.Alignment - } - } - }).ToList() - }; - - rows.Add(row); - } - - listContainer.Items.Add(header); - listContainer.Items.AddRange(rows); - - - _adaptiveCard.Body.Add(listContainer); - return this; - } - - public AdaptiveCardBuilder AddActionButton(string title, string url) - { - var action = new AdaptiveOpenUrlAction() - { - Title = title, - Url = new Uri(url) - }; - - _adaptiveCard.Actions.Add(action); - - return this; - } - - public AdaptiveCardBuilder AddNewLine() - { - var container = new AdaptiveContainer() - { - Separator = true - }; - - _adaptiveCard.Body.Add(container); - - return this; - } - - public AdaptiveCard Build() - { - return _adaptiveCard; - } - - - public class ListObject - { - public string Value { get; set; } - public AdaptiveHorizontalAlignment Alignment { get; set; } - } -} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/DTOs/ScheduledNotificationQueueDto.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/DTOs/ScheduledNotificationQueueDto.cs deleted file mode 100644 index 85d0545a5..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/DTOs/ScheduledNotificationQueueDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport.DTOs; - -public class ScheduledNotificationQueueDto -{ - public IEnumerable AzureUniqueId { get; set; } - public string FullDepartment { get; set; } - public string DepartmentSapId { get; set; } -} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ResourceOwnerReportDataCreator.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ResourceOwnerReportDataCreator.cs deleted file mode 100644 index 28dc2926f..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ResourceOwnerReportDataCreator.cs +++ /dev/null @@ -1,317 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Fusion.Resources.Functions.Common.ApiClients; - -namespace Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport; - -public abstract class ResourceOwnerReportDataCreator -{ - public static int GetTotalNumberOfPersonnel( - IEnumerable listOfInternalPersonnel) - { - return listOfInternalPersonnel.Count(); - } - - public static int GetCapacityInUse( - List listOfInternalPersonnel) - { - var actualWorkLoad = 0.0; - var actualLeave = 0.0; - foreach (var personnel in listOfInternalPersonnel) - { - actualWorkLoad += personnel.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); - - actualWorkLoad += personnel.EmploymentStatuses - .Where(ab => ab.Type == IResourcesApiClient.ApiAbsenceType.OtherTasks && ab.IsActive) - .Select(ab => ab.AbsencePercentage) - .Sum() ?? 0; - - actualLeave += personnel.EmploymentStatuses - .Where(ab => - ab.Type is IResourcesApiClient.ApiAbsenceType.Absence - or IResourcesApiClient.ApiAbsenceType.Vacation && ab.IsActive) - .Select(ab => ab.AbsencePercentage) - .Sum() ?? 0; - } - - var maximumPotentialWorkLoad = listOfInternalPersonnel.Count * 100; - var potentialWorkLoad = maximumPotentialWorkLoad - actualLeave; - if (potentialWorkLoad <= 0) - return 0; - var capacityInUse = actualWorkLoad / potentialWorkLoad * 100; - if (capacityInUse < 0) - return 0; - - return (int)Math.Round(capacityInUse); - } - - public static int GetNumberOfRequestsLastWeek( - IEnumerable requests) - { - return requests - .Count(req => - req.Type != null && !req.Type.Equals(RequestType.ResourceOwnerChange.ToString(), - StringComparison.OrdinalIgnoreCase) - && req.Created > DateTime.UtcNow.AddDays(-7) && !req.IsDraft); - } - - public static int GetNumberOfOpenRequests( - IEnumerable requests) - => requests.Count(req => - req.State != null && req.Type != null && !req.Type.Equals(RequestType.ResourceOwnerChange.ToString(), - StringComparison.OrdinalIgnoreCase) && - !req.HasProposedPerson && - !req.State.Equals(RequestState.Completed.ToString(), StringComparison.OrdinalIgnoreCase)); - - - public static int GetNumberOfRequestsStartingInMoreThanThreeMonths( - IEnumerable requests) - { - var threeMonthsFromToday = DateTime.UtcNow.AddMonths(3); - return requests - .Count(x => x.Type != null && - x.OrgPositionInstance != null && - x.State != null && - !x.State.Equals(RequestState.Completed.ToString(), StringComparison.OrdinalIgnoreCase) && - !x.Type.Equals(RequestType.ResourceOwnerChange.ToString(), - StringComparison.OrdinalIgnoreCase) && - x.OrgPositionInstance.AppliesFrom > threeMonthsFromToday); - } - - public static int GetNumberOfRequestsStartingInLessThanThreeMonths( - IEnumerable requests) - { - var threeMonthsFromToday = DateTime.UtcNow.AddMonths(3); - var today = DateTime.UtcNow; - return requests - .Count(x => x.Type != null && - x.OrgPositionInstance != null && - x.State != null && - !x.State.Equals(RequestState.Completed.ToString(), StringComparison.OrdinalIgnoreCase) && - !x.Type.Equals(RequestType.ResourceOwnerChange.ToString(), - StringComparison.OrdinalIgnoreCase) && - x.OrgPositionInstance.AppliesFrom < threeMonthsFromToday && - x.OrgPositionInstance.AppliesFrom > today && !x.HasProposedPerson); - } - - public static int GetAverageTimeToHandleRequests( - IEnumerable requests) - { - /* - * Average time to handle request: average number of days from request created/sent to candidate is proposed - last 12 months - * Calculation: - * We find all the requests for the last 12 months that are not of type "ResourceOwnerChange" (for the specific Department) - * For each of these request we find the number of days that it takes from when a requests is created (which is when the request is sent from Task Owner to ResourceOwner) - * to the requests is handeled by ResourceOwner (a person is proposed). If the request is still being processed it will not have a date for when - * it is handled (proposed date), and then we will use todays date. - * We then sum up the total amount of days used to handle a request and divide by the total number of requests for which we have found the handle-time - */ - - var requestsHandledByResourceOwner = 0; - var totalNumberOfDays = 0.0; - var twelveMonths = DateTime.UtcNow.AddMonths(-12); - - - // Not to include requests that are sent by ResourceOwners (ResourceOwnerChange) or requests created more than 3 months ago - var requestsLastTwelveMonthsWithoutResourceOwnerChangeRequest = requests - .Where(req => req.Created > twelveMonths) - .Where(r => r.Workflow is not null) - .Where(_ => true) - .Where(req => req.Type != null && !req.Type.Equals(RequestType.ResourceOwnerChange.ToString(), - StringComparison.OrdinalIgnoreCase)); - - foreach (var request in requestsLastTwelveMonthsWithoutResourceOwnerChangeRequest) - { - // If the requests doesnt have state it means that it is in draft. Do not need to check these - if (request.State == null) - continue; - if (request.Workflow?.Steps is null) - continue; - - // First: find the date for creation (this implies that the request has been sent to resourceowner) - var dateForCreation = request.Workflow.Steps - .FirstOrDefault(step => step.Name.Equals("Created") && step.IsCompleted)?.Completed.Value.DateTime; - - if (dateForCreation == null) - continue; - - //Second: Try to find the date for proposed (this implies that resourceowner have handled the request) - var dateOfApprovalOrToday = request.Workflow.Steps - .FirstOrDefault(step => step.Name.Equals("Proposed") && step.IsCompleted)?.Completed.Value.DateTime; - - // if there are no proposal date we will used todays date for calculation - dateOfApprovalOrToday = dateOfApprovalOrToday ?? DateTime.UtcNow; - - - requestsHandledByResourceOwner++; - var timespanDifference = dateOfApprovalOrToday - dateForCreation; - var differenceInDays = timespanDifference.Value.TotalDays; - totalNumberOfDays += differenceInDays; - } - - if (requestsHandledByResourceOwner <= 0) - return 0; - - var averageAmountOfTimeDouble = totalNumberOfDays / requestsHandledByResourceOwner; - return (int)Math.Round(averageAmountOfTimeDouble); - } - - public static int GetAllocationChangesAwaitingTaskOwnerAction( - IEnumerable requests) - { - return requests - .Where(req => - req.Type.Equals(RequestType.ResourceOwnerChange.ToString(), StringComparison.OrdinalIgnoreCase)) - .Where(req => - req.State != null && - req.State.Equals(RequestState.Created.ToString(), StringComparison.OrdinalIgnoreCase)) - .ToList() - .Count; - } - - public static int CalculateDepartmentChangesLastWeek(IEnumerable internalPersonnel) - { - /* - * How we calculate the changes: - * Find all active instanses or all instanses for each personnel that starts within 3 months - * To find the instances that have changes related to them: - * Find all instances that have the field "AllocationState" not set to null - * Find all instances where AllocationUpdated > 7 days ago - */ - - var threeMonthsFromToday = DateTime.UtcNow.AddMonths(3); - var today = DateTime.UtcNow; - var weekBackInTime = DateTime.UtcNow.AddDays(-7); - - // Find all active (IsActive) instances or instances that have start date (appliesFrom) > threeMonthsFromToday - var instancesThatAreActiveOrBecomesActiveWithinThreeMonths = internalPersonnel - .SelectMany(per => per.PositionInstances - .Where(pis => (pis.AppliesFrom < threeMonthsFromToday && pis.AppliesFrom > today) || pis.AppliesTo > today || pis.IsActive)); - - var instancesWithAllocationStateSetAndAllocationUpdateWithinLastWeek = instancesThatAreActiveOrBecomesActiveWithinThreeMonths - .Where(per => per.AllocationState != null) - .Where(pos => pos.AllocationUpdated != null && pos.AllocationUpdated > weekBackInTime).ToList(); - - return instancesWithAllocationStateSetAndAllocationUpdateWithinLastWeek.Count(); - } - - public static IEnumerable GetPersonnelPositionsEndingWithNoFutureAllocation( - IEnumerable listOfInternalPersonnel) - { - return listOfInternalPersonnel - .Where(AllocatedPersonWithNoFutureAllocation.GotFutureAllocation) - .Select(AllocatedPersonWithNoFutureAllocation.Create); - } - - public static IEnumerable GetPersonnelAllocatedMoreThan100Percent( - IEnumerable listOfInternalPersonnel) - { - return listOfInternalPersonnel - .Select(AllocatedPersonnelWithWorkLoad.Create) - .Where(p => p.TotalWorkload > 100); - } -} - -public class AllocatedPersonnel -{ - public string FullName { get; } - - protected AllocatedPersonnel(IResourcesApiClient.InternalPersonnelPerson person) - { - FullName = person.Name; - } -} - -public class AllocatedPersonnelWithWorkLoad : AllocatedPersonnel -{ - public double TotalWorkload { get; } - - private AllocatedPersonnelWithWorkLoad(IResourcesApiClient.InternalPersonnelPerson person) : base(person) - { - TotalWorkload = CalculateTotalWorkload(person); - } - - public static AllocatedPersonnelWithWorkLoad Create(IResourcesApiClient.InternalPersonnelPerson person) - { - return new AllocatedPersonnelWithWorkLoad(person); - } - - private double CalculateTotalWorkload(IResourcesApiClient.InternalPersonnelPerson person) - { - var totalWorkLoad = person.EmploymentStatuses - .Where(ab => ab.Type != IResourcesApiClient.ApiAbsenceType.Absence && ab.IsActive) - .Select(ab => ab.AbsencePercentage).Sum(); - totalWorkLoad += person.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); - if (totalWorkLoad is null) - return 0; - - if (totalWorkLoad < 0) - return 0; - - return totalWorkLoad.Value; - } -} - -public class AllocatedPersonWithNoFutureAllocation : AllocatedPersonnel -{ - public DateTime? EndDate { get; } - - - private AllocatedPersonWithNoFutureAllocation(IResourcesApiClient.InternalPersonnelPerson person) : base(person) - { - var endingPosition = person.PositionInstances.Find(instance => instance.IsActive); - if (endingPosition is null) - { - EndDate = null; - return; - } - - EndDate = endingPosition.AppliesTo; - } - - public static AllocatedPersonWithNoFutureAllocation Create(IResourcesApiClient.InternalPersonnelPerson person) - { - return new AllocatedPersonWithNoFutureAllocation(person); - } - - public static bool GotFutureAllocation(IResourcesApiClient.InternalPersonnelPerson person) - { - var gotLongLastingPosition = person.PositionInstances.Any(pdi => pdi.AppliesTo >= DateTime.UtcNow.AddMonths(3)); - if (gotLongLastingPosition) - return false; - - var gotFutureAllocation = person.PositionInstances.Any(pdi => pdi.AppliesFrom > DateTime.UtcNow); - if (gotFutureAllocation) - return false; - - return person.PositionInstances.Any(pdi => pdi.IsActive); - } -} - -public enum RequestState -{ - Approval, - Proposal, - Provisioning, - Created, - Completed -} - -public enum RequestType -{ - Allocation, - ResourceOwnerChange -} - -public enum ChangeType -{ - PositionInstanceCreated, - PersonAssignedToPosition, - PositionInstanceAllocationStateChanged, - PositionInstanceAppliesToChanged, - PositionInstanceAppliesFromChanged, - PositionInstanceParentPositionIdChanged, - PositionInstancePercentChanged, - PositionInstanceLocationChanged, -} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportContentBuilderFunction.cs deleted file mode 100644 index 8783ea52d..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportContentBuilderFunction.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AdaptiveCards; -using Azure.Messaging.ServiceBus; -using Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport.DTOs; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.ServiceBus; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using static Fusion.Resources.Functions.Functions.Notifications.AdaptiveCardBuilder; -using Fusion.Integration.Profile; -using Fusion.Resources.Functions.Common.ApiClients; -using Fusion.Resources.Functions.Common.ApiClients.ApiModels; -using static Fusion.Resources.Functions.Common.ApiClients.IResourcesApiClient; - -namespace Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport; - -public class ScheduledReportContentBuilderFunction -{ - private readonly ILogger _logger; - private readonly INotificationApiClient _notificationsClient; - private readonly IResourcesApiClient _resourceClient; - private readonly IOrgClient _orgClient; - private readonly IConfiguration _configuration; - - public ScheduledReportContentBuilderFunction(ILogger logger, - IResourcesApiClient resourcesApiClient, - INotificationApiClient notificationsClient, - IOrgClient orgClient, IConfiguration configuration) - { - _logger = logger; - _resourceClient = resourcesApiClient; - _notificationsClient = notificationsClient; - _orgClient = orgClient; - _configuration = configuration; - } - - [FunctionName("scheduled-report-content-Builder-function")] - public async Task RunAsync( - [ServiceBusTrigger("%scheduled_notification_report_queue%", Connection = "AzureWebJobsServiceBus")] - ServiceBusReceivedMessage message, ServiceBusMessageActions messageReceiver) - { - _logger.LogInformation( - $"{nameof(ScheduledReportContentBuilderFunction)} " + - $"started with message: {message.Body}"); - - try - { - var body = Encoding.UTF8.GetString(message.Body); - var dto = JsonConvert.DeserializeObject(body); - - if (!dto.AzureUniqueId.Any()) - throw new Exception("There are no recipients. This should have been filtered"); - - if (string.IsNullOrEmpty(dto.FullDepartment)) - throw new Exception("FullDepartmentIdentifier not valid."); - - await BuildContentForResourceOwner(dto.AzureUniqueId, dto.FullDepartment, dto.DepartmentSapId); - - _logger.LogInformation( - $"{nameof(ScheduledReportContentBuilderFunction)} " + - $"finished with message: {message.Body}"); - } - catch (Exception e) - { - _logger.LogError( - $"{nameof(ScheduledReportContentBuilderFunction)} " + - $"failed with exception: {e.Message}"); - } - finally - { - // Complete the message regardless of outcome. - await messageReceiver.CompleteMessageAsync(message); - } - } - - private async Task BuildContentForResourceOwner(IEnumerable azureUniqueIds, string fullDepartment, string departmentSapId) - { - // Requests for department - var departmentRequests = (await _resourceClient.GetAllRequestsForDepartment(fullDepartment)).ToList(); - - // Personnel for the department - var departmentPersonnel = - (await GetPersonnelForDepartmentExludingConsultantAndExternal(fullDepartment)).ToList(); - - // Check if the department has personnel, abort if not - if(departmentPersonnel.Count() == 0) - { - _logger.LogInformation("Department contains no personnel, no need to send notification"); - - return; - } - - // Create notification card - var card = CreateResourceOwnerAdaptiveCard(departmentPersonnel, departmentRequests, fullDepartment, departmentSapId); - - foreach (var azureUniqueIdStr in azureUniqueIds) - { - Guid azureUniqueId = Guid.Empty; - - if(!Guid.TryParse(azureUniqueIdStr, out azureUniqueId)) - { - _logger.LogError($"Unable to parse notification recipient '{azureUniqueIdStr}'"); - - continue; - } - - if( azureUniqueId.Equals(Guid.Empty) ) - { - _logger.LogError($"Empty notification recipient '{azureUniqueIdStr}'"); - - continue; - } - - await SendNotification(fullDepartment, card, azureUniqueId); - } - } - - - private async Task> GetPersonnelForDepartmentExludingConsultantAndExternal( - string fullDepartment) - { - var personnel = (await _resourceClient.GetAllPersonnelForDepartment(fullDepartment)).ToList(); - if (!personnel.Any()) - throw new Exception("No personnel found for department"); - - var personnelWithoutConsultant = personnel.Where(per => - per.AccountType != FusionAccountType.Consultant.ToString() && - per.AccountType != FusionAccountType.External.ToString()).ToList(); - - return personnelWithoutConsultant; - } - - private async Task> GetChangeLogEvents( - IEnumerable listOfInternalPersonnel) - { - var threeMonthsFromToday = DateTime.UtcNow.AddMonths(3); - var today = DateTime.UtcNow; - - var listOfInternalPersonnelWithOnlyActiveProjects = listOfInternalPersonnel - .SelectMany(per => per.PositionInstances.Where(pis => - (pis.AppliesFrom < threeMonthsFromToday && pis.AppliesFrom > today) || pis.AppliesTo > today)) - .ToList(); - - var distinctProjectId = listOfInternalPersonnelWithOnlyActiveProjects.Select(p => p.Project?.Id).Distinct(); - var listAllRelevantInstanceIds = listOfInternalPersonnelWithOnlyActiveProjects.Select(x => x.InstanceId); - - ParallelOptions parallelOptions = new() - { - MaxDegreeOfParallelism = 3 - }; - - var data = new ConcurrentDictionary(); - await Parallel.ForEachAsync(distinctProjectId, parallelOptions, async (project, _) => - { - var changeLogForPersonnel = await _orgClient.GetChangeLog(project.ToString(), today.AddDays(-7)); - data.TryAdd(project.ToString(), changeLogForPersonnel); - }); - - return (from value in data.Values.ToList() - from item in value.Events - where listAllRelevantInstanceIds.Contains(item.InstanceId) - select item).ToList(); - } - - private AdaptiveCard CreateResourceOwnerAdaptiveCard( - List personnel, - List requests, - string departmentIdentifier, string departmentSapId) - { - var personnelAllocationUri = $"{PortalUri()}apps/personnel-allocation/{departmentSapId}"; - var endingPositionsObjectList = ResourceOwnerReportDataCreator - .GetPersonnelPositionsEndingWithNoFutureAllocation(personnel) - .Select(ep => new List - { - new() - { - Value = ep.FullName, - Alignment = AdaptiveHorizontalAlignment.Left - }, - new() - { - Value = ep.EndDate is null ? "No end date" : $"End date: {ep.EndDate.Value:dd/MM/yyyy}", - Alignment = AdaptiveHorizontalAlignment.Right - } - }) - .ToList(); - var personnelMoreThan100PercentObjectList = ResourceOwnerReportDataCreator - .GetPersonnelAllocatedMoreThan100Percent(personnel) - .Select(ep => new List - { - new() - { - Value = ep.FullName, - Alignment = AdaptiveHorizontalAlignment.Left - }, - new() - { - Value = $"{ep.TotalWorkload} %", - Alignment = AdaptiveHorizontalAlignment.Right - } - }) - .ToList(); - var averageTimeToHandleRequests = ResourceOwnerReportDataCreator.GetAverageTimeToHandleRequests(requests); - var card = new AdaptiveCardBuilder() - .AddHeading($"**Weekly summary - {departmentIdentifier}**") - .AddTextRow( - ResourceOwnerReportDataCreator.GetTotalNumberOfPersonnel(personnel).ToString(), - "Number of personnel (employees and external hire)") - .AddTextRow( - ResourceOwnerReportDataCreator.GetCapacityInUse(personnel).ToString(), - "Capacity in use", - "%") - .AddTextRow( - ResourceOwnerReportDataCreator.GetNumberOfRequestsLastWeek(requests).ToString(), - "New requests last week") - .AddTextRow( - ResourceOwnerReportDataCreator.GetNumberOfOpenRequests(requests).ToString(), - "Open requests") - .AddTextRow( - ResourceOwnerReportDataCreator.GetNumberOfRequestsStartingInLessThanThreeMonths(requests).ToString(), - "Requests with start date < 3 months") - .AddTextRow( - ResourceOwnerReportDataCreator.GetNumberOfRequestsStartingInMoreThanThreeMonths(requests).ToString(), - "Requests with start date > 3 months") - .AddTextRow( - averageTimeToHandleRequests > 0 - ? averageTimeToHandleRequests + " day(s)" - : "Less than a day", - "Average time to handle request (last 12 months)") - .AddTextRow( - ResourceOwnerReportDataCreator.GetAllocationChangesAwaitingTaskOwnerAction(requests).ToString(), - "Allocation changes awaiting task owner action") - .AddTextRow( - ResourceOwnerReportDataCreator.CalculateDepartmentChangesLastWeek(personnel).ToString(), - "Project changes last week affecting next 3 months") - .AddListContainer("Allocations ending soon with no future allocation:", endingPositionsObjectList) - .AddListContainer("Personnel with more than 100% workload:", personnelMoreThan100PercentObjectList) - .AddNewLine() - .AddActionButton("Go to Personnel allocation app", personnelAllocationUri) - .Build(); - - return card; - } - - private async Task SendNotification(string fullDepartment, AdaptiveCard card, Guid azureUniqueId) - { - var sendNotification = await _notificationsClient.SendNotification( - new SendNotificationsRequest - { - Title = $"Weekly summary - {fullDepartment}", - EmailPriority = 1, - Card = card, - Description = $"Weekly report for department - {fullDepartment}" - }, - azureUniqueId); - - // Throwing exception if the response is not successful. - if (!sendNotification) - { - throw new Exception( - $"Failed to send notification to resource-owner with AzureUniqueId: '{azureUniqueId}'."); - } - } - - private string PortalUri() - { - var portalUri = _configuration["Endpoints_portal"] ?? "https://fusion.equinor.com/"; - if (!portalUri.EndsWith("/")) - portalUri += "/"; - return portalUri; - } -} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportTimerTriggerFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportTimerTriggerFunction.cs deleted file mode 100644 index 26c1c24c5..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ResourceOwner/WeeklyReport/ScheduledReportTimerTriggerFunction.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Fusion.Resources.Functions.Common.ApiClients; -using Fusion.Resources.Functions.Common.ApiClients.ApiModels; -using Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport.DTOs; -using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using static Fusion.Resources.Functions.Common.ApiClients.LineOrgApiClient; - -namespace Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport; - -public class ScheduledReportTimerTriggerFunction -{ - private readonly ILineOrgApiClient _lineOrgClient; - private readonly IResourcesApiClient _resourcesApiClient; - private readonly ILogger _logger; - private readonly string _serviceBusConnectionString; - private readonly string _queueName; - - // The function should start 02:10 and in order to be finished before 07:00 it spaces out the batch work over 4.5 hours - private int _totalBatchTimeInMinutes = 270; - - public ScheduledReportTimerTriggerFunction( - ILineOrgApiClient lineOrgApiClient, - IResourcesApiClient resourcesApiClient, - ILogger logger, - IConfiguration configuration) - { - _lineOrgClient = lineOrgApiClient; - _resourcesApiClient = resourcesApiClient; - _logger = logger; - _serviceBusConnectionString = configuration["AzureWebJobsServiceBus"]; - _queueName = configuration["scheduled_notification_report_queue"]; - - // Handling reading 'total_batch_time_in_minutes' from configuration - var totalBatchTimeInMinutesStr = configuration["total_batch_time_in_minutes"]; - - if (!string.IsNullOrEmpty(totalBatchTimeInMinutesStr)) - { - _totalBatchTimeInMinutes = int.Parse(totalBatchTimeInMinutesStr); - } - else - { - logger.LogWarning("Env variable 'scheduled_notification_report_queue' not found, using default '120'."); - - _totalBatchTimeInMinutes = 120; - } - } - - [FunctionName("scheduled-report-timer-trigger-function")] - public async Task RunAsync( - [TimerTrigger("0 10 0 * * MON", RunOnStartup = false)] - TimerInfo scheduledReportTimer) - { - _logger.LogInformation( - $"{nameof(ScheduledReportTimerTriggerFunction)} " + - $"started at: {DateTime.UtcNow}"); - - try - { - var client = new ServiceBusClient(_serviceBusConnectionString); - var sender = client.CreateSender(_queueName); - - await SendResourceOwnersToQueue(sender, _totalBatchTimeInMinutes); - - _logger.LogInformation( - $"{nameof(ScheduledReportTimerTriggerFunction)} " + - $"finished at: {DateTime.UtcNow}"); - } - catch (Exception e) - { - _logger.LogError( - $"{nameof(ScheduledReportTimerTriggerFunction)} " + - $"failed with exception: {e.Message}"); - } - } - - private async Task SendResourceOwnersToQueue(ServiceBusSender sender, int totalBatchTimeInMinutes) - { - try - { - // Query departments from LineOrg - var departments = (await _lineOrgClient.GetOrgUnitDepartmentsAsync()) - .Where(d => d.FullDepartment != null) // Exclude departments with blank department name - .Where(d => d.Level >= 4) // Only include departments of level 4 and up - .Where(x => x.Management?.Persons.Length > 0) // Exclude departments with no receivers - .ToList(); - - // Calculate batch time based of total number of departments and the allowed run time - var batchTimeInMinutes = CalculateBatchTime(totalBatchTimeInMinutes, departments.Count); - - var totalNumberOfDepartments = departments.Count; - int totalNumberOfRecipients = departments.Sum(orgUnit => orgUnit.Management.Persons.Length); - - _logger.LogInformation($"With {totalNumberOfDepartments} departments it's going to send notification to {totalNumberOfRecipients} recipients"); - - // Send the queue for processing - - var resourceOwnerMessageSent = 0; - - foreach (var dep in departments) - { - var notificationRecipients = dep.Management.Persons.Select(x => x.AzureUniqueId).ToList(); - - // Get delegates for department, if any - var delegatesResult = await _resourcesApiClient.GetDelegatedResponsibleForDepartment(dep.FullDepartment); - - // Add the user id to the list - notificationRecipients.AddRange(delegatesResult.Select(x => x.DelegatedResponsible.AzureUniquePersonId)); - - // Clean up duplicates in the list - notificationRecipients = notificationRecipients.Distinct().ToList(); - - var timeDelayInMinutes = resourceOwnerMessageSent++ * batchTimeInMinutes; - - try - { - await SendDtoToQueue(sender, new ScheduledNotificationQueueDto - { - AzureUniqueId = notificationRecipients, - FullDepartment = dep.FullDepartment, - DepartmentSapId = dep.SapId - }, timeDelayInMinutes); - } - catch (Exception e) - { - _logger.LogError( - $"ServiceBus queue '{_queueName}' " + - $"item failed with exception when sending message: {e.Message}"); - } - } - } - catch (Exception e) - { - _logger.LogError( - $"ServiceBus queue '{_queueName}' " + - $"failed collecting resource-owners with exception: {e.Message}"); - } - - _logger.LogInformation("Job completed"); - } - - private async Task> GetLineOrgPersonsFromDepartmentsChunked( - List selectedDepartments) - { - var resourceOwners = new List(); - - const int chuckSize = 10; - - for (var i = 0; i < selectedDepartments.Count; i += chuckSize) - { - var chunk = selectedDepartments.Skip(i).Take(chuckSize).ToList(); - - var chunkedResourceOwners = - await _lineOrgClient.GetResourceOwnersFromFullDepartment(chunk); - - resourceOwners.AddRange(chunkedResourceOwners); - } - - return resourceOwners; - } - - private async Task SendDtoToQueue(ServiceBusSender sender, ScheduledNotificationQueueDto dto, double delayInMinutes) - { - var serializedDto = JsonConvert.SerializeObject(dto); - - var message = new ServiceBusMessage(Encoding.UTF8.GetBytes(serializedDto)) - { - ScheduledEnqueueTime = DateTime.UtcNow.AddMinutes(delayInMinutes) - }; - - await sender.SendMessageAsync(message); - } - - private float CalculateBatchTime(int totalBatchTimeInMinutes, int departmentCount) - { - var batchTimeInMinutes = totalBatchTimeInMinutes * 1f / departmentCount; - - _logger.LogInformation($"Batching time is calculated to {batchTimeInMinutes.ToString("F2")} minutes ({(60 * batchTimeInMinutes).ToString("F2")} sec)"); - - return batchTimeInMinutes; - } -} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/local.settings.template.json b/src/backend/function/Fusion.Resources.Functions/local.settings.template.json index 8882ff547..3fc1b10f9 100644 --- a/src/backend/function/Fusion.Resources.Functions/local.settings.template.json +++ b/src/backend/function/Fusion.Resources.Functions/local.settings.template.json @@ -6,7 +6,6 @@ "FUNCTIONS_INPROC_NET8_ENABLED": "1", "FUNCTIONS_EXTENSION_VERSION": "~4", "provision_position_queue": "provision-position-[REPLACE WITH DEV QUEUE]", - "scheduled_notification_report_queue": "scheduled-notification-[REPLACE WITH DEV QUEUE]", "AzureWebJobsServiceBus": "[REPLACE WITH SB CONNECTION STRING]", "AzureAd_TenantId": "3aa4a235-b6e2-48d5-9195-7fcf05b459b0", "AzureAd_ClientId": "5a842df8-3238-415d-b168-9f16a6a6031b", @@ -15,9 +14,7 @@ "Endpoints_people": "https://people.ci.api.fusion-dev.net/", "Endpoints_org": "https://org.ci.api.fusion-dev.net/", "Endpoints_resources": "https://fra-resources.ci.api.fusion-dev.net/", - "Endpoints_summary": "https://fra-summary.ci.api.fusion-dev.net/", "Endpoints_context": "https://context.ci.api.fusion-dev.net/", - "Endpoints_notifications": "https://notifications.ci.api.fusion-dev.net/", "Endpoints_Resources_Fusion": "5a842df8-3238-415d-b168-9f16a6a6031b", "AzureWebJobs.provision-position-request.Disabled": "true", "AzureWebJobs.internal-requests-reassign-invalid-departments.Disabled": "true" diff --git a/src/backend/tests/Fusion.Resources.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs b/src/backend/tests/Fusion.Resources.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs deleted file mode 100644 index bb898c4ca..000000000 --- a/src/backend/tests/Fusion.Resources.Functions.Tests/Notifications/Mock/NotificationReportApiResponseMock.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using Fusion.ApiClients.Org; -using Fusion.Resources.Functions.Common.ApiClients; -using Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport; - -namespace Fusion.Resources.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/backend/tests/Fusion.Resources.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs b/src/backend/tests/Fusion.Resources.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs deleted file mode 100644 index a20c4aec5..000000000 --- a/src/backend/tests/Fusion.Resources.Functions.Tests/Notifications/ScheduledReportNotificationBuilderTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Collections.Generic; -using FluentAssertions; -using Fusion.ApiClients.Org; -using Fusion.Resources.Functions.Common.ApiClients; -using Fusion.Resources.Functions.Functions.Notifications.ResourceOwner.WeeklyReport; -using Fusion.Resources.Functions.Tests.Notifications.Mock; -using Xunit; - -namespace Fusion.Resources.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