Skip to content

Commit

Permalink
(Feature): Scheduled weekly report (#599)
Browse files Browse the repository at this point in the history
- [x] New feature
- [ ] Bug fix
- [ ] High impact

**Description of work:**


**Testing:**
- [x] Can be tested
- [ ] Automatic tests created / updated
- [ ] ~~Local tests are passing~~



**Checklist:**
- [ ] Considered automated tests
- [ ] Considered updating specification / documentation
- [ ] Considered work items 
- [ ] Considered security
- [ ] Performed developer testing
- [ ] Checklist finalized / ready for review

---------

Co-authored-by: Aleksander Lund <[email protected]>
  • Loading branch information
BouVid and aleklundeq authored Oct 26, 2023
1 parent cc1975f commit 0265d56
Show file tree
Hide file tree
Showing 20 changed files with 902 additions and 7 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,31 @@ This makes the app transferable to other teams.
Test app: [GUID]
Production app: [GUID]

## Functions

### Scheduled report function

This functions send a weekly report to task- and resource-owners.

#### Flow

- The time triggered function (`ScheduledReportTimerTriggerFunction.cs`)
run once every week (every sunday at 6 AM UTC). The time triggered unction call the LineOrg API to get all resourceOwners.
- Individual recipients are sent to a queue on Azure Servicebus.
- The content builder function (`ScheduledReportContentBuilderFunction.cs`) is triggered by the queue.
The content builder function generate an adaptive card specific to each
recipient and their respective department.
- The email content is sent to the Core Notifications API which send the email to the recipient.

```mermaid
sequenceDiagram
Time triggered function ->>+ Resource API: Get Departments
Resource API ->>+ Time triggered function: Departments
Time triggered function ->>+ LineOrg API: Get recipients for department
LineOrg API ->>+ Time triggered function: Recipients
Time triggered function ->>+ Servicebus queue: Recipient
Servicebus queue ->>+ Content builder function: Recipient
Content builder function ->>+ Core Notifications API: Content for recipient
Core Notifications API ->>+ Content builder function: Result
```
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task<ActionResult<ApiCollection<ApiResourceAllocationRequest>>> Get

var authResult = await Request.RequireAuthorizationAsync(r =>
{
r.AlwaysAccessWhen().FullControl().FullControlInternal();
r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication();
r.AnyOf(or =>
{
or.BeResourceOwner(new DepartmentPath(departmentString).GoToLevel(2), includeDescendants: true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Fusion.Resources.Functions.ApiClients.ApiModels;

public class LineOrgPersonsResponse
{
[JsonProperty("totalCount")] public int TotalCount { get; set; }

[JsonProperty("count")] public int Count { get; set; }

[JsonProperty("@nextPage")] public object NextPage { get; set; }

[JsonProperty("value")] public List<LineOrgPerson> Value { get; set; }
}
public class Manager
{
[JsonProperty("azureUniqueId")] public string AzureUniqueId { get; set; }

[JsonProperty("mail")] public string Mail { get; set; }

[JsonProperty("department")] public string Department { get; set; }

[JsonProperty("fullDepartment")] public string FullDepartment { get; set; }

[JsonProperty("name")] public string Name { get; set; }

[JsonProperty("jobTitle")] public string JobTitle { get; set; }
}
public class LineOrgPerson
{
[JsonProperty("azureUniqueId")] public string AzureUniqueId { get; set; }

[JsonProperty("managerId")] public string ManagerId { get; set; }

[JsonProperty("manager")] public Manager Manager { get; set; }

[JsonProperty("department")] public string Department { get; set; }

[JsonProperty("fullDepartment")] public string FullDepartment { get; set; }

[JsonProperty("name")] public string Name { get; set; }

[JsonProperty("givenName")] public string GivenName { get; set; }

[JsonProperty("surname")] public string Surname { get; set; }

[JsonProperty("jobTitle")] public string JobTitle { get; set; }

[JsonProperty("mail")] public string Mail { get; set; }

[JsonProperty("country")] public string Country { get; set; }

[JsonProperty("phone")] public string Phone { get; set; }

[JsonProperty("officeLocation")] public string OfficeLocation { get; set; }

[JsonProperty("userType")] public string UserType { get; set; }

[JsonProperty("isResourceOwner")] public bool IsResourceOwner { get; set; }

[JsonProperty("hasChildPositions")] public bool HasChildPositions { get; set; }

[JsonProperty("hasOfficeLicense")] public bool HasOfficeLicense { get; set; }

[JsonProperty("created")] public DateTime Created { get; set; }

[JsonProperty("updated")] public DateTime Updated { get; set; }

[JsonProperty("lastSyncDate")] public DateTime LastSyncDate { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Newtonsoft.Json;

namespace Fusion.Resources.Functions.ApiClients.ApiModels;

public class SendNotificationsRequest
{
[JsonProperty("emailPriority")] public int EmailPriority { get; set; }

[JsonProperty("title")] public string Title { get; set; }

[JsonProperty("description")] public string Description { get; set; }

[JsonProperty("card")] public object Card { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Fusion.Resources.Functions.ApiClients.ApiModels;

namespace Fusion.Resources.Functions.ApiClients;

public interface ILineOrgApiClient
{
Task<IEnumerable<string>> GetOrgUnitDepartmentsAsync();
Task<List<LineOrgPerson>> GetResourceOwnersFromFullDepartment(List<string> fullDepartments);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
using Fusion.Resources.Functions.ApiClients.ApiModels;

namespace Fusion.Resources.Functions.ApiClients;

public interface INotificationApiClient
{
Task<bool> SendNotification(SendNotificationsRequest request, Guid azureUniqueId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Fusion.ApiClients.Org;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Fusion.Resources.Functions.ApiClients
Expand All @@ -11,6 +12,8 @@ public interface IResourcesApiClient
Task<IEnumerable<ProjectReference>> GetProjectsAsync();
Task<IEnumerable<ResourceAllocationRequest>> GetIncompleteDepartmentAssignedResourceAllocationRequestsForProjectAsync(ProjectReference project);
Task<bool> ReassignRequestAsync(ResourceAllocationRequest item, string? department);
Task<IEnumerable<ResourceAllocationRequest>> GetAllRequestsForDepartment(string departmentIdentifier);
Task<IEnumerable<InternalPersonnelPerson>> GetAllPersonnelForDepartment(string departmentIdentifier);

#region Models

Expand All @@ -25,6 +28,53 @@ public class ResourceAllocationRequest
public ApiPositionInstanceV2? OrgPositionInstance { get; set; }
public ProposedPerson? ProposedPerson { get; set; }
public bool HasProposedPerson => ProposedPerson?.Person.AzureUniquePersonId is not null;
public string? State { get; set; }
public Workflow? workflow { get; set; }
}

public enum RequestState
{
approval, proposal, provisioning, created, completed
}

public class Workflow
{
public string LogicAppName { get; set; }
public string LogicAppVersion { get; set; }

[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]
public ApiWorkflowState State { get; set; }

public IEnumerable<WorkflowStep> Steps { get; set; }

public enum ApiWorkflowState { Running, Canceled, Error, Completed, Terminated, Unknown }

}

public class WorkflowStep
{
public string Id { get; set; }
public string Name { get; set; }

public bool IsCompleted => Completed.HasValue;

/// <summary>
/// Pending, Approved, Rejected, Skipped
/// </summary>
[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]
public ApiWorkflowStepState State { get; set; }

public DateTimeOffset? Started { get; set; }
public DateTimeOffset? Completed { get; set; }
public DateTimeOffset? DueDate { get; set; }
public InternalPersonnelPerson? CompletedBy { get; set; }
public string Description { get; set; }
public string? Reason { get; set; }

public string? PreviousStep { get; set; }
public string? NextStep { get; set; }

public enum ApiWorkflowStepState { Pending, Approved, Rejected, Skipped, Unknown }
}

public class ProposedPerson
Expand All @@ -42,6 +92,40 @@ public class Person
public class ProjectReference
{
public Guid Id { get; set; }
public Guid? InternalId { get; set; }
public string? Name { get; set; }
}

public class InternalPersonnelPerson
{

public Guid? AzureUniquePersonId { get; set; }
public string? Mail { get; set; } = null!;
public string? Name { get; set; } = null!;
public string? PhoneNumber { get; set; }
public string? JobTitle { get; set; }
public string? OfficeLocation { get; set; }
public string? Department { get; set; }
public string? FullDepartment { get; set; }
public bool IsResourceOwner { get; set; }
public string? AccountType { get; set; }
public List<PersonnelPosition> PositionInstances { get; set; } = new List<PersonnelPosition>();
}

public class PersonnelPosition
{
public Guid? PositionId { get; set; }
public Guid? InstanceId { get; set; }
public DateTime? AppliesFrom { get; set; }
public DateTime? AppliesTo { get; set; }
public string? Name { get; set; } = null!;
public string? Location { get; set; }
public string? AllocationState { get; set; }
public DateTime? AllocationUpdated { get; set; }

public bool IsActive => AppliesFrom <= DateTime.UtcNow.Date && AppliesTo >= DateTime.UtcNow.Date;
public double Workload { get; set; }
public ProjectReference? Project { get; set; }
}
#endregion Models
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Fusion.Resources.Functions.ApiClients.ApiModels;
using Fusion.Resources.Functions.Integration;

namespace Fusion.Resources.Functions.ApiClients;
Expand All @@ -26,6 +27,16 @@ public async Task<IEnumerable<string>> GetOrgUnitDepartmentsAsync()
.Select(x => x.FullDepartment!).ToList();
}

public async Task<List<LineOrgPerson>> GetResourceOwnersFromFullDepartment(List<string> fullDepartments)
{
var queryString = $"/lineorg/persons?$filter=fullDepartment in " +
$"({fullDepartments.Aggregate((a, b) => $"'{a}', '{b}'")}) " +
$"and isResourceOwner eq 'true'";
var resourceOwners = await lineOrgClient.GetAsJsonAsync<LineOrgPersonsResponse>(queryString);

return resourceOwners.Value;
}

internal class DepartmentRef
{
public string? FullDepartment { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Fusion.Resources.Functions.ApiClients.ApiModels;
using Newtonsoft.Json;

namespace Fusion.Resources.Functions.ApiClients;

public class NotificationApiClient : INotificationApiClient
{
private readonly HttpClient _client;

public NotificationApiClient(IHttpClientFactory httpClientFactory)
{
_client = httpClientFactory.CreateClient(HttpClientNames.Application.Notifications);
}

public async Task<bool> SendNotification(SendNotificationsRequest request, Guid azureUniqueId)
{
var content = JsonConvert.SerializeObject(request);
var stringContent = new StringContent(content, Encoding.UTF8, "application/json");

var response = await _client.PostAsync($"/persons/{azureUniqueId}/notifications?api-version=1.0",
stringContent);

return response.IsSuccessStatusCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#nullable enable
using System;
using Fusion.Resources.Functions.Integration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
Expand Down Expand Up @@ -36,6 +35,23 @@ public async Task<IEnumerable<ResourceAllocationRequest>> GetIncompleteDepartmen
return data.Value.Where(x => x.AssignedDepartment is not null);
}

public async Task<IEnumerable<ResourceAllocationRequest>> GetAllRequestsForDepartment(string departmentIdentifier)
{
var response = await resourcesClient.GetAsJsonAsync<InternalCollection<ResourceAllocationRequest>>(
$"departments/{departmentIdentifier}/resources/requests?$expand=orgPosition,orgPositionInstance,actions");

return response.Value.ToList();
}

public async Task<IEnumerable<InternalPersonnelPerson>> GetAllPersonnelForDepartment(string departmentIdentifier)
{
var response = await resourcesClient.GetAsJsonAsync<InternalCollection<InternalPersonnelPerson>>(
$"departments/{departmentIdentifier}/resources/personnel?api-version=2.0&$includeCurrentAllocations=true");

return response.Value.ToList();

}

public async Task<bool> ReassignRequestAsync(ResourceAllocationRequest item, string? department)
{
var content = JsonConvert.SerializeObject(new { AssignedDepartment = department });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services

builder.AddLineOrgClient();
services.AddScoped<ILineOrgApiClient, LineOrgApiClient>();

builder.AddNotificationsClient();
services.AddScoped<INotificationApiClient, NotificationApiClient>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;

namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCard;

public class ResourceOwnerAdaptiveCardData
{
public int TotalNumberOfRequests { get; set; }
public int NumberOfOlderRequests { get; set; }
public int NumberOfNewRequestsWithNoNomination { get; set; }
public int NumberOfOpenRequests { get; set; }
internal IEnumerable<PersonnelContent> PersonnelPositionsEndingWithNoFutureAllocation { get; set; }
public int PercentAllocationOfTotalCapacity { get; set; }
internal IEnumerable<PersonnelContent> PersonnelAllocatedMoreThan100Percent { get; set; }
public int NumberOfExtContractsEnding { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Fusion.Resources.Functions.Functions.Notifications.Models.DTOs;

public class ScheduledNotificationQueueDto
{
public string AzureUniqueId { get; set; }
public string FullDepartment { get; set; }
public NotificationRoleType Role { get; set; }
}
public enum NotificationRoleType
{
ResourceOwner,
TaskOwner
}
Loading

0 comments on commit 0265d56

Please sign in to comment.