Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageVersion Include="Altinn.Common.EFormidlingClient" Version="1.3.3" />
<PackageVersion Include="Altinn.Common.PEP" Version="4.2.2" />
<PackageVersion Include="Altinn.Platform.Models" Version="1.6.1" />
<PackageVersion Include="Altinn.Platform.Storage.Interface" Version="4.3.0" />
<PackageVersion Include="Altinn.Platform.Storage.Interface" Version="4.4.0" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.6.0" />
Expand Down
2 changes: 2 additions & 0 deletions src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Altinn.App.Core.Features.ExternalApi;
using Altinn.App.Core.Features.FileAnalyzis;
using Altinn.App.Core.Features.Notifications.Email;
using Altinn.App.Core.Features.Notifications.Order;
using Altinn.App.Core.Features.Notifications.Sms;
using Altinn.App.Core.Features.Options;
using Altinn.App.Core.Features.Options.Altinn3LibraryCodeList;
Expand Down Expand Up @@ -277,6 +278,7 @@ private static void AddNotificationServices(IServiceCollection services)
{
services.AddHttpClient<IEmailNotificationClient, EmailNotificationClient>();
services.AddHttpClient<ISmsNotificationClient, SmsNotificationClient>();
services.AddHttpClient<INotificationCancelClient, NotificationCancelClient>();
}

private static void AddPdfServices(IServiceCollection services)
Expand Down
17 changes: 17 additions & 0 deletions src/Altinn.App.Core/Features/INotificationCancelClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Altinn.App.Core.Features;

/// <summary>
/// Client for managing cancellation of notifications ordered through the app.
/// This is used to cancel notifications that have been ordered but not yet sent,
/// for example when a process is ended before the notification has been sent.
/// </summary>
public interface INotificationCancelClient
{
/// <summary>
/// Cancels a previously ordered notification.
/// </summary>
/// <param name="notificationOrderId">The order ID of the notification to cancel.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task Cancel(Guid notificationOrderId, CancellationToken ct);
}
17 changes: 17 additions & 0 deletions src/Altinn.App.Core/Features/INotificationOrderClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Altinn.App.Core.Models.Notifications.Future;

namespace Altinn.App.Core.Features;

/// <summary>
/// Client for managing notification orders in the Altinn notification system.
/// </summary>
public interface INotificationOrderClient
{
/// <summary>
/// Orders a notification based on the provided request.
/// </summary>
/// <param name="request"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<NotificationOrderResponse> Order(NotificationOrderRequest request, CancellationToken ct);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public EmailNotificationClient(

public async Task<EmailOrderResponse> Order(EmailNotification emailNotification, CancellationToken ct)
{
using var activity = _telemetry?.StartNotificationOrderActivity(_orderType);
using var activity = _telemetry?.StartNotificationOrderActivity(_orderType, emailNotification.SendersReference);

HttpResponseMessage? httpResponseMessage = null;
string? httpContent = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Net.Http.Headers;
using System.Text.Json;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Models.Notifications.Future;
using Altinn.Common.AccessTokenClient.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Altinn.App.Core.Features.Notifications.Future;

internal sealed class NotificationOrderClient : INotificationOrderClient
{
private readonly ILogger<NotificationOrderClient> _logger;
private readonly HttpClient _httpClient;
private readonly PlatformSettings _platformSettings;
private readonly IAppMetadata _appMetadata;
private readonly IAccessTokenGenerator _accessTokenGenerator;
private readonly Telemetry? _telemetry;

public NotificationOrderClient(
ILogger<NotificationOrderClient> logger,
HttpClient httpClient,
IOptions<PlatformSettings> platformSettings,
IAppMetadata appMetadata,
IAccessTokenGenerator accessTokenGenerator,
Telemetry? telemetry = null
)
{
_logger = logger;
_httpClient = httpClient;
_platformSettings = platformSettings.Value;
_appMetadata = appMetadata;
_accessTokenGenerator = accessTokenGenerator;
_telemetry = telemetry;
}

public async Task<NotificationOrderResponse> Order(NotificationOrderRequest request, CancellationToken ct)
{
using var activity = _telemetry?.StartNotificationOrderActivity(
Telemetry.Notifications.OrderType.Future,
request.SendersReference
);

HttpResponseMessage? httpResponseMessage = null;
string? httpContent = null;
try
{
var application = await _appMetadata.GetApplicationMetadata();

var uri = _platformSettings.ApiNotificationEndpoint.TrimEnd('/') + "/future/orders";
var body = JsonSerializer.Serialize(request);

using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StringContent(body, new MediaTypeHeaderValue("application/json")),
};
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Headers.Add(
Constants.General.PlatformAccessTokenHeaderName,
_accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)
);

httpResponseMessage = await _httpClient.SendAsync(httpRequestMessage, ct);
httpContent = await httpResponseMessage.Content.ReadAsStringAsync(ct);

if (httpResponseMessage.IsSuccessStatusCode)
{
var orderResponse =
JsonSerializer.Deserialize<NotificationOrderResponse>(httpContent)
?? throw new JsonException("Couldn't deserialize notification order response.");

_telemetry?.RecordNotificationOrder(
Telemetry.Notifications.OrderType.Future,
Telemetry.Notifications.OrderResult.Success
);
return orderResponse;
}

throw new HttpRequestException(
$"Got error status code for notification order: {(int)httpResponseMessage.StatusCode}"
);
}
catch (Exception e) when (e is not NotificationOrderException)
{
_telemetry?.RecordNotificationOrder(
Telemetry.Notifications.OrderType.Future,
Telemetry.Notifications.OrderResult.Error
);

var ex = new NotificationOrderException(
$"Something went wrong when processing the notification order",
httpResponseMessage,
httpContent,
e
);
_logger.LogError(ex, "Error when processing notification order");
throw ex;
}
finally
{
httpResponseMessage?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Altinn.App.Core.Models.Notifications.Email;
using Altinn.App.Core.Models.Notifications.Sms;

namespace Altinn.App.Core.Features.Notifications;

/// <summary>
/// Interface for implementing app-specific logic for deriving notifications.
/// </summary>
[ImplementableByApps]
public interface INotificationProvider
{
/// <summary>
/// Used to select the correct <see cref="INotificationProvider" /> implementation for a given notification task. Should match the NotificationProviderId parameter in the task configuration.
/// </summary>
public string Id { get; init; }

/// <summary>
/// Returns the email notification to be sent for the given notification task.
/// </summary>
public List<EmailNotification> ProvidedEmailNotifications { get; }

/// <summary>
/// Returns the SMS notification to be sent for the given notification task.
/// </summary>
public List<SmsNotification> ProvidedSmsNotifications { get; }
}
11 changes: 11 additions & 0 deletions src/Altinn.App.Core/Features/Notifications/INotificationReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Altinn.App.Core.Models.Notifications;

namespace Altinn.App.Core.Features.Notifications;

internal interface INotificationReader
{
/// <summary>
/// Gets the notifications for the current task.
/// </summary>
NotificationsWrapper GetProvidedNotifications(string notificationProviderId, CancellationToken ct);
}
24 changes: 24 additions & 0 deletions src/Altinn.App.Core/Features/Notifications/INotificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
using Altinn.App.Core.Models.Notifications;
using Altinn.App.Core.Models.Notifications.Email;
using Altinn.App.Core.Models.Notifications.Sms;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features.Notifications;

internal interface INotificationService
{
Task<List<NotificationReference>> ProcessNotificationOrders(
string language,
List<EmailNotification> emailNotifications,
List<SmsNotification> smsNotifications,
CancellationToken ct
);
Task<List<NotificationReference>> NotifyInstanceOwner(
string language,
Instance instance,
EmailConfig? emailOverride,
SmsConfig? smsOverride,
CancellationToken ct
);
}
38 changes: 38 additions & 0 deletions src/Altinn.App.Core/Features/Notifications/NotificationReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Altinn.App.Core.Exceptions;
using Altinn.App.Core.Models.Notifications;

namespace Altinn.App.Core.Features.Notifications;

internal sealed class NotificationReader : INotificationReader
{
private readonly AppImplementationFactory _appImplementationFactory;

public NotificationReader(AppImplementationFactory appImplementationFactory)
{
_appImplementationFactory = appImplementationFactory;
}

public NotificationsWrapper GetProvidedNotifications(string notificationProviderId, CancellationToken ct)
{
INotificationProvider provider;
try
{
provider = _appImplementationFactory
.GetAll<INotificationProvider>()
.Single(x => x.Id == notificationProviderId);
}
catch (InvalidOperationException ex)
{
throw new ConfigurationException(
$"The notification provider id did not match an implementation of {nameof(INotificationProvider)}",
ex
);
}

var notificationsWrapper = new NotificationsWrapper(
provider.ProvidedEmailNotifications,
provider.ProvidedSmsNotifications
);
return notificationsWrapper;
}
}
Loading
Loading