Skip to content

Commit

Permalink
feat(summary): Az function task owner report sender (#722)
Browse files Browse the repository at this point in the history
- [x] New feature
- [ ] Bug fix
- [ ] High impact

**Description of work:**
<!--- Please give a description of the work --->


AB57452

Similar to the weekly resource owner report sender this is the
equivalent sender for the task owner report. Due to the report format
being more complex, I had to do some manual transformation of the
adaptive card html. Without these changes the layout would look fine in
new outlook and browser, but would look very bad in classic outlook.

AZ func is disabled in FQA and FPRD. A seperate PR will enable them once
testing of the solution has been performed.


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

<!--- Please give a description of how this can be tested --->


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

<!--- Other comments --->
  • Loading branch information
Jonathanio123 authored Dec 2, 2024
1 parent 516e7db commit ab0c33a
Show file tree
Hide file tree
Showing 23 changed files with 919 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ steps:
resources = "https://fra-resources-$pullRequestNumber.pr.api.fusion-dev.net"
summary = "https://fra-summary-$pullRequestNumber.pr.api.fusion-dev.net"
roles = "https://roles.$fusionEnvironment.api.fusion-dev.net"
mail = "https://mail.$fusionEnvironment.api.fusion-dev.net"
}
resources = @{
fusion = "${{ parameters.fusionResource }}"
Expand Down
3 changes: 3 additions & 0 deletions pipelines/templates/deploy-summary-function-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ steps:
$notifications = "https://notification.api.fusion.equinor.com"
$context = "https://context.api.fusion.equinor.com"
$roles = "https://roles.api.fusion.equinor.com"
$mail = "https://mail.api.fusion.equinor.com"
}
else {
$summary = "https://fra-summary.$environment.api.fusion-dev.net"
Expand All @@ -59,6 +60,7 @@ steps:
$context = "https://context.$fusionEnvironment.api.fusion-dev.net"
$portal = "https://fusion.$fusionEnvironment.fusion-dev.net"
$roles = "https://roles.$fusionEnvironment.api.fusion-dev.net"
$mail = "https://mail.$fusionEnvironment.api.fusion-dev.net"
}
$settings = @{
Expand All @@ -79,6 +81,7 @@ steps:
portal = $portal
summary = $summary
roles = $roles
mail = $mail
}
resources = @{
fusion = "${{ parameters.fusionResource }}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels;

public class ApiContext
{
public Guid Id { get; set; }

public string? ExternalId { get; set; }

public ApiContextType Type { get; set; } = null!;

public Dictionary<string, object?> Value { get; set; } = null!;

public string Title { get; set; } = null!;

public string? Source { get; set; }

public bool IsActive { get; set; }
}

public class ApiContextType
{
public string Id { get; set; } = null!;

public bool IsChildType { get; set; }

public string[]? ParentTypeIds { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels;

public class SendEmailRequest
{
public required string[] Recipients { get; set; }
public required string Subject { get; set; }
public required string Body { get; set; }
public string? FromDisplayName { get; set; }
}

public class SendEmailWithTemplateRequest
{
public required string Subject { get; set; }

public required string[] Recipients { get; set; }

/// <summary>
/// Specify the content that is to be displayed in the mail
/// </summary>
public required MailBody MailBody { get; set; }
}

public class MailBody
{
/// <summary>
/// The main content in the mail placed between the header and footer
/// </summary>
public required string HtmlContent { get; set; }

/// <summary>
/// Optional. If not specified, the footer template will be used
/// </summary>
public string? HtmlFooter { get; set; }

/// <summary>
/// Optional. A text that is displayed inside the header. Will default to 'Mail from Fusion'
/// </summary>
public string? HeaderTitle { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
using Fusion.Resources.Functions.Common.Integration.Http;

namespace Fusion.Resources.Functions.Common.ApiClients;

public class ContextApiClient : IContextApiClient
{
private readonly HttpClient client;

public ContextApiClient(IHttpClientFactory httpClientFactory)
{
client = httpClientFactory.CreateClient(HttpClientNames.Application.Context);
}

public async Task<ICollection<ApiContext>> GetContextsAsync(string? contextType = null, CancellationToken cancellationToken = default)
{
var url = contextType is null ? "/contexts" : $"/contexts?$filter=type eq '{contextType}'";
return await client.GetAsJsonAsync<ICollection<ApiContext>>(url, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Fusion.Resources.Functions.Common.ApiClients.ApiModels;

namespace Fusion.Resources.Functions.Common.ApiClients;

public interface IContextApiClient
{
public Task<ICollection<ApiContext>> GetContextsAsync(string? contextType = null, CancellationToken cancellationToken = default);
}
10 changes: 10 additions & 0 deletions src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Fusion.Resources.Functions.Common.ApiClients.ApiModels;

namespace Fusion.Resources.Functions.Common.ApiClients;

public interface IMailApiClient
{
public Task SendEmailAsync(SendEmailRequest request, CancellationToken cancellationToken = default);

public Task SendEmailWithTemplateAsync(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments,
public Task<ApiWeeklySummaryReport?> GetLatestWeeklyReportAsync(string departmentSapId,
CancellationToken cancellationToken = default);

/// <exception cref="SummaryApiError"></exception>
public Task<ApiWeeklyTaskOwnerReport?> GetLatestWeeklyTaskOwnerReportAsync(Guid projectId,
CancellationToken cancellationToken = default);

/// <exception cref="SummaryApiError"></exception>
public Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummaryReport report,
CancellationToken cancellationToken = default);
Expand Down
48 changes: 48 additions & 0 deletions src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Text;
using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
using Fusion.Resources.Functions.Common.Extensions;
using Fusion.Resources.Functions.Common.Integration.Errors;
using Fusion.Resources.Functions.Common.Integration.Http;
using Newtonsoft.Json;

namespace Fusion.Resources.Functions.Common.ApiClients;

public class MailApiClient : IMailApiClient
{
private readonly HttpClient mailClient;

public MailApiClient(IHttpClientFactory httpClientFactory)
{
mailClient = httpClientFactory.CreateClient(HttpClientNames.Application.Mail);
mailClient.Timeout = TimeSpan.FromMinutes(2);
}

public async Task SendEmailAsync(SendEmailRequest request, CancellationToken cancellationToken = default)
{
var json = JsonConvert.SerializeObject(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");

using var response = await mailClient.PostAsync("/mails", content, cancellationToken);

await ThrowIfNotSuccess(response);
}

public async Task SendEmailWithTemplateAsync(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default)
{
var json = JsonConvert.SerializeObject(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");

using var response = await mailClient.PostAsync($"templates/{templateName}/mails", content, cancellationToken);

await ThrowIfNotSuccess(response);
}

private async Task ThrowIfNotSuccess(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
throw new ApiError(response.RequestMessage!.RequestUri!.ToString(), response.StatusCode, body, "Response from API call indicates error");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ public async Task<ApiProject> PutProjectAsync(ApiProject project, CancellationTo
cancellationToken: cancellationToken))?.Items?.FirstOrDefault();
}

public async Task<ApiWeeklyTaskOwnerReport?> GetLatestWeeklyTaskOwnerReportAsync(Guid projectId, CancellationToken cancellationToken = default)
{
var lastMonday = DateTime.UtcNow.GetPreviousWeeksMondayDate();

var queryString = $"/projects/{projectId}/task-owners-summary-reports/weekly?$filter=PeriodStart eq '{lastMonday.Date:O}'&$top=1";


using var response = await summaryClient.GetAsync(queryString, cancellationToken);

await ThrowIfUnsuccessfulAsync(response);


await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);

return (await JsonSerializer.DeserializeAsync<ApiCollection<ApiWeeklyTaskOwnerReport>>(contentStream,
jsonSerializerOptions,
cancellationToken: cancellationToken))?.Items?.FirstOrDefault();
}

public async Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummaryReport report,
CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services
builder.AddRolesClient();
services.AddScoped<IRolesApiClient, RolesApiClient>();

builder.AddMailClient();
services.AddScoped<IMailApiClient, MailApiClient>();

builder.AddContextClient();
services.AddScoped<IContextApiClient, ContextApiClient>();

return services;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Fusion.Services.Org.ApiModels" Version="8.0.5"/>
<PackageReference Include="AdaptiveCards" Version="3.1.0" />
<PackageReference Include="AdaptiveCards" Version="2.7.3"/>
<PackageReference Include="Fusion.ApiClients.Org" Version="8.0.4"/>
<PackageReference Include="Fusion.Integration" Version="8.0.8"/>
<PackageReference Include="Fusion.Events.Azure.Functions.Extensions" Version="6.0.5"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Fusion.Resources.Functions.Common.Integration.Authentication;
using Fusion.Resources.Functions.Common.Integration.ServiceDiscovery;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Fusion.Resources.Functions.Common.Integration.Http.Handlers;

public class ContextHttpHandler : FunctionHttpMessageHandler
{
private readonly IOptions<HttpClientsOptions> options;

public ContextHttpHandler(ILoggerFactory logger, ITokenProvider tokenProvider, IServiceDiscovery serviceDiscovery, IOptions<HttpClientsOptions> options)
: base(logger.CreateLogger<ContextHttpHandler>(), tokenProvider, serviceDiscovery)
{
this.options = options;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await SetEndpointUriForRequestAsync(request, ServiceEndpoint.Context);
await AddAuthHeaderForRequestAsync(request, options.Value.Fusion);

return await base.SendAsync(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Fusion.Resources.Functions.Common.Integration.Authentication;
using Fusion.Resources.Functions.Common.Integration.ServiceDiscovery;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Fusion.Resources.Functions.Common.Integration.Http.Handlers;

public class MailHttpHandler : FunctionHttpMessageHandler
{
private readonly IOptions<HttpClientsOptions> options;

public MailHttpHandler(ILoggerFactory loggerFactory, ITokenProvider tokenProvider, IServiceDiscovery serviceDiscovery, IOptions<HttpClientsOptions> options)
: base(loggerFactory.CreateLogger<MailHttpHandler>(), tokenProvider, serviceDiscovery)
{
this.options = options;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await SetEndpointUriForRequestAsync(request, ServiceEndpoint.Mail);
await AddAuthHeaderForRequestAsync(request, options.Value.Fusion);

return await base.SendAsync(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public HttpClientFactoryBuilder AddRolesClient()
services.AddTransient<RolesHttpHandler>();
services.AddHttpClient(HttpClientNames.Application.Roles, client =>
{
client.BaseAddress = new Uri("https://fusion-notifications");
client.BaseAddress = new Uri("https://fusion-roles");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddHttpMessageHandler<RolesHttpHandler>()
Expand All @@ -122,6 +122,34 @@ public HttpClientFactoryBuilder AddRolesClient()
return this;
}

public HttpClientFactoryBuilder AddMailClient()
{
services.AddTransient<MailHttpHandler>();
services.AddHttpClient(HttpClientNames.Application.Mail, client =>
{
client.BaseAddress = new Uri("https://fusion-mail");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddHttpMessageHandler<MailHttpHandler>()
.AddTransientHttpErrorPolicy(DefaultRetryPolicy());

return this;
}

public HttpClientFactoryBuilder AddContextClient()
{
services.AddTransient<ContextHttpHandler>();
services.AddHttpClient(HttpClientNames.Application.Context, client =>
{
client.BaseAddress = new Uri("https://fusion-context");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddHttpMessageHandler<ContextHttpHandler>()
.AddTransientHttpErrorPolicy(DefaultRetryPolicy());

return this;
}

private readonly TimeSpan[] DefaultSleepDurations = new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) };

private Func<PolicyBuilder<HttpResponseMessage>, IAsyncPolicy<HttpResponseMessage>> DefaultRetryPolicy(TimeSpan[] sleepDurations = null) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static class Application
public const string Context = "App.Context";
public const string LineOrg = "App.LineOrg";
public const string Roles = "App.Roles";
public const string Mail = "App.Mail";
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public sealed class ServiceEndpoint
public static ServiceEndpoint Context = new ServiceEndpoint { Key = "context" };
public static ServiceEndpoint LineOrg = new ServiceEndpoint { Key = "lineorg" };
public static ServiceEndpoint Roles = new ServiceEndpoint { Key = "roles" };
public static ServiceEndpoint Mail = new ServiceEndpoint { Key = "mail" };
}
}
Loading

0 comments on commit ab0c33a

Please sign in to comment.