From 07dfef0d4051b1cd69265a95dddef602c1fa5554 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:13:48 +0200 Subject: [PATCH] chore(Summary): Filter departments to PRD and also send report to delegated responsibles (#675) - [x] New feature - [ ] Bug fix - [ ] High impact **Description of work:** Renamed functions. Now only takes PRD departmens like the old function did. The Departments model has been reworked and now has a field for Resource owners and Delegated resource owners. This required refactoring of a lot of code. For now they're separated into two properties but could be merged into a single recipients list instead, **Testing:** - [ ] Can be tested - [x] Automatic tests created / updated - [x] Local tests are passing **Checklist:** - [x] Considered automated tests - [ ] ~~Considered updating specification / documentation~~ - [x] Considered work items - [x] Considered security - [x] Performed developer testing - [x] Checklist finalized / ready for review --- .../ApiClients/ISummaryApiClient.cs | 16 ++---- src/Fusion.Summary.Api/BaseController.cs | 6 +++ .../Controllers/ApiModels/ApiDepartment.cs | 10 ++-- .../Controllers/DepartmentsController.cs | 30 ++++++++--- .../Requests/PutDepartmentRequest.cs | 6 ++- ....cs => 20240821133720_Initial.Designer.cs} | 11 ++-- ...7_Initial.cs => 20240821133720_Initial.cs} | 5 +- .../SummaryDbContextModelSnapshot.cs | 9 +++- .../Database/Models/DbDepartment.cs | 10 ++-- .../Domain/Commands/CreateDepartment.cs | 10 ++-- .../Domain/Commands/UpdateDepartment.cs | 18 +++---- .../Domain/Models/QueryDepartment.cs | 13 +++-- .../Deployment/disabled-functions.json | 4 +- .../Functions/DepartmentResourceOwnerSync.cs | 50 ++++++++++--------- ...er.cs => WeeklyDepartmentSummarySender.cs} | 26 ++++++---- .../HandlerTests/DepartmentTests.cs | 38 +++++++------- .../Helpers/DepartmentHelpers.cs | 3 +- .../IntegrationTests/DepartmentTests.cs | 4 +- 18 files changed, 161 insertions(+), 108 deletions(-) rename src/Fusion.Summary.Api/Database/Migrations/{20240807132047_Initial.Designer.cs => 20240821133720_Initial.Designer.cs} (94%) rename src/Fusion.Summary.Api/Database/Migrations/{20240807132047_Initial.cs => 20240821133720_Initial.cs} (92%) rename src/Fusion.Summary.Functions/Functions/{WeeklyReportSender.cs => WeeklyDepartmentSummarySender.cs} (85%) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs index c31a9fa3f..fbcaa9b3e 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs @@ -26,23 +26,17 @@ public Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummary public class ApiResourceOwnerDepartment { - public ApiResourceOwnerDepartment(string departmentSapId, string fullDepartmentName, - Guid resourceOwnerAzureUniqueId) - { - DepartmentSapId = departmentSapId; - FullDepartmentName = fullDepartmentName; - ResourceOwnerAzureUniqueId = resourceOwnerAzureUniqueId; - } - public ApiResourceOwnerDepartment() { } - public string DepartmentSapId { get; init; } = string.Empty; + public string DepartmentSapId { get; init; } = null!; + public string FullDepartmentName { get; init; } = null!; + + public Guid[] ResourceOwnersAzureUniqueId { get; init; } = null!; - public string FullDepartmentName { get; init; } = string.Empty; + public Guid[] DelegateResourceOwnersAzureUniqueId { get; init; } = null!; - public Guid ResourceOwnerAzureUniqueId { get; init; } } public record ApiCollection(ICollection Items); diff --git a/src/Fusion.Summary.Api/BaseController.cs b/src/Fusion.Summary.Api/BaseController.cs index 6cfd36301..d24c23bce 100644 --- a/src/Fusion.Summary.Api/BaseController.cs +++ b/src/Fusion.Summary.Api/BaseController.cs @@ -31,4 +31,10 @@ protected Task DispatchAsync(IRequest command) var profileResolver = HttpContext.RequestServices.GetRequiredService(); return await profileResolver.ResolvePersonBasicProfileAsync(personId); } + + protected async Task> ResolvePersonsAsync(IEnumerable personIdentifiers) + { + var profileResolver = HttpContext.RequestServices.GetRequiredService(); + return await profileResolver.ResolvePersonsAsync(personIdentifiers); + } } \ No newline at end of file diff --git a/src/Fusion.Summary.Api/Controllers/ApiModels/ApiDepartment.cs b/src/Fusion.Summary.Api/Controllers/ApiModels/ApiDepartment.cs index ff96a5dc7..40814513f 100644 --- a/src/Fusion.Summary.Api/Controllers/ApiModels/ApiDepartment.cs +++ b/src/Fusion.Summary.Api/Controllers/ApiModels/ApiDepartment.cs @@ -5,16 +5,20 @@ namespace Fusion.Summary.Api.Controllers.ApiModels; public class ApiDepartment { public string DepartmentSapId { get; set; } = string.Empty; - public Guid ResourceOwnerAzureUniqueId { get; set; } public string FullDepartmentName { get; set; } = string.Empty; + public Guid[] ResourceOwnersAzureUniqueId { get; init; } = null!; + + public Guid[] DelegateResourceOwnersAzureUniqueId { get; init; } = null!; + public static ApiDepartment FromQueryDepartment(QueryDepartment queryDepartment) { return new ApiDepartment { DepartmentSapId = queryDepartment.SapDepartmentId, - ResourceOwnerAzureUniqueId = queryDepartment.ResourceOwnerAzureUniqueId, - FullDepartmentName = queryDepartment.FullDepartmentName + FullDepartmentName = queryDepartment.FullDepartmentName, + ResourceOwnersAzureUniqueId = queryDepartment.ResourceOwnersAzureUniqueId.ToArray(), + DelegateResourceOwnersAzureUniqueId = queryDepartment.DelegateResourceOwnersAzureUniqueId.ToArray() }; } } \ No newline at end of file diff --git a/src/Fusion.Summary.Api/Controllers/DepartmentsController.cs b/src/Fusion.Summary.Api/Controllers/DepartmentsController.cs index 8b9eca047..fc96419cc 100644 --- a/src/Fusion.Summary.Api/Controllers/DepartmentsController.cs +++ b/src/Fusion.Summary.Api/Controllers/DepartmentsController.cs @@ -1,5 +1,6 @@ using Fusion.AspNetCore.FluentAuthorization; using Fusion.Authorization; +using Fusion.Integration.Profile; using Fusion.Summary.Api.Authorization.Extensions; using Fusion.Summary.Api.Controllers.ApiModels; using Fusion.Summary.Api.Controllers.Requests; @@ -121,8 +122,17 @@ public async Task PutV1(string sapDepartmentId, [FromBody] PutDep if (string.IsNullOrWhiteSpace(sapDepartmentId)) return BadRequest("SapDepartmentId route parameter is required"); - if (await ResolvePersonAsync(request.ResourceOwnerAzureUniqueId) is null) - return BadRequest("Resource owner not found in azure ad"); + var personIdentifiers = request.ResourceOwnersAzureUniqueId + .Concat(request.DelegateResourceOwnersAzureUniqueId) + .Select(p => new PersonIdentifier(p)); + + var unresolvedProfiles = (await ResolvePersonsAsync(personIdentifiers)) + .Where(r => !r.Success) + .ToList(); + + if (unresolvedProfiles.Count != 0) + return BadRequest($"Profiles: {string.Join(',', unresolvedProfiles)} could not be resolved"); + var department = await DispatchAsync(new GetDepartment(sapDepartmentId)); @@ -132,19 +142,23 @@ public async Task PutV1(string sapDepartmentId, [FromBody] PutDep await DispatchAsync( new CreateDepartment( sapDepartmentId, - request.ResourceOwnerAzureUniqueId, - request.FullDepartmentName)); + request.FullDepartmentName, + request.ResourceOwnersAzureUniqueId, + request.DelegateResourceOwnersAzureUniqueId)); return Created(); } - // Check if department owner has changed - else if (department.ResourceOwnerAzureUniqueId != request.ResourceOwnerAzureUniqueId) + + // Check if department owners has changed + if (!department.ResourceOwnersAzureUniqueId.SequenceEqual(request.ResourceOwnersAzureUniqueId) || + !department.DelegateResourceOwnersAzureUniqueId.SequenceEqual(request.DelegateResourceOwnersAzureUniqueId)) { await DispatchAsync( new UpdateDepartment( sapDepartmentId, - request.ResourceOwnerAzureUniqueId, - request.FullDepartmentName)); + request.FullDepartmentName, + request.ResourceOwnersAzureUniqueId, + request.DelegateResourceOwnersAzureUniqueId)); return Ok(); } diff --git a/src/Fusion.Summary.Api/Controllers/Requests/PutDepartmentRequest.cs b/src/Fusion.Summary.Api/Controllers/Requests/PutDepartmentRequest.cs index 2f6f7383b..010f760ae 100644 --- a/src/Fusion.Summary.Api/Controllers/Requests/PutDepartmentRequest.cs +++ b/src/Fusion.Summary.Api/Controllers/Requests/PutDepartmentRequest.cs @@ -2,14 +2,16 @@ namespace Fusion.Summary.Api.Controllers.Requests; -public record PutDepartmentRequest(string FullDepartmentName, Guid ResourceOwnerAzureUniqueId) +public record PutDepartmentRequest(string FullDepartmentName, Guid[] ResourceOwnersAzureUniqueId, Guid[] DelegateResourceOwnersAzureUniqueId) { public class Validator : AbstractValidator { public Validator() { RuleFor(x => x.FullDepartmentName).NotEmpty(); - RuleFor(x => x.ResourceOwnerAzureUniqueId).NotEmpty(); + RuleFor(x => x.ResourceOwnersAzureUniqueId.Concat(x.DelegateResourceOwnersAzureUniqueId)) + .NotEmpty() + .WithMessage($"Either {nameof(ResourceOwnersAzureUniqueId)} or {nameof(DelegateResourceOwnersAzureUniqueId)} must contain at least one element."); } } }; diff --git a/src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.Designer.cs b/src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.Designer.cs similarity index 94% rename from src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.Designer.cs rename to src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.Designer.cs index 14ca58c8e..b465f6ed5 100644 --- a/src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.Designer.cs +++ b/src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Fusion.Summary.Api.Database.Migrations { [DbContext(typeof(SummaryDbContext))] - [Migration("20240807132047_Initial")] + [Migration("20240821133720_Initial")] partial class Initial { /// @@ -30,12 +30,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("DepartmentSapId") .HasColumnType("nvarchar(450)"); + b.Property("DelegateResourceOwnersAzureUniqueId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("FullDepartmentName") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("ResourceOwnerAzureUniqueId") - .HasColumnType("uniqueidentifier"); + b.Property("ResourceOwnersAzureUniqueId") + .IsRequired() + .HasColumnType("nvarchar(max)"); b.HasKey("DepartmentSapId"); diff --git a/src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.cs b/src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.cs similarity index 92% rename from src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.cs rename to src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.cs index 3bb4bd517..b2aebda1a 100644 --- a/src/Fusion.Summary.Api/Database/Migrations/20240807132047_Initial.cs +++ b/src/Fusion.Summary.Api/Database/Migrations/20240821133720_Initial.cs @@ -16,8 +16,9 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { DepartmentSapId = table.Column(type: "nvarchar(450)", nullable: false), - ResourceOwnerAzureUniqueId = table.Column(type: "uniqueidentifier", nullable: false), - FullDepartmentName = table.Column(type: "nvarchar(max)", nullable: false) + FullDepartmentName = table.Column(type: "nvarchar(max)", nullable: false), + ResourceOwnersAzureUniqueId = table.Column(type: "nvarchar(max)", nullable: false), + DelegateResourceOwnersAzureUniqueId = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { diff --git a/src/Fusion.Summary.Api/Database/Migrations/SummaryDbContextModelSnapshot.cs b/src/Fusion.Summary.Api/Database/Migrations/SummaryDbContextModelSnapshot.cs index b758f80c1..915d5b7ff 100644 --- a/src/Fusion.Summary.Api/Database/Migrations/SummaryDbContextModelSnapshot.cs +++ b/src/Fusion.Summary.Api/Database/Migrations/SummaryDbContextModelSnapshot.cs @@ -27,12 +27,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DepartmentSapId") .HasColumnType("nvarchar(450)"); + b.Property("DelegateResourceOwnersAzureUniqueId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("FullDepartmentName") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("ResourceOwnerAzureUniqueId") - .HasColumnType("uniqueidentifier"); + b.Property("ResourceOwnersAzureUniqueId") + .IsRequired() + .HasColumnType("nvarchar(max)"); b.HasKey("DepartmentSapId"); diff --git a/src/Fusion.Summary.Api/Database/Models/DbDepartment.cs b/src/Fusion.Summary.Api/Database/Models/DbDepartment.cs index bc1d4e849..61468a8f5 100644 --- a/src/Fusion.Summary.Api/Database/Models/DbDepartment.cs +++ b/src/Fusion.Summary.Api/Database/Models/DbDepartment.cs @@ -6,16 +6,20 @@ namespace Fusion.Summary.Api.Database.Models; public class DbDepartment { public string DepartmentSapId { get; set; } = string.Empty; - public Guid ResourceOwnerAzureUniqueId { get; set; } public string FullDepartmentName { get; set; } = string.Empty; + public List ResourceOwnersAzureUniqueId { get; set; } = []; + + public List DelegateResourceOwnersAzureUniqueId { get; set; } = []; + public static DbDepartment FromQueryDepartment(QueryDepartment queryDepartment) { return new DbDepartment { DepartmentSapId = queryDepartment.SapDepartmentId, - ResourceOwnerAzureUniqueId = queryDepartment.ResourceOwnerAzureUniqueId, - FullDepartmentName = queryDepartment.FullDepartmentName + FullDepartmentName = queryDepartment.FullDepartmentName, + ResourceOwnersAzureUniqueId = queryDepartment.ResourceOwnersAzureUniqueId.ToList(), + DelegateResourceOwnersAzureUniqueId = queryDepartment.DelegateResourceOwnersAzureUniqueId.ToList() }; } diff --git a/src/Fusion.Summary.Api/Domain/Commands/CreateDepartment.cs b/src/Fusion.Summary.Api/Domain/Commands/CreateDepartment.cs index 9ce70474a..1ac4b728a 100644 --- a/src/Fusion.Summary.Api/Domain/Commands/CreateDepartment.cs +++ b/src/Fusion.Summary.Api/Domain/Commands/CreateDepartment.cs @@ -9,13 +9,15 @@ public class CreateDepartment : IRequest { private QueryDepartment _queryDepartment; - public CreateDepartment(string SapDepartmentId, Guid ResourceOwnerAzureUniqueId, string FullDepartmentName) + public CreateDepartment(string sapDepartmentId, string fullDepartmentName, + IEnumerable resourceOwnersAzureUniqueId, IEnumerable delegateResourceOwnersAzureUniqueId) { _queryDepartment = new QueryDepartment { - SapDepartmentId = SapDepartmentId, - ResourceOwnerAzureUniqueId = ResourceOwnerAzureUniqueId, - FullDepartmentName = FullDepartmentName + SapDepartmentId = sapDepartmentId, + FullDepartmentName = fullDepartmentName, + ResourceOwnersAzureUniqueId = resourceOwnersAzureUniqueId.ToList(), + DelegateResourceOwnersAzureUniqueId = delegateResourceOwnersAzureUniqueId.ToList() }; } diff --git a/src/Fusion.Summary.Api/Domain/Commands/UpdateDepartment.cs b/src/Fusion.Summary.Api/Domain/Commands/UpdateDepartment.cs index 5ebe4e61c..e73fa6ac3 100644 --- a/src/Fusion.Summary.Api/Domain/Commands/UpdateDepartment.cs +++ b/src/Fusion.Summary.Api/Domain/Commands/UpdateDepartment.cs @@ -10,13 +10,15 @@ public class UpdateDepartment : IRequest { private QueryDepartment _queryDepartment; - public UpdateDepartment(string SapDepartmentId, Guid ResourceOwnerAzureUniqueId, string FullDepartmentName) + public UpdateDepartment(string sapDepartmentId, string fullDepartmentName, + IEnumerable resourceOwnersAzureUniqueId, IEnumerable delegateResourceOwnersAzureUniqueId) { _queryDepartment = new QueryDepartment { - SapDepartmentId = SapDepartmentId, - ResourceOwnerAzureUniqueId = ResourceOwnerAzureUniqueId, - FullDepartmentName = FullDepartmentName + SapDepartmentId = sapDepartmentId, + FullDepartmentName = fullDepartmentName, + ResourceOwnersAzureUniqueId = resourceOwnersAzureUniqueId.ToList(), + DelegateResourceOwnersAzureUniqueId = delegateResourceOwnersAzureUniqueId.ToList() }; } @@ -35,12 +37,10 @@ public async Task Handle(UpdateDepartment request, CancellationToken cancellatio if (existingDepartment != null) { - if (existingDepartment.ResourceOwnerAzureUniqueId != request._queryDepartment.ResourceOwnerAzureUniqueId) - { - existingDepartment.ResourceOwnerAzureUniqueId = request._queryDepartment.ResourceOwnerAzureUniqueId; + existingDepartment.ResourceOwnersAzureUniqueId = request._queryDepartment.ResourceOwnersAzureUniqueId.ToList(); + existingDepartment.DelegateResourceOwnersAzureUniqueId = request._queryDepartment.DelegateResourceOwnersAzureUniqueId.ToList(); - await _context.SaveChangesAsync(); - } + await _context.SaveChangesAsync(); } } } diff --git a/src/Fusion.Summary.Api/Domain/Models/QueryDepartment.cs b/src/Fusion.Summary.Api/Domain/Models/QueryDepartment.cs index 4054f2cfa..860ee2669 100644 --- a/src/Fusion.Summary.Api/Domain/Models/QueryDepartment.cs +++ b/src/Fusion.Summary.Api/Domain/Models/QueryDepartment.cs @@ -6,16 +6,19 @@ namespace Fusion.Summary.Api.Domain.Models; public class QueryDepartment { public string SapDepartmentId { get; set; } = string.Empty; - public Guid ResourceOwnerAzureUniqueId { get; set; } public string FullDepartmentName { get; set; } = string.Empty; + public List ResourceOwnersAzureUniqueId { get; set; } = null!; + + public List DelegateResourceOwnersAzureUniqueId { get; set; } = null!; public static QueryDepartment FromDbDepartment(DbDepartment dbDepartment) { return new QueryDepartment { SapDepartmentId = dbDepartment.DepartmentSapId, - ResourceOwnerAzureUniqueId = dbDepartment.ResourceOwnerAzureUniqueId, - FullDepartmentName = dbDepartment.FullDepartmentName + FullDepartmentName = dbDepartment.FullDepartmentName, + ResourceOwnersAzureUniqueId = dbDepartment.ResourceOwnersAzureUniqueId.ToList(), + DelegateResourceOwnersAzureUniqueId = dbDepartment.DelegateResourceOwnersAzureUniqueId.ToList() }; } @@ -24,7 +27,9 @@ public static QueryDepartment FromApiDepartment(ApiDepartment apiDepartment) return new QueryDepartment { SapDepartmentId = apiDepartment.DepartmentSapId, - FullDepartmentName = apiDepartment.FullDepartmentName + FullDepartmentName = apiDepartment.FullDepartmentName, + ResourceOwnersAzureUniqueId = apiDepartment.ResourceOwnersAzureUniqueId.ToList(), + DelegateResourceOwnersAzureUniqueId = apiDepartment.DelegateResourceOwnersAzureUniqueId.ToList() }; } } diff --git a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json index 0a590e596..7aeeda537 100644 --- a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json +++ b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json @@ -2,8 +2,8 @@ { "environment": "pr", "disabledFunctions": [ - "department-resource-owner-sync", - "weekly-report-sender", + "weekly-department-recipients-sync", + "weekly-department-summary-sender", "weekly-department-summary-worker" ] } diff --git a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs index cd814de2f..27a18114d 100644 --- a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs @@ -18,6 +18,7 @@ public class DepartmentResourceOwnerSync private readonly ILineOrgApiClient lineOrgApiClient; private readonly ISummaryApiClient summaryApiClient; private readonly IConfiguration configuration; + private readonly IResourcesApiClient resourcesApiClient; private string _serviceBusConnectionString; private string _weeklySummaryQueueName; @@ -25,11 +26,13 @@ public class DepartmentResourceOwnerSync public DepartmentResourceOwnerSync( ILineOrgApiClient lineOrgApiClient, ISummaryApiClient summaryApiClient, - IConfiguration configuration) + IConfiguration configuration, + IResourcesApiClient resourcesApiClient) { this.lineOrgApiClient = lineOrgApiClient; this.summaryApiClient = summaryApiClient; this.configuration = configuration; + this.resourcesApiClient = resourcesApiClient; } /// @@ -41,7 +44,7 @@ public DepartmentResourceOwnerSync( /// Cancellation token /// /// - [FunctionName("department-resource-owner-sync")] + [FunctionName("weekly-department-recipients-sync")] public async Task RunAsync( [TimerTrigger("0 05 00 * * *", RunOnStartup = false)] TimerInfo timerInfo, CancellationToken cancellationToken @@ -54,32 +57,31 @@ public async Task RunAsync( var sender = client.CreateSender(_weeklySummaryQueueName); // Fetch all departments - var departments = await lineOrgApiClient.GetOrgUnitDepartmentsAsync(); + var departments = (await lineOrgApiClient.GetOrgUnitDepartmentsAsync()) + .DistinctBy(d => d.SapId) + .Where(d => d.FullDepartment != null && d.SapId != null) + .Where(d => d.FullDepartment!.Contains("PRD")) + .Where(d => d.Management.Persons.Length > 0); - var selectedDepartments = departments - .Where(d => d.FullDepartment != null).DistinctBy(d => d.SapId).ToList(); - if (!selectedDepartments.Any()) - throw new Exception("No departments found."); + var apiDepartments = new List(); - // TODO: Retrieving resource-owners wil be refactored later - var resourceOwners = new List(); - foreach (var orgUnitsChunk in selectedDepartments.Chunk(10)) + foreach (var orgUnit in departments) { - var chunkedResourceOwners = - await lineOrgApiClient.GetResourceOwnersFromFullDepartment(orgUnitsChunk); - resourceOwners.AddRange(chunkedResourceOwners); - } - - if (!resourceOwners.Any()) - throw new Exception("No resource-owners found."); - - var resourceOwnerDepartments = resourceOwners - .Where(ro => ro.DepartmentSapId is not null && Guid.TryParse(ro.AzureUniqueId, out _)) - .Select(resourceOwner => new - ApiResourceOwnerDepartment(resourceOwner.DepartmentSapId!, resourceOwner.FullDepartment, - Guid.Parse(resourceOwner.AzureUniqueId))); + var delegatedResponsibles = (await resourcesApiClient + .GetDelegatedResponsibleForDepartment(orgUnit.SapId!)) + .Select(d => Guid.Parse(d.DelegatedResponsible.AzureUniquePersonId)) + .Distinct() + .ToArray(); + apiDepartments.Add(new ApiResourceOwnerDepartment() + { + DepartmentSapId = orgUnit.SapId!, + FullDepartmentName = orgUnit.FullDepartment!, + ResourceOwnersAzureUniqueId = orgUnit.Management.Persons.Select(p => Guid.Parse(p.AzureUniqueId)).Distinct().ToArray(), + DelegateResourceOwnersAzureUniqueId = delegatedResponsibles + }); + } var parallelOptions = new ParallelOptions() { @@ -88,7 +90,7 @@ public async Task RunAsync( }; // Use Parallel.ForEachAsync to easily limit the number of parallel requests - await Parallel.ForEachAsync(resourceOwnerDepartments, parallelOptions, + await Parallel.ForEachAsync(apiDepartments, parallelOptions, async (ownerDepartment, token) => { // Update the database diff --git a/src/Fusion.Summary.Functions/Functions/WeeklyReportSender.cs b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs similarity index 85% rename from src/Fusion.Summary.Functions/Functions/WeeklyReportSender.cs rename to src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs index f6007a81b..ec3d874a3 100644 --- a/src/Fusion.Summary.Functions/Functions/WeeklyReportSender.cs +++ b/src/Fusion.Summary.Functions/Functions/WeeklyDepartmentSummarySender.cs @@ -13,29 +13,31 @@ namespace Fusion.Summary.Functions.Functions; -public class WeeklyReportSender +public class WeeklyDepartmentSummarySender { private readonly ISummaryApiClient summaryApiClient; + private readonly IResourcesApiClient resourcesApiClient; private readonly INotificationApiClient notificationApiClient; - private readonly ILogger logger; + private readonly ILogger logger; private readonly IConfiguration configuration; - public WeeklyReportSender(ISummaryApiClient summaryApiClient, INotificationApiClient notificationApiClient, - ILogger logger, IConfiguration configuration) + public WeeklyDepartmentSummarySender(ISummaryApiClient summaryApiClient, INotificationApiClient notificationApiClient, + ILogger logger, IConfiguration configuration, IResourcesApiClient resourcesApiClient) { this.summaryApiClient = summaryApiClient; this.notificationApiClient = notificationApiClient; this.logger = logger; this.configuration = configuration; + this.resourcesApiClient = resourcesApiClient; } - [FunctionName("weekly-report-sender")] + [FunctionName("weekly-department-summary-sender")] public async Task RunAsync([TimerTrigger("0 0 8 * * 1", RunOnStartup = false)] TimerInfo timerInfo) { var departments = await summaryApiClient.GetDepartmentsAsync(); - if (departments is null) + if (departments is null || !departments.Any()) { logger.LogCritical("No departments found. Exiting"); return; @@ -70,7 +72,14 @@ await Parallel.ForEachAsync(departments, options, async (department, ct) => throw; } - await notificationApiClient.SendNotification(notification, department.ResourceOwnerAzureUniqueId); + var reportReceivers = department.ResourceOwnersAzureUniqueId.Concat(department.DelegateResourceOwnersAzureUniqueId); + + foreach (var azureId in reportReceivers) + { + var result = await notificationApiClient.SendNotification(notification, azureId); + if (!result) + logger.LogError("Failed to send notification to user with AzureId {AzureId} | Report {@ReportId}", azureId, summaryReport); + } }); } @@ -172,5 +181,4 @@ private string GetPortalUri() portalUri += "/"; return portalUri; } -} - +} \ No newline at end of file diff --git a/src/tests/Fusion.Summary.Api.Tests/HandlerTests/DepartmentTests.cs b/src/tests/Fusion.Summary.Api.Tests/HandlerTests/DepartmentTests.cs index 461ff6be6..a81ce121e 100644 --- a/src/tests/Fusion.Summary.Api.Tests/HandlerTests/DepartmentTests.cs +++ b/src/tests/Fusion.Summary.Api.Tests/HandlerTests/DepartmentTests.cs @@ -42,7 +42,7 @@ public async Task CreateDepartment_ShouldCreateDepartment() var department = new DbDepartment { DepartmentSapId = "1001", FullDepartmentName = "Department A" }; // Act - await handler.Handle(new CreateDepartment(department.DepartmentSapId, department.ResourceOwnerAzureUniqueId, department.FullDepartmentName), CancellationToken.None); + await handler.Handle(new CreateDepartment(department.DepartmentSapId, department.FullDepartmentName, [], []), CancellationToken.None); // Assert var dbDepartment = _context.Departments.FirstOrDefault(d => d.DepartmentSapId == department.DepartmentSapId); @@ -58,11 +58,11 @@ public async Task GetAllDepartments_ShouldReturnAllDepartments() var handler = new CreateDepartment.Handler(_context); var queryHandler = new GetAllDepartments.Handler(_context); - var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A", ResourceOwnerAzureUniqueId = Guid.Empty }; - var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B", ResourceOwnerAzureUniqueId = Guid.Empty }; + var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A", ResourceOwnersAzureUniqueId = [], DelegateResourceOwnersAzureUniqueId = [] }; + var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B", ResourceOwnersAzureUniqueId = [], DelegateResourceOwnersAzureUniqueId = [] }; - await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.ResourceOwnerAzureUniqueId, departmentA.FullDepartmentName), CancellationToken.None); - await handler.Handle(new CreateDepartment(departmentB.SapDepartmentId, departmentB.ResourceOwnerAzureUniqueId, departmentB.FullDepartmentName), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.FullDepartmentName, departmentA.ResourceOwnersAzureUniqueId, departmentA.DelegateResourceOwnersAzureUniqueId), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentB.SapDepartmentId, departmentB.FullDepartmentName, departmentB.ResourceOwnersAzureUniqueId, departmentB.DelegateResourceOwnersAzureUniqueId), CancellationToken.None); // Act var result = await queryHandler.Handle(new GetAllDepartments(), CancellationToken.None); @@ -84,8 +84,8 @@ public async Task GetDepartmentById_ExistingId_ShouldReturnDepartment() var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A" }; var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B" }; - await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.ResourceOwnerAzureUniqueId, departmentA.FullDepartmentName), CancellationToken.None); - await handler.Handle(new CreateDepartment(departmentB.SapDepartmentId, departmentB.ResourceOwnerAzureUniqueId, departmentB.FullDepartmentName), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.FullDepartmentName, [], []), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentB.SapDepartmentId, departmentB.FullDepartmentName, [], []), CancellationToken.None); // Act var result = await queryHandler.Handle(new GetDepartment(departmentA.SapDepartmentId), CancellationToken.None); @@ -118,22 +118,22 @@ public async Task UpdateDepartment_ExistingId_ShouldUpdateDepartment() var updateHandler = new UpdateDepartment.Handler(_context); var queryHandler = new GetDepartment.Handler(_context); - var resourceOwner1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); - var resourceOwner2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + Guid[] resourceOwners1 = [Guid.Parse("00000000-0000-0000-0000-000000000001")]; + Guid[] resourceOwners2 = [Guid.Parse("00000000-0000-0000-0000-000000000002")]; var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A" }; var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B" }; - await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, resourceOwner1, departmentA.FullDepartmentName), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.FullDepartmentName, resourceOwners1, []), CancellationToken.None); // Act - await updateHandler.Handle(new UpdateDepartment(departmentA.SapDepartmentId, resourceOwner2, departmentA.FullDepartmentName), CancellationToken.None); + await updateHandler.Handle(new UpdateDepartment(departmentA.SapDepartmentId, departmentA.FullDepartmentName, resourceOwners2, []), CancellationToken.None); var result = await queryHandler.Handle(new GetDepartment(departmentA.SapDepartmentId), CancellationToken.None); // Assert Assert.NotNull(result); - Assert.Equal(resourceOwner2, result.ResourceOwnerAzureUniqueId); + Assert.Equal(resourceOwners2, result.ResourceOwnersAzureUniqueId); } [Fact] @@ -144,22 +144,22 @@ public async Task UpdateDepartment_NonExistingId_ShouldNotUpdateDepartment() var updateHandler = new UpdateDepartment.Handler(_context); var queryHandler = new GetDepartment.Handler(_context); - var resourceOwner1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); - var resourceOwner2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + List resourceOwner1 = [Guid.Parse("00000000-0000-0000-0000-000000000001")]; + List resourceOwner2 = [Guid.Parse("00000000-0000-0000-0000-000000000002")]; - var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A", ResourceOwnerAzureUniqueId = resourceOwner1 }; - var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B", ResourceOwnerAzureUniqueId = resourceOwner2 }; + var departmentA = new QueryDepartment { SapDepartmentId = "1001", FullDepartmentName = "Department A", ResourceOwnersAzureUniqueId = resourceOwner1 }; + var departmentB = new QueryDepartment { SapDepartmentId = "1002", FullDepartmentName = "Department B", ResourceOwnersAzureUniqueId = resourceOwner2 }; - await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.ResourceOwnerAzureUniqueId, departmentA.FullDepartmentName), CancellationToken.None); + await handler.Handle(new CreateDepartment(departmentA.SapDepartmentId, departmentA.FullDepartmentName, departmentA.ResourceOwnersAzureUniqueId, []), CancellationToken.None); // Act - await updateHandler.Handle(new UpdateDepartment(departmentB.SapDepartmentId, resourceOwner2, departmentB.FullDepartmentName), CancellationToken.None); + await updateHandler.Handle(new UpdateDepartment(departmentB.SapDepartmentId, departmentB.FullDepartmentName, departmentB.ResourceOwnersAzureUniqueId, []), CancellationToken.None); var result = await queryHandler.Handle(new GetDepartment(departmentA.SapDepartmentId), CancellationToken.None); // Assert Assert.NotNull(result); - Assert.Equal(resourceOwner1, result.ResourceOwnerAzureUniqueId); + Assert.Equal(resourceOwner1, result.ResourceOwnersAzureUniqueId); } } } \ No newline at end of file diff --git a/src/tests/Fusion.Summary.Api.Tests/Helpers/DepartmentHelpers.cs b/src/tests/Fusion.Summary.Api.Tests/Helpers/DepartmentHelpers.cs index 24479358d..2f959059f 100644 --- a/src/tests/Fusion.Summary.Api.Tests/Helpers/DepartmentHelpers.cs +++ b/src/tests/Fusion.Summary.Api.Tests/Helpers/DepartmentHelpers.cs @@ -10,7 +10,8 @@ public static ApiDepartment GenerateDepartment(Guid resourceOwnerAzureUniqueId) { FullDepartmentName = $"Test FullDepartmentName {Guid.NewGuid()}", DepartmentSapId = $"Test DepartmentSapId {Guid.NewGuid()}", - ResourceOwnerAzureUniqueId = resourceOwnerAzureUniqueId + ResourceOwnersAzureUniqueId = [resourceOwnerAzureUniqueId], + DelegateResourceOwnersAzureUniqueId = [] }; public static async Task PutDepartmentAsync(this HttpClient client, Guid resourceOwnerAzureUniqueId, diff --git a/src/tests/Fusion.Summary.Api.Tests/IntegrationTests/DepartmentTests.cs b/src/tests/Fusion.Summary.Api.Tests/IntegrationTests/DepartmentTests.cs index d24921bb9..790ff9db9 100644 --- a/src/tests/Fusion.Summary.Api.Tests/IntegrationTests/DepartmentTests.cs +++ b/src/tests/Fusion.Summary.Api.Tests/IntegrationTests/DepartmentTests.cs @@ -66,8 +66,8 @@ await _client.PutDepartmentAsync(newOwner.AzureUniqueId!.Value, d => updatedDepartment.DepartmentSapId.Should() .Be(department.DepartmentSapId, "It is still the same department with the same sap id"); - updatedDepartment.ResourceOwnerAzureUniqueId.Should() - .Be(newOwner.AzureUniqueId!.Value, "The owner should be updated"); + updatedDepartment.ResourceOwnersAzureUniqueId.Should() + .Contain(newOwner.AzureUniqueId!.Value, "The owner should be updated"); updatedDepartment.FullDepartmentName.Should() .Be(department.FullDepartmentName, "It is still the same department with the same full department name");