{previewText}
diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnNotificationConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnNotificationConfiguration.cs
new file mode 100644
index 0000000000..5106d7d809
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnNotificationConfiguration.cs
@@ -0,0 +1,91 @@
+using System.Xml.Serialization;
+using Altinn.App.Core.Features.Notifications;
+
+namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
+
+///
+/// Configuration properties for notifications in a process task.
+///
+public sealed class AltinnNotificationConfiguration
+{
+ /// Optionally set a notification provider that should be used for sending notifications related to this task.
+ /// The notification provider with a matching ID must be registered as a transient service in the DI container.
+ ///
+ /// The provider must be an implementation of
+ [XmlElement("notificationProviderId", Namespace = "http://altinn.no/process")]
+ public string? NotificationProviderId { get; set; }
+
+ ///
+ /// Configuration for sending SMS notifications. If not set, no SMS notifications will be sent.
+ ///
+ [XmlElement("smsConfig", Namespace = "http://altinn.no/process")]
+ public SmsConfig? SmsConfig { get; set; }
+
+
+ ///
+ /// Configuration for sending email notifications. If not set, no email notifications will be sent.
+ ///
+ [XmlElement("emailConfig", Namespace = "http://altinn.no/process")]
+ public EmailConfig? EmailConfig { get; set; }
+
+ internal ValidAltinnNotificationConfiguration Validate()
+ {
+ //TODO: implement validation logic
+
+ return new ValidAltinnNotificationConfiguration(NotificationProviderId, SmsConfig, EmailConfig);
+ }
+}
+
+///
+/// Configuration for sending SMS notifications
+///
+public class SmsConfig
+{
+ ///
+ /// Indicates whether an SMS should be sent or not. False by default.
+ ///
+ [XmlAttribute("sendSms")]
+ public bool SendSms { get; set; } = false;
+
+ ///
+ /// The senders number to be used when sending the SMS.
+ ///
+ [XmlElement("senderNumber", Namespace = "http://altinn.no/process")]
+ public string SenderNumber { get; set; } = string.Empty;
+
+ ///
+ /// Text resource ID for the body of the SMS.
+ ///
+ [XmlElement("bodyTextResource", Namespace = "http://altinn.no/process")]
+ public string BodyTextResource { get; set; } = string.Empty;
+}
+
+///
+/// Configuration for sending email notifications
+///
+public class EmailConfig
+{
+ ///
+ /// Indicates whether an email should be sent or not. False by default.
+ ///
+ [XmlAttribute("sendEmail")]
+ public bool SendEmail { get; set; } = false;
+
+ ///
+ /// Text resource ID for the subject of the email.
+ ///
+ [XmlElement("subjectTextResource", Namespace = "http://altinn.no/process")]
+ public string SubjectTextResource { get; set; } = string.Empty;
+
+ ///
+ /// Text resource ID for the body of the email.
+ ///
+ [XmlElement("bodyTextResource", Namespace = "http://altinn.no/process")]
+ public string BodyTextResource { get; set; } = string.Empty;
+}
+
+internal readonly record struct ValidAltinnNotificationConfiguration(
+ string? NotificationProviderId,
+ SmsConfig? SmsConfig,
+ EmailConfig? EmailConfig
+);
diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs
index c037ea7540..6437e842e6 100644
--- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs
+++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs
@@ -52,6 +52,12 @@ public class AltinnTaskExtension
[XmlElement("subformPdfConfig", Namespace = "http://altinn.no/process")]
public AltinnSubformPdfConfiguration? SubformPdfConfiguration { get; set; }
+ ///
+ /// Gets or sets the configuration for notifications
+ ///
+ [XmlElement("notificationConfig", Namespace = "http://altinn.no/process")]
+ public AltinnNotificationConfiguration? NotificationConfiguration { get; set; }
+
///
/// Retrieves a configuration item for given environment, in a predictable manner.
/// Specific configurations (those specifying an environment) takes precedence over global configurations.
diff --git a/src/Altinn.App.Core/Internal/Process/End/CancelNotificationsProcessEnd.cs b/src/Altinn.App.Core/Internal/Process/End/CancelNotificationsProcessEnd.cs
new file mode 100644
index 0000000000..7dba43409f
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/End/CancelNotificationsProcessEnd.cs
@@ -0,0 +1,37 @@
+using Altinn.App.Core.Features;
+using Altinn.App.Core.Models.Notifications.Order;
+using Altinn.Platform.Storage.Interface.Models;
+
+namespace Altinn.App.Core.Internal.Process.End;
+
+internal sealed class CancelNotificationsProcessEnd : IProcessEnd
+{
+ private readonly INotificationCancelClient _notificationCancelClient;
+
+ public CancelNotificationsProcessEnd(INotificationCancelClient notificationCancelClient)
+ {
+ _notificationCancelClient = notificationCancelClient;
+ }
+ public async Task End(Instance instance, List? events)
+ {
+ // TODO: Fetch notification orders
+ List notificationOrderIds = [];
+
+ foreach (string notificationOrderId in notificationOrderIds)
+ {
+ try
+ {
+ Guid orderGuid = Guid.Parse(notificationOrderId);
+ await _notificationCancelClient.Cancel(orderGuid, CancellationToken.None);
+ }
+ catch (NotificationCancelException e)
+ {
+ // Log and swallow exception, we don't want to fail the entire process end if cancelling notifications fails
+ }
+ catch (FormatException e)
+ {
+ // Log and swallow exception, the notification order id was not in a valid format
+ }
+ }
+ }
+}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
index 3c84167766..e8376d8703 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
@@ -360,7 +360,7 @@ serviceTaskResult is ServiceTaskFailedResult
if (moveToNextResult.IsEndEvent)
{
_telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange);
- await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events);
+ await RunProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events);
}
return new ProcessChangeResult(mutatedInstance: instance)
@@ -758,9 +758,9 @@ private async Task HandleMoveToNext(Instance instance, string?
}
///
- /// Runs IProcessEnd implementations defined in the app.
+ /// Runs all IProcessEnd implementations.
///
- private async Task RunAppDefinedProcessEndHandlers(Instance instance, List? events)
+ private async Task RunProcessEndHandlers(Instance instance, List? events)
{
var processEnds = _appImplementationFactory.GetAll().ToList();
if (processEnds.Count is 0)
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs
index cb0b17b624..7700c593ac 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs
@@ -5,6 +5,7 @@
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Expressions;
+using Altinn.App.Core.Internal.Language;
using Altinn.App.Core.Models;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.Extensions.DependencyInjection;
@@ -46,7 +47,7 @@ public async Task Finalize(string taskId, Instance instance)
{
ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata();
- var dataAccessor = await _instanceDataUnitOfWorkInitializer.Init(instance, taskId, "nb");
+ var dataAccessor = await _instanceDataUnitOfWorkInitializer.Init(instance, taskId, LanguageConst.Nb);
List tasks = [];
foreach (
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/NotificationServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/NotificationServiceTask.cs
new file mode 100644
index 0000000000..1923132848
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/NotificationServiceTask.cs
@@ -0,0 +1,139 @@
+using System.Configuration;
+using Altinn.App.Core.Features.Auth;
+using Altinn.App.Core.Features.Notifications;
+using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
+using Altinn.App.Core.Models.Notifications;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
+
+internal sealed class NotificationServiceTask : IServiceTask
+{
+ private readonly IProcessReader _processReader;
+ private readonly INotificationService _notificationService;
+ private readonly INotificationReader _notificationReader;
+ private readonly IAuthenticationContext _authenticationContext;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public string Type => "notify";
+
+ public NotificationServiceTask(
+ IProcessReader processReader,
+ INotificationService notificationService,
+ INotificationReader notificationReader,
+ IAuthenticationContext authenticationContext,
+ IHttpContextAccessor httpContextAccessor
+ )
+ {
+ _processReader = processReader;
+ _notificationService = notificationService;
+ _notificationReader = notificationReader;
+ _authenticationContext = authenticationContext;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public async Task Execute(ServiceTaskContext context)
+ {
+ CancellationToken ct = context.CancellationToken;
+ string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId;
+
+ ValidAltinnNotificationConfiguration notificationConfig = GetValidNotificationConfig(taskId);
+
+ string language = await GetLanguageFromContext();
+
+ if (string.IsNullOrWhiteSpace(notificationConfig.NotificationProviderId) is false)
+ {
+ await HandleInterfaceProvidedNotifications(notificationConfig.NotificationProviderId, language, ct);
+ }
+
+ if (notificationConfig.SmsConfig is not null || notificationConfig.EmailConfig is not null)
+ {
+ await HandleProcessConfigurationProvidedNotifications(context, notificationConfig, language, ct);
+ }
+
+ return ServiceTaskResult.Success();
+ }
+
+ private async Task GetLanguageFromContext()
+ {
+ HttpContext? httpContext = _httpContextAccessor.HttpContext;
+ var queries = httpContext?.Request.Query;
+ var auth = _authenticationContext.Current;
+
+ var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage();
+ return language;
+ }
+
+ private static string? GetOverriddenLanguage(IQueryCollection? queries)
+ {
+ if (queries is null)
+ {
+ return null;
+ }
+
+ if (
+ queries.TryGetValue("language", out StringValues queryLanguage)
+ || queries.TryGetValue("lang", out queryLanguage)
+ )
+ {
+ return queryLanguage.ToString();
+ }
+
+ return null;
+ }
+
+ private async Task HandleProcessConfigurationProvidedNotifications(
+ ServiceTaskContext context,
+ ValidAltinnNotificationConfiguration notificationConfig,
+ string language,
+ CancellationToken ct
+ )
+ {
+ List references = await _notificationService.NotifyInstanceOwner(
+ language,
+ context.InstanceDataMutator.Instance,
+ notificationConfig.EmailConfig ?? new EmailConfig(),
+ notificationConfig.SmsConfig ?? new SmsConfig(),
+ ct
+ );
+ }
+
+ private async Task HandleInterfaceProvidedNotifications(
+ string notificationProviderId,
+ string language,
+ CancellationToken ct
+ )
+ {
+ try
+ {
+ NotificationsWrapper nw = _notificationReader.GetProvidedNotifications(notificationProviderId, ct);
+
+ List references = await _notificationService.ProcessNotificationOrders(
+ language,
+ nw.EmailNotifications ?? [],
+ nw.SmsNotifications ?? [],
+ ct
+ );
+ }
+ catch (ConfigurationException ex)
+ {
+ // TODO: log. For now, rethrowing to explicitly show the exception that is expected for invalid configuration.
+ throw ex;
+ }
+ }
+
+ private ValidAltinnNotificationConfiguration GetValidNotificationConfig(string taskId)
+ {
+ AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId);
+ AltinnNotificationConfiguration? notificationConfig = altinnTaskExtension?.NotificationConfiguration;
+
+ if (notificationConfig == null)
+ {
+ // No notification configuration specified, return an empty configuration.
+ return new ValidAltinnNotificationConfiguration();
+ }
+
+ return notificationConfig.Validate();
+ }
+}
diff --git a/src/Altinn.App.Core/Internal/Texts/BackendTextResources.cs b/src/Altinn.App.Core/Internal/Texts/BackendTextResources.cs
new file mode 100644
index 0000000000..82797ab22c
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Texts/BackendTextResources.cs
@@ -0,0 +1,96 @@
+using Altinn.App.Core.Internal.Language;
+using Altinn.Platform.Storage.Interface.Models;
+
+namespace Altinn.App.Core.Internal.Texts;
+
+internal record BackendTextResource
+{
+ internal const string ValidationErrorsRequired = "backend.validation_errors.required";
+ internal const string PdfDefaultFileName = "backend.pdf_default_file_name";
+ internal const string PdfPreviewText = "backend.pdf_preview_text";
+ internal const string SmsDefaultBody = "backend.sms_default_body";
+ internal const string EmailDefaultSubject = "backend.email_default_subject";
+ internal const string EmailDefaultBody = "backend.email_default_body";
+}
+
+internal static class BackendTextResources
+{
+
+ internal static TextResourceElement? GetBackendFallbackResource(
+ string resource,
+ string language
+ )
+ {
+ return resource switch
+ {
+ BackendTextResource.ValidationErrorsRequired => new TextResourceElement()
+ {
+ Id = resource,
+ Value = language switch
+ {
+ LanguageConst.En => "Field is required",
+ LanguageConst.Nn => "Feltet er påkravd",
+ _ => "Feltet er påkrevd",
+ },
+ },
+ BackendTextResource.PdfDefaultFileName => new TextResourceElement()
+ {
+ Id = resource,
+ Value = "{0}.pdf",
+ Variables =
+ [
+ new TextResourceVariable()
+ {
+ Key = "appName",
+ DataSource = "text",
+ DefaultValue = "Altinn PDF",
+ },
+ ],
+ },
+ BackendTextResource.PdfPreviewText => new TextResourceElement()
+ {
+ Id = resource,
+ Value = language switch
+ {
+ LanguageConst.En => "The document is a preview",
+ LanguageConst.Nn => "Dokumentet er ein førehandsvisning",
+ _ => "Dokumentet er en forhåndsvisning",
+ },
+ },
+ BackendTextResource.SmsDefaultBody =>
+ new TextResourceElement()
+ {
+ Id = resource,
+ Value = language switch
+ {
+ LanguageConst.En => "You have received a message in your Altinn inbox. Log in to view the message.",
+ LanguageConst.Nn => "Du har motteke ei melding i innboksen din i Altinn. Logg inn for å sjå meldinga.",
+ _ => "Du har mottatt en melding i innboksen din i Altinn. Logg inn for å se meldingen.",
+ },
+ },
+ BackendTextResource.EmailDefaultSubject =>
+ new TextResourceElement()
+ {
+ Id = resource,
+ Value = language switch
+ {
+ LanguageConst.En => "You have received a message in your Altinn inbox",
+ LanguageConst.Nn => "Du har motteke ei melding i innboksen din i Altinn",
+ _ => "Du har mottatt en melding i innboksen din i Altinn",
+ },
+ },
+ BackendTextResource.EmailDefaultBody =>
+ new TextResourceElement()
+ {
+ Id = resource,
+ Value = language switch
+ {
+ LanguageConst.En => "You have received a message in your Altinn inbox. Log in to view the message.",
+ LanguageConst.Nn => "Du har motteke ei melding i innboksen din i Altinn. Logg inn for å sjå meldinga.",
+ _ => "Du har mottatt en melding i innboksen din i Altinn. Logg inn for å se meldingen.",
+ },
+ },
+ _ => null,
+ };
+ }
+}
diff --git a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs
index a3d078c69d..5598dca4b8 100644
--- a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs
+++ b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs
@@ -281,54 +281,7 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn
return new TextResourceElement() { Id = "appName", Value = _app };
}
- return GetBackendFallbackResource(key, language);
- }
-
- private static TextResourceElement? GetBackendFallbackResource(string key, string language)
- {
- // When the list of backend text resources grows, we might want to have these in a separate file or similar.
- switch (key)
- {
- case "backend.validation_errors.required":
- return new TextResourceElement()
- {
- Id = "backend.validation_errors.required",
- Value = language switch
- {
- LanguageConst.Nb => "Feltet er påkrevd",
- LanguageConst.Nn => "Feltet er påkravd",
- _ => "Field is required",
- },
- };
- case "backend.pdf_default_file_name":
- return new TextResourceElement()
- {
- Id = "backend.pdf_default_file_name",
- Value = "{0}.pdf",
- Variables =
- [
- new TextResourceVariable()
- {
- Key = "appName",
- DataSource = "text",
- DefaultValue = "Altinn PDF",
- },
- ],
- };
- case "pdfPreviewText":
- return new TextResourceElement()
- {
- Id = "pdfPreviewText",
- Value = language switch
- {
- LanguageConst.En => "The document is a preview",
- LanguageConst.Nn => "Dokumentet er ein førehandsvisning",
- _ => "Dokumentet er en forhåndsvisning",
- },
- };
- }
-
- return null;
+ return BackendTextResources.GetBackendFallbackResource(key, language);
}
///
diff --git a/src/Altinn.App.Core/Models/Notifications/Email/EmailRecipient.cs b/src/Altinn.App.Core/Models/Notifications/Email/EmailRecipient.cs
index 94d9d6613a..84e52275cd 100644
--- a/src/Altinn.App.Core/Models/Notifications/Email/EmailRecipient.cs
+++ b/src/Altinn.App.Core/Models/Notifications/Email/EmailRecipient.cs
@@ -5,4 +5,8 @@ namespace Altinn.App.Core.Models.Notifications.Email;
///
/// Represents the recipient of an email.
///
-public sealed record EmailRecipient([property: JsonPropertyName("emailAddress")] string EmailAddress);
+public sealed record EmailRecipient(
+ [property: JsonPropertyName("emailAddress")] string? EmailAddress = null,
+ [property: JsonPropertyName("organizationNumber")] string? OrganizationNumber = null,
+ [property: JsonPropertyName("nationalIdentityNumber")] string? NationalIdentityNumber = null
+);
diff --git a/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderException.cs b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderException.cs
new file mode 100644
index 0000000000..f8a05fbe1d
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderException.cs
@@ -0,0 +1,20 @@
+using Altinn.App.Core.Exceptions;
+
+namespace Altinn.App.Core.Models.Notifications.Future;
+
+///
+/// Exception thrown when a notification order could not be created.
+///
+public sealed class NotificationOrderException : AltinnException
+{
+ internal NotificationOrderException(
+ string? message,
+ HttpResponseMessage? response,
+ string? content,
+ Exception? innerException
+ )
+ : base(
+ $"{message}: StatusCode={response?.StatusCode}\nReason={response?.ReasonPhrase}\nBody={content}\n",
+ innerException
+ ) { }
+}
diff --git a/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderRequest.cs b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderRequest.cs
new file mode 100644
index 0000000000..691c570253
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderRequest.cs
@@ -0,0 +1,376 @@
+using System.Text.Json.Serialization;
+
+namespace Altinn.App.Core.Models.Notifications.Future;
+
+///
+/// Represents a request for creating a notification order in the Altinn Notifications service.
+///
+public sealed record NotificationOrderRequest
+{
+ ///
+ /// Gets or sets the idempotency identifier for this request.
+ ///
+ ///
+ /// Repeated requests with the same identifier will only result in one notification order being created.
+ ///
+ [JsonPropertyName("idempotencyId")]
+ public required string IdempotencyId { get; init; }
+
+ ///
+ /// Gets or sets an optional reference identifier from the app.
+ ///
+ ///
+ /// Use this to correlate the notification order with a record in your app, such as an instance ID.
+ ///
+ [JsonPropertyName("sendersReference")]
+ public required string SendersReference { get; init; }
+
+ ///
+ /// Gets or sets the earliest time the notification should be sent.
+ ///
+ ///
+ /// Defaults to the current UTC time, meaning the notification will be sent as soon as possible.
+ ///
+ [JsonPropertyName("requestedSendTime")]
+ public DateTime RequestedSendTime { get; init; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets an optional endpoint the Altinn Notifications service will call to determine
+ /// whether the notification should still be sent at delivery time.
+ ///
+ ///
+ /// Useful for notifications scheduled in the future where the condition for sending may no longer
+ /// apply by the time the send time is reached.
+ ///
+ [JsonPropertyName("conditionEndpoint")]
+ public Uri? ConditionEndpoint { get; init; }
+
+ ///
+ /// Gets or sets the recipient of the notification.
+ ///
+ [JsonPropertyName("recipient")]
+ public required NotificationRecipient Recipient { get; init; }
+}
+
+///
+/// Defines the recipient of a notification order. Exactly one recipient type should be set.
+///
+public sealed record NotificationRecipient
+{
+ ///
+ /// Gets or sets a recipient identified by a direct email address.
+ ///
+ [JsonPropertyName("recipientEmail")]
+ public RecipientEmail? RecipientEmail { get; init; }
+
+ ///
+ /// Gets or sets a recipient identified by a direct phone number.
+ ///
+ [JsonPropertyName("recipientSms")]
+ public RecipientSms? RecipientSms { get; init; }
+
+ ///
+ /// Gets or sets a recipient identified by a Norwegian national identity number.
+ ///
+ ///
+ /// Contact information will be retrieved from the Common Contact Register (KRR).
+ ///
+ [JsonPropertyName("recipientPerson")]
+ public RecipientPerson? RecipientPerson { get; init; }
+
+ ///
+ /// Gets or sets a recipient identified by a Norwegian organization number.
+ ///
+ ///
+ /// Contact information will be retrieved from the Central Coordinating Register for Legal Entities (Enhetsregisteret).
+ ///
+ [JsonPropertyName("recipientOrganization")]
+ public RecipientOrganization? RecipientOrganization { get; init; }
+
+ ///
+ /// Gets or sets a recipient identified by an external identity, used for self-identified users
+ /// who authenticate via ID-porten email login.
+ ///
+ ///
+ /// Contact information will be retrieved from Altinn Profile using the external identity.
+ ///
+ [JsonPropertyName("recipientSelfIdentifiedUser")]
+ public RecipientSelfIdentifiedUser? RecipientSelfIdentifiedUser { get; init; }
+}
+
+///
+/// Identifies a notification recipient by a direct email address.
+///
+public sealed record RecipientEmail
+{
+ ///
+ /// Gets or sets the recipient's email address.
+ ///
+ [JsonPropertyName("emailAddress")]
+ public required string EmailAddress { get; init; }
+
+ ///
+ /// Gets or sets the email content and delivery options.
+ ///
+ [JsonPropertyName("emailSettings")]
+ public required EmailSendingOptions Settings { get; init; }
+}
+
+///
+/// Identifies a notification recipient by a direct phone number.
+///
+public sealed record RecipientSms
+{
+ ///
+ /// Gets or sets the recipient's phone number in international format.
+ ///
+ [JsonPropertyName("phoneNumber")]
+ public required string PhoneNumber { get; init; }
+
+ ///
+ /// Gets or sets the SMS content and delivery options.
+ ///
+ [JsonPropertyName("smsSettings")]
+ public required SmsSendingOptions Settings { get; init; }
+}
+
+///
+/// Identifies a notification recipient by a Norwegian national identity number.
+///
+public sealed record RecipientPerson
+{
+ ///
+ /// Gets or sets the recipient's national identity number.
+ ///
+ [JsonPropertyName("nationalIdentityNumber")]
+ public required string NationalIdentityNumber { get; init; }
+
+ ///
+ /// Gets or sets an optional Altinn resource identifier used for authorization and auditing.
+ ///
+ [JsonPropertyName("resourceId")]
+ public string? ResourceId { get; init; }
+
+ ///
+ /// Gets or sets the notification channel to use.
+ ///
+ ///
+ /// Defaults to , meaning email will be attempted
+ /// first with SMS as fallback.
+ ///
+ [JsonPropertyName("channelSchema")]
+ public NotificationChannel ChannelSchema { get; init; } = NotificationChannel.EmailPreferred;
+
+ ///
+ /// Gets or sets whether to ignore the recipient's reservation against electronic communication in KRR.
+ ///
+ [JsonPropertyName("ignoreReservation")]
+ public bool IgnoreReservation { get; init; } = false;
+
+ ///
+ /// Gets or sets email content and delivery options. Required when the channel scheme includes email.
+ ///
+ [JsonPropertyName("emailSettings")]
+ public EmailSendingOptions? EmailSettings { get; init; }
+
+ ///
+ /// Gets or sets SMS content and delivery options. Required when the channel scheme includes SMS.
+ ///
+ [JsonPropertyName("smsSettings")]
+ public SmsSendingOptions? SmsSettings { get; init; }
+}
+
+///
+/// Identifies a notification recipient by a Norwegian organization number.
+///
+public sealed record RecipientOrganization
+{
+ ///
+ /// Gets or sets the organization number.
+ ///
+ [JsonPropertyName("orgNumber")]
+ public required string OrgNumber { get; init; }
+
+ ///
+ /// Gets or sets an optional Altinn resource identifier used for authorization and auditing.
+ ///
+ [JsonPropertyName("resourceId")]
+ public string? ResourceId { get; init; }
+
+ ///
+ /// Gets or sets the notification channel to use.
+ ///
+ [JsonPropertyName("channelSchema")]
+ public required NotificationChannel ChannelSchema { get; init; }
+
+ ///
+ /// Gets or sets email content and delivery options. Required when the channel scheme includes email.
+ ///
+ [JsonPropertyName("emailSettings")]
+ public EmailSendingOptions? EmailSettings { get; init; }
+
+ ///
+ /// Gets or sets SMS content and delivery options. Required when the channel scheme includes SMS.
+ ///
+ [JsonPropertyName("smsSettings")]
+ public SmsSendingOptions? SmsSettings { get; init; }
+}
+
+///
+/// Identifies a notification recipient by an external identity, used for self-identified users
+/// who authenticate via ID-porten email login.
+///
+public sealed record RecipientSelfIdentifiedUser
+{
+ ///
+ /// Gets or sets the recipient's external identity in URN format.
+ ///
+ ///
+ /// Supported formats:
+ ///
+ /// - urn:altinn:person:idporten-email:{email} — ID-porten email login
+ /// - urn:altinn:person:legacy-selfidentified:{username} — legacy username/password login
+ ///
+ ///
+ [JsonPropertyName("externalIdentity")]
+ public required string ExternalIdentity { get; init; }
+
+ ///
+ /// Gets or sets an optional Altinn resource identifier used for authorization and auditing.
+ ///
+ [JsonPropertyName("resourceId")]
+ public string? ResourceId { get; init; }
+
+ ///
+ /// Gets or sets the notification channel to use. Defaults to .
+ ///
+ [JsonPropertyName("channelSchema")]
+ public NotificationChannel ChannelSchema { get; init; } = NotificationChannel.Email;
+
+ ///
+ /// Gets or sets email content and delivery options. Required when the channel scheme includes email.
+ ///
+ [JsonPropertyName("emailSettings")]
+ public EmailSendingOptions? EmailSettings { get; init; }
+
+ ///
+ /// Gets or sets SMS content and delivery options. Required when the channel scheme includes SMS.
+ ///
+ [JsonPropertyName("smsSettings")]
+ public SmsSendingOptions? SmsSettings { get; init; }
+}
+
+///
+/// Defines content and delivery options for an email notification.
+///
+public sealed record EmailSendingOptions
+{
+ ///
+ /// Gets or sets an optional sender email address to display to the recipient.
+ ///
+ [JsonPropertyName("senderEmailAddress")]
+ public string? SenderEmailAddress { get; init; }
+
+ ///
+ /// Gets or sets the subject line of the email.
+ ///
+ [JsonPropertyName("subject")]
+ public required string Subject { get; init; }
+
+ ///
+ /// Gets or sets the body content of the email.
+ ///
+ [JsonPropertyName("body")]
+ public required string Body { get; init; }
+
+ ///
+ /// Gets or sets the content type of the email body. Defaults to .
+ ///
+ [JsonPropertyName("contentType")]
+ public EmailContentType ContentType { get; init; } = EmailContentType.Plain;
+
+ ///
+ /// Gets or sets when the email may be delivered. Defaults to .
+ ///
+ [JsonPropertyName("sendingTimePolicy")]
+ public SendingTimePolicy SendingTimePolicy { get; init; } = SendingTimePolicy.Anytime;
+}
+
+///
+/// Defines content and delivery options for an SMS notification.
+///
+public sealed record SmsSendingOptions
+{
+ ///
+ /// Gets or sets an optional sender name or number displayed to the recipient.
+ ///
+ [JsonPropertyName("sender")]
+ public string? Sender { get; init; }
+
+ ///
+ /// Gets or sets the text content of the SMS.
+ ///
+ [JsonPropertyName("body")]
+ public required string Body { get; init; }
+
+ ///
+ /// Gets or sets when the SMS may be delivered. Defaults to
+ /// to avoid sending messages at unsociable hours.
+ ///
+ [JsonPropertyName("sendingTimePolicy")]
+ public SendingTimePolicy SendingTimePolicy { get; init; } = SendingTimePolicy.Daytime;
+}
+
+///
+/// Defines the notification channel or channel priority scheme to use when delivering a notification.
+///
+public enum NotificationChannel
+{
+ /// Email only.
+ [JsonStringEnumMemberName("Email")]
+ Email,
+
+ /// SMS only.
+ [JsonStringEnumMemberName("Sms")]
+ Sms,
+
+ /// Email first, SMS as fallback if the recipient has no email address.
+ [JsonStringEnumMemberName("EmailPreferred")]
+ EmailPreferred,
+
+ /// SMS first, email as fallback if the recipient has no phone number.
+ [JsonStringEnumMemberName("SmsPreferred")]
+ SmsPreferred,
+
+ /// Both email and SMS are sent simultaneously.
+ [JsonStringEnumMemberName("EmailAndSms")]
+ EmailAndSms,
+}
+
+///
+/// Defines the content type of an email body.
+///
+public enum EmailContentType
+{
+ /// Plain text.
+ [JsonStringEnumMemberName("Plain")]
+ Plain,
+
+ /// HTML markup.
+ [JsonStringEnumMemberName("Html")]
+ Html,
+}
+
+///
+/// Defines when a notification may be delivered.
+///
+public enum SendingTimePolicy
+{
+ /// The notification may be sent at any time of day.
+ [JsonStringEnumMemberName("Anytime")]
+ Anytime,
+
+ /// The notification will only be sent during daytime hours.
+ [JsonStringEnumMemberName("Daytime")]
+ Daytime,
+}
diff --git a/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderResponse.cs b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderResponse.cs
new file mode 100644
index 0000000000..79b499c297
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/Future/NotificationOrderResponse.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Altinn.App.Core.Models.Notifications.Future;
+
+///
+/// Represents the response received after ordering a notification, including details about the notification and any associated reminders.
+///
+public sealed record NotificationOrderResponse
+{
+ ///
+ /// The unique identifier for the notification order, which can be used for tracking and reference purposes.
+ ///
+ [JsonPropertyName("notificationOrderId")]
+ public required Guid OrderChainId { get; init; }
+
+ ///
+ /// Details about the notification.
+ ///
+ [JsonPropertyName("notification")]
+ public required NotificationOrderShipment Notification { get; init; }
+
+ ///
+ /// A list of reminders associated with the notification order, if any
+ ///
+ [JsonPropertyName("reminders")]
+ public List Reminders { get; init; } = [];
+}
+
+///
+/// Represents the details of a notification shipment.
+///
+public sealed record NotificationOrderShipment
+{
+ ///
+ /// The unique identifier for the notification shipment.
+ /// Used to cancel the shipment if needed.
+ ///
+ [JsonPropertyName("shipmentId")]
+ public required Guid ShipmentId { get; init; }
+
+ ///
+ /// The reference from the request.
+ ///
+ [JsonPropertyName("sendersReference")]
+ public string? SendersReference { get; init; }
+}
diff --git a/src/Altinn.App.Core/Models/Notifications/NotificationReference.cs b/src/Altinn.App.Core/Models/Notifications/NotificationReference.cs
new file mode 100644
index 0000000000..1995cab05a
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/NotificationReference.cs
@@ -0,0 +1,8 @@
+namespace Altinn.App.Core.Models.Notifications;
+
+///
+/// Represents a reference to a notification order, which can be used to track the status of the notification.
+///
+/// The reference provided by the sender of the notification, which can be used to correlate the notification with the sender's own records.
+/// The unique identifier for the notification order, which can be used to track the status of the notification in the notification system.
+public sealed record NotificationReference(string SendersReference, string OrderId);
diff --git a/src/Altinn.App.Core/Models/Notifications/NotificationsWrapper.cs b/src/Altinn.App.Core/Models/Notifications/NotificationsWrapper.cs
new file mode 100644
index 0000000000..13e96eefaa
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/NotificationsWrapper.cs
@@ -0,0 +1,9 @@
+using Altinn.App.Core.Models.Notifications.Email;
+using Altinn.App.Core.Models.Notifications.Sms;
+
+namespace Altinn.App.Core.Models.Notifications;
+
+internal readonly record struct NotificationsWrapper(
+ List? EmailNotifications,
+ List? SmsNotifications
+);
diff --git a/src/Altinn.App.Core/Models/Notifications/Order/NotificationCancelException.cs b/src/Altinn.App.Core/Models/Notifications/Order/NotificationCancelException.cs
new file mode 100644
index 0000000000..46c04bd270
--- /dev/null
+++ b/src/Altinn.App.Core/Models/Notifications/Order/NotificationCancelException.cs
@@ -0,0 +1,20 @@
+using Altinn.App.Core.Exceptions;
+
+namespace Altinn.App.Core.Models.Notifications.Order;
+
+///
+/// Exception thrown when a notification order could not be cancelled.
+///
+public sealed class NotificationCancelException : AltinnException
+{
+ internal NotificationCancelException(
+ string? message,
+ HttpResponseMessage? response,
+ string? content,
+ Exception? innerException
+ )
+ : base(
+ $"{message}: StatusCode={response?.StatusCode}\nReason={response?.ReasonPhrase}\nBody={content}\n",
+ innerException
+ ) { }
+}
diff --git a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs
index d011e92d10..78f169ebec 100644
--- a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs
+++ b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs
@@ -15,9 +15,8 @@ public sealed record SmsNotification
///
[JsonPropertyName("senderNumber")]
public required string SenderNumber { get; init; }
-
///
- /// The phone number to use as sender of the SMS.
+ /// The body of the SMS.
///
[JsonPropertyName("body")]
public required string Body { get; init; }
@@ -42,7 +41,8 @@ public DateTime? RequestedSendTime
}
///
- /// The phone number to use as sender of the SMS.
+ /// The senders reference for the sms.
+ /// Used to track the sms request.
///
[JsonPropertyName("sendersReference")]
public required string SendersReference { get; init; }
@@ -52,4 +52,10 @@ public DateTime? RequestedSendTime
///
[JsonPropertyName("recipients")]
public required IReadOnlyList
Recipients { get; init; }
+
+ ///
+ /// Gets or sets the ID of the resource that the notification is related to.
+ ///
+ [JsonPropertyName("resourceId")]
+ public string? ResourceId { get; set; }
}
diff --git a/src/Altinn.App.Core/Models/Notifications/Sms/SmsRecipient.cs b/src/Altinn.App.Core/Models/Notifications/Sms/SmsRecipient.cs
index 0d79f1216c..1aba19e495 100644
--- a/src/Altinn.App.Core/Models/Notifications/Sms/SmsRecipient.cs
+++ b/src/Altinn.App.Core/Models/Notifications/Sms/SmsRecipient.cs
@@ -12,7 +12,7 @@ namespace Altinn.App.Core.Models.Notifications.Sms;
/// Organization number.
/// National Identity number.
public sealed record SmsRecipient(
- [property: JsonPropertyName("mobileNumber")] string MobileNumber,
- [property: JsonPropertyName("organisationNumber")] string? OrganisationNumber = null,
+ [property: JsonPropertyName("mobileNumber")] string? MobileNumber = null,
+ [property: JsonPropertyName("organizationNumber")] string? OrganisationNumber = null,
[property: JsonPropertyName("nationalIdentityNumber")] string? NationalIdentityNumber = null
);