Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Feature): Scheduled weekly report #599

Merged
merged 33 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e6d2358
added catch for invalid operation exception to controllers where this…
BouVid Sep 26, 2023
0459ec3
Merge branch 'master' of https://github.com/equinor/fusion-app-resour…
BouVid Sep 26, 2023
f640dd1
added functions
BouVid Sep 26, 2023
9f6a407
settings file added for notification
BouVid Sep 27, 2023
48e43bb
logging correction
BouVid Sep 27, 2023
013d048
handpicked resourceowners added.
BouVid Sep 28, 2023
68cb25c
scheduledrepot dto added
BouVid Oct 3, 2023
5b2bc7f
exception handling added.
BouVid Oct 4, 2023
f52f7d5
adaptive card template added for resourceowners.
BouVid Oct 5, 2023
058cc88
notifications httpclient added.
BouVid Oct 5, 2023
485329c
notification api integration added.
BouVid Oct 5, 2023
0797c30
core notification api integration added.
BouVid Oct 6, 2023
a843383
documentation updated.
BouVid Oct 12, 2023
6963e4e
Added BeTrustedApplication to authorization when getting from GetRequ…
aleklundeq Oct 12, 2023
4e9f003
ContentBuilder + Adjustments to AdaptiveCards template (WIP)
aleklundeq Oct 12, 2023
2dd4bfd
Merge branch 'feature/function/scheduled-weekly-report' of https://gi…
aleklundeq Oct 12, 2023
ea40104
Adjustments to get all thev values for ContentBuilder
aleklundeq Oct 16, 2023
f7e27ef
Working adaptive card. Needs refactoring
aleklundeq Oct 18, 2023
89b79ca
cron schedule changed
BouVid Oct 20, 2023
7fa9978
Adjusting AdaptiveCard to also display all data
aleklundeq Oct 20, 2023
3658e0e
Merge branch 'feature/function/scheduled-weekly-report' of https://gi…
aleklundeq Oct 20, 2023
d357dab
Refactor and small cleanup
aleklundeq Oct 23, 2023
0a2eb9a
cleanup
BouVid Oct 23, 2023
e3efa82
unused messagecompleter removed
BouVid Oct 23, 2023
1208900
que name chagned
BouVid Oct 24, 2023
92cf532
que name chagned
BouVid Oct 24, 2023
35dd9fb
removed unwanted changes
BouVid Oct 24, 2023
2a82d3d
removed unwanted changes
BouVid Oct 24, 2023
105b255
Merge branch 'master' into feature/function/scheduled-weekly-report
BouVid Oct 24, 2023
6288826
Merge branch 'master' into feature/function/scheduled-weekly-report
BouVid Oct 24, 2023
2b85e80
Merge branch 'feature/function/scheduled-weekly-report' of https://gi…
BouVid Oct 24, 2023
c3d6127
package cleanup
BouVid Oct 24, 2023
0164564
handpicked resourceowner-department added
BouVid Oct 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 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; }

Check warning on line 42 in src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LogicAppName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
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; }

Check warning on line 56 in src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public string Name { get; set; }

Check warning on line 57 in src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

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; }

Check warning on line 71 in src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Description' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
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 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
Loading