From 89887bce65a1f3f3af890820cf3134845f8bf46e Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 15 Nov 2024 11:29:20 +0100 Subject: [PATCH 01/75] Improve the documentation --- src/Altinn.Notifications/Models/RecipientExt.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Altinn.Notifications/Models/RecipientExt.cs b/src/Altinn.Notifications/Models/RecipientExt.cs index 91111f0d..307b921f 100644 --- a/src/Altinn.Notifications/Models/RecipientExt.cs +++ b/src/Altinn.Notifications/Models/RecipientExt.cs @@ -3,39 +3,39 @@ namespace Altinn.Notifications.Models; /// -/// Class representing a notification recipient +/// Class representing a notification recipient. /// /// -/// External representaion to be used in the API. +/// External representation to be used in the API. /// public class RecipientExt { /// - /// Gets or sets the email address of the recipient + /// Gets or sets the email address of the recipient. /// [JsonPropertyName("emailAddress")] public string? EmailAddress { get; set; } /// - /// Gets or sets the mobile number of the recipient + /// Gets or sets the mobile number of the recipient. /// [JsonPropertyName("mobileNumber")] public string? MobileNumber { get; set; } /// - /// Gets or sets the organization number of the recipient + /// Gets or sets the organization number of the recipient. /// [JsonPropertyName("organizationNumber")] public string? OrganizationNumber { get; set; } /// - /// Gets or sets the national identity number of the recipient + /// Gets or sets the national identity number of the recipient. /// [JsonPropertyName("nationalIdentityNumber")] public string? NationalIdentityNumber { get; set; } /// - /// Gets or sets a value indicating whether the recipient is reserved from digital communication + /// Gets or sets a value indicating whether the recipient is reserved from digital communication. /// [JsonPropertyName("isReserved")] public bool? IsReserved { get; set; } From 01eeb3fc0bef179293043ce4c6d7965130c128cf Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 15 Nov 2024 11:31:38 +0100 Subject: [PATCH 02/75] Improve the documentation and initialization --- .../Models/NotificationOrderRequestBaseExt.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs index a73d42a6..a2cc66a2 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs @@ -1,46 +1,44 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; /// -/// Base class for common properties of notification order requests +/// Base class for common properties of notification order requests. /// public class NotificationOrderRequestBaseExt { /// - /// Gets or sets the send time of the email. Defaults to UtcNow + /// Gets or sets the send time of the email. Defaults to UtcNow. /// [JsonPropertyName("requestedSendTime")] public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow; /// - /// Gets or sets the senders reference on the notification + /// Gets or sets the sender's reference on the notification. /// [JsonPropertyName("sendersReference")] public string? SendersReference { get; set; } /// - /// Gets or sets the list of recipients + /// Gets or sets the list of recipients. /// [JsonPropertyName("recipients")] - [Required] - public List Recipients { get; set; } = new List(); + public List Recipients { get; set; } = []; /// - /// Gets or sets whether notifications generated by this order should ignore KRR reservations + /// Gets or sets whether notifications generated by this order should ignore KRR reservations. /// [JsonPropertyName("ignoreReservation")] public bool? IgnoreReservation { get; set; } /// - /// Gets or sets the id of the resource that the notification is related to + /// Gets or sets the ID of the resource that the notification is related to. /// [JsonPropertyName("resourceId")] public string? ResourceId { get; set; } /// - /// Gets or sets the condition endpoint used to check the send condition + /// Gets or sets the condition endpoint used to check the send condition. /// [JsonPropertyName("conditionEndpoint")] public Uri? ConditionEndpoint { get; set; } From 43ed8557f19b29f467488bb8e2fe5880fde30b4f Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 15 Nov 2024 11:33:45 +0100 Subject: [PATCH 03/75] Improve the documentation --- .../Models/SmsNotificationOrderRequestExt.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index ca335db3..c0b863f6 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -5,7 +5,7 @@ namespace Altinn.Notifications.Models; /// -/// Class representing an SMS notification order request +/// Class representing an SMS notification order request. /// /// /// External representation to be used in the API. @@ -13,21 +13,22 @@ namespace Altinn.Notifications.Models; public class SmsNotificationOrderRequestExt : NotificationOrderRequestBaseExt { /// - /// Gets or sets the sender number of the SMS + /// Gets or sets the sender number of the SMS. /// [JsonPropertyName("senderNumber")] public string? SenderNumber { get; set; } /// - /// Gets or sets the body of the SMS + /// Gets or sets the body of the SMS. /// [JsonPropertyName("body")] [Required] public string Body { get; set; } = string.Empty; /// - /// Json serialized the + /// Serializes the to JSON. /// + /// A JSON string representation of the object. public string Serialize() { return JsonSerializer.Serialize(this); From a553fcfd6bd2f49d3d56eea676ef6b72cf6e32e2 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Wed, 20 Nov 2024 14:15:54 +0100 Subject: [PATCH 04/75] Fill in the recipient's name based on whether or not placeholder keywords are used in the template. --- .../Extensions/StringExtensions.cs | 41 ++ .../Integrations/IRegisterClient.cs | 35 +- .../NotificationTemplate/EmailTemplate.cs | 56 +- .../INotificationTemplate.cs | 12 +- .../NotificationTemplate/SmsTemplate.cs | 36 +- .../Models/Parties/PartyDetails.cs | 33 ++ .../Models/Parties/PartyDetailsLookupBatch.cs | 15 + .../Parties/PartyDetailsLookupRequest.cs | 25 + .../Parties/PartyDetailsLookupResult.cs | 15 + .../Models/Parties/PersonNameComponents.cs | 22 + .../Models/Recipient.cs | 41 +- .../Models/RecipientNameComponents.cs | 29 + .../Services/ContactPointService.cs | 539 ++++++++++-------- .../Interfaces/IContactPointService.cs | 71 ++- .../Services/OrderRequestService.cs | 6 + .../Register/RegisterClient.cs | 95 ++- 16 files changed, 718 insertions(+), 353 deletions(-) create mode 100644 src/Altinn.Notifications.Core/Extensions/StringExtensions.cs create mode 100644 src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs create mode 100644 src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs create mode 100644 src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs create mode 100644 src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs create mode 100644 src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs create mode 100644 src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs diff --git a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs new file mode 100644 index 00000000..06798bfa --- /dev/null +++ b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs @@ -0,0 +1,41 @@ +using System.Text.RegularExpressions; + +namespace Altinn.Notifications.Core.Extensions; + +/// +/// Provides extension methods for the class. +/// +public static partial class StringExtensions +{ + /// + /// The regex pattern used to identify recipient name placeholders in a string. + /// + private static readonly Regex _recipientNamePlaceholdersRegex = RecipientNamePlaceholdersKeywordsRegex(); + + /// + /// Checks whether the specified string contains any recipient name placeholders. + /// + /// The string to check. + /// true if the string contains one or more recipient name placeholders; otherwise, false. + /// + /// The following recipient name placeholders are supported: + /// + /// $recipientFirstName$ - The first name of the recipient. + /// $recipientMiddleName$ - The middle name of the recipient. + /// $recipientLastName$ - The last name of the recipient. + /// $recipientName$ - The full name of the recipient or organization. + /// + /// + public static bool ContainsRecipientNamePlaceholders(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + return _recipientNamePlaceholdersRegex.IsMatch(value); + } + + [GeneratedRegex(@"\$recipient(FirstName|MiddleName|LastName|Name)\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex RecipientNamePlaceholdersKeywordsRegex(); +} diff --git a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs index 44135b33..40998dac 100644 --- a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs @@ -1,17 +1,30 @@ using Altinn.Notifications.Core.Models.ContactPoints; +using Altinn.Notifications.Core.Models.Parties; -namespace Altinn.Notifications.Core.Integrations +namespace Altinn.Notifications.Core.Integrations; + +/// +/// Defines a contract for interacting with the register service. +/// +public interface IRegisterClient { /// - /// Interface describing a client for the register service + /// Asynchronously retrieves contact point details for the specified organizations. + /// + /// A collection of organization numbers for which contact point details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the contact points of the specified organizations. + /// + Task> GetOrganizationContactPoints(List organizationNumbers); + + /// + /// Asynchronously retrieves the party details for the specified social security numbers. /// - public interface IRegisterClient - { - /// - /// Retrieves contact points for a list of organizations - /// - /// A list of organization numbers to look up contact points for - /// A list of for the provided organizations - public Task> GetOrganizationContactPoints(List organizationNumbers); - } + /// A collection of social security numbers for which party details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified parties. + /// + Task> GetPartyDetails(List socialSecurityNumbers); } diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs index dbee9a59..35c4aee5 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs @@ -1,45 +1,71 @@ -using Altinn.Notifications.Core.Enums; +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Extensions; namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an email notification +/// Template for an email notification. /// public class EmailTemplate : INotificationTemplate { - /// - public NotificationTemplateType Type { get; internal set; } + /// + /// Gets the body of the email. + /// + public string Body { get; internal set; } = string.Empty; + + /// + /// Gets the content type of the email. + /// + public EmailContentType ContentType { get; internal set; } /// - /// Gets the from adress of emails created by the template + /// Gets the sender address of the email. /// public string FromAddress { get; internal set; } = string.Empty; /// - /// Gets the subject of emails created by the template + /// Gets a value indicating whether the email body or subject contains any recipient name placeholders. /// - public string Subject { get; internal set; } = string.Empty; + /// + /// true if the email body or subject contains any recipient name placeholders; otherwise, false. + /// + [JsonIgnore] + public bool HasRecipientNamePlaceholders + { + get + { + return Subject.ContainsRecipientNamePlaceholders() || Body.ContainsRecipientNamePlaceholders(); + } + } /// - /// Gets the body of emails created by the template + /// Gets the subject of the email. /// - public string Body { get; internal set; } = string.Empty; + public string Subject { get; internal set; } = string.Empty; /// - /// Gets the content type of emails created by the template + /// Gets the type of the notification template. /// - public EmailContentType ContentType { get; internal set; } + /// + /// The type of the notification template, represented by the enum. + /// + public NotificationTemplateType Type { get; internal set; } = NotificationTemplateType.Email; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified from address, subject, body, and content type. /// + /// The sender address of the email. If null, an empty string is used. + /// The subject of the email. + /// The body of the email. + /// The content type of the email. public EmailTemplate(string? fromAddress, string subject, string body, EmailContentType contentType) { - FromAddress = fromAddress ?? string.Empty; - Subject = subject; Body = body; + Subject = subject; ContentType = contentType; - Type = NotificationTemplateType.Email; + FromAddress = fromAddress ?? string.Empty; } /// diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs index 5329fc08..ae0785f7 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs @@ -5,7 +5,7 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Base class for a notification template +/// Represents a base notification template. /// [JsonDerivedType(typeof(EmailTemplate), "email")] [JsonDerivedType(typeof(SmsTemplate), "sms")] @@ -13,7 +13,13 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; public interface INotificationTemplate { /// - /// Gets the type for the template + /// Indicates whether the notification contains any recipient name placeholders. /// - public NotificationTemplateType Type { get; } + [JsonIgnore] + bool HasRecipientNamePlaceholders { get; } + + /// + /// The type of the notification template. + /// + NotificationTemplateType Type { get; } } diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs index 9d1cb2db..5ce62a65 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs @@ -1,33 +1,51 @@ +using System.Text.Json.Serialization; + using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Extensions; namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an SMS notification +/// Template for an SMS notification. /// public class SmsTemplate : INotificationTemplate { - /// - public NotificationTemplateType Type { get; internal set; } + /// + /// Gets the body of the SMS. + /// + public string Body { get; internal set; } = string.Empty; /// - /// Gets the number from which the SMS is created by the template + /// Gets a value indicating whether the SMS body contains any recipient name placeholders. + /// + /// + /// true if the SMS body contains any recipient name placeholders; otherwise, false. + /// + [JsonIgnore] + public bool HasRecipientNamePlaceholders => Body.ContainsRecipientNamePlaceholders(); + + /// + /// Gets the number from which the SMS is sent. /// public string SenderNumber { get; internal set; } = string.Empty; /// - /// Gets the body of SMSs created by the template + /// Gets the type of the notification template. /// - public string Body { get; internal set; } = string.Empty; + /// + /// The type of the notification template, represented by the enum. + /// + public NotificationTemplateType Type { get; internal set; } = NotificationTemplateType.Sms; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified sender number and body. /// + /// The number from which the SMS is sent. If null, an empty string is used. + /// The body of the SMS. public SmsTemplate(string? senderNumber, string body) { - SenderNumber = senderNumber ?? string.Empty; Body = body; - Type = NotificationTemplateType.Sms; + SenderNumber = senderNumber ?? string.Empty; } /// diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs new file mode 100644 index 00000000..0957df70 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Parties; + +/// +/// Represents the details for a specific party. +/// +public class PartyDetails +{ + /// + /// Gets or sets the name of the party. + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// Gets or sets the organization number of the party, if applicable. + /// + [JsonPropertyName("orgNo")] + public string? OrganizationNumber { get; set; } + + /// + /// Gets or sets the components of the person's name, if available. + /// + [JsonPropertyName("personName")] + public PersonNameComponents? PersonName { get; set; } + + /// + /// Gets or sets the social security number of the party, if applicable. + /// + [JsonPropertyName("ssn")] + public string? NationalIdentityNumber { get; set; } +} diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs new file mode 100644 index 00000000..490eee27 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Parties; + +/// +/// Represents a request to look up party details by their identifiers. +/// +public class PartyDetailsLookupBatch +{ + /// + /// Gets or sets the list of lookup criteria for parties. + /// + [JsonPropertyName("parties")] + public List? PartyDetailsLookupRequestList { get; set; } +} diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs new file mode 100644 index 00000000..1fa3bf14 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Parties; + +/// +/// Represents a lookup criterion for a single party. +/// +public record PartyDetailsLookupRequest +{ + /// + /// Gets or sets the organization number of the party. + /// + /// The organization number of the party. + [JsonPropertyName("orgNo")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? OrganizationNumber { get; init; } + + /// + /// Gets or sets the social security number of the party. + /// + /// The social security number of the party. + [JsonPropertyName("ssn")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SocialSecurityNumber { get; init; } +} diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs new file mode 100644 index 00000000..3b99eff6 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Parties; + +/// +/// Represents the response for a party details lookup operation. +/// +public class PartyDetailsLookupResult +{ + /// + /// Gets or sets the list of party details. + /// + [JsonPropertyName("partyNames")] + public List? PartyDetailsList { get; set; } +} diff --git a/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs b/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs new file mode 100644 index 00000000..8365ff66 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs @@ -0,0 +1,22 @@ +namespace Altinn.Notifications.Core.Models; + +/// +/// Represents the components of a person's name. +/// +public record PersonNameComponents +{ + /// + /// Gets the first name. + /// + public string? FirstName { get; init; } + + /// + /// Gets the middle name. + /// + public string? MiddleName { get; init; } + + /// + /// Gets the sure name. + /// + public string? LastName { get; init; } +} diff --git a/src/Altinn.Notifications.Core/Models/Recipient.cs b/src/Altinn.Notifications.Core/Models/Recipient.cs index 1fc089dd..e503279e 100644 --- a/src/Altinn.Notifications.Core/Models/Recipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipient.cs @@ -5,50 +5,61 @@ namespace Altinn.Notifications.Core.Models; /// -/// Class representing a notification recipient +/// Class representing a notification recipient. /// public class Recipient { /// - /// Gets the recipient's organization number + /// Gets or sets the list of address points for the recipient. /// - public string? OrganizationNumber { get; set; } = null; + public List AddressInfo { get; set; } = new(); /// - /// Gets the recipient's national identity number + /// Gets or sets a value indicating whether the recipient is reserved from digital communication. /// - public string? NationalIdentityNumber { get; set; } = null; + public bool? IsReserved { get; set; } /// - /// Gets or sets a value indicating whether the recipient is reserved from digital communication + /// Gets or sets the recipient name components. /// - public bool? IsReserved { get; set; } + public RecipientNameComponents? NameComponents { get; set; } /// - /// Gets a list of address points for the recipient + /// Gets or sets the recipient's national identity number. /// - public List AddressInfo { get; set; } = new List(); + public string? NationalIdentityNumber { get; set; } + + /// + /// Gets or sets the recipient's organization number. + /// + public string? OrganizationNumber { get; set; } /// /// Initializes a new instance of the class. /// - public Recipient(List addressInfo, string? organizationNumber = null, string? nationalIdentityNumber = null) + public Recipient() { - OrganizationNumber = organizationNumber; - NationalIdentityNumber = nationalIdentityNumber; - AddressInfo = addressInfo; } /// /// Initializes a new instance of the class. /// - public Recipient() + /// The list of address points for the recipient. + /// The recipient's organization number. + /// The recipient's national identity number. + /// The recipient name components. + public Recipient(List addressInfo, string? organizationNumber = null, string? nationalIdentityNumber = null, RecipientNameComponents? nameComponents = null) { + AddressInfo = addressInfo; + NameComponents = nameComponents; + OrganizationNumber = organizationNumber; + NationalIdentityNumber = nationalIdentityNumber; } /// - /// Creates a deep copy of the recipient object + /// Creates a deep copy of the recipient object. /// + /// A deep copy of the recipient object. internal Recipient DeepCopy() { string json = JsonSerializer.Serialize(this); diff --git a/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs b/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs new file mode 100644 index 00000000..aba2e390 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models; + +/// +/// Represents the components of a recipient's name. +/// +public class RecipientNameComponents +{ + /// + /// Gets the first name. + /// + public string? FirstName { get; init; } + + /// + /// Gets the full name. + /// + public string? Name { get; init; } + + /// + /// Gets the last name (surname). + /// + public string? LastName { get; init; } + + /// + /// Gets the middle name. + /// + public string? MiddleName { get; init; } +} diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index ff506db5..5d8577fe 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -6,314 +6,367 @@ using Altinn.Notifications.Core.Models.ContactPoints; using Altinn.Notifications.Core.Services.Interfaces; -namespace Altinn.Notifications.Core.Services +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the using Altinn platform services to lookup contact points +/// +public class ContactPointService : IContactPointService { + private readonly IProfileClient _profileClient; + private readonly IRegisterClient _registerClient; + private readonly IAuthorizationService _authorizationService; + /// - /// Implementation of the using Altinn platform services to lookup contact points + /// Initializes a new instance of the class. /// - public class ContactPointService : IContactPointService + public ContactPointService(IProfileClient profile, IRegisterClient register, IAuthorizationService authorizationService) { - private readonly IProfileClient _profileClient; - private readonly IRegisterClient _registerClient; - private readonly IAuthorizationService _authorizationService; - - /// - /// Initializes a new instance of the class. - /// - public ContactPointService(IProfileClient profile, IRegisterClient register, IAuthorizationService authorizationService) - { - _profileClient = profile; - _registerClient = register; - _authorizationService = authorizationService; - } + _profileClient = profile; + _registerClient = register; + _authorizationService = authorizationService; + } - /// - public async Task AddEmailContactPoints(List recipients, string? resourceId) - { - await AugmentRecipients( - recipients, - resourceId, - (recipient, userContactPoints) => + /// + public async Task AddEmailContactPoints(List recipients, string? resourceId) + { + await AugmentRecipients( + recipients, + resourceId, + (recipient, userContactPoints) => + { + if (!string.IsNullOrEmpty(userContactPoints.Email)) { - if (!string.IsNullOrEmpty(userContactPoints.Email)) - { - recipient.AddressInfo.Add(new EmailAddressPoint(userContactPoints.Email)); - } + recipient.AddressInfo.Add(new EmailAddressPoint(userContactPoints.Email)); + } - return recipient; - }, - (recipient, orgContactPoints) => - { - recipient.AddressInfo.AddRange(orgContactPoints.EmailList - .Select(e => new EmailAddressPoint(e)) - .ToList()); - - recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints - .Where(u => !string.IsNullOrEmpty(u.Email)) - .Select(u => new EmailAddressPoint(u.Email)) - .ToList()); - return recipient; - }); - } + return recipient; + }, + (recipient, orgContactPoints) => + { + recipient.AddressInfo.AddRange(orgContactPoints.EmailList + .Select(e => new EmailAddressPoint(e)) + .ToList()); + + recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints + .Where(u => !string.IsNullOrEmpty(u.Email)) + .Select(u => new EmailAddressPoint(u.Email)) + .ToList()); + return recipient; + }); + } - /// - public async Task AddSmsContactPoints(List recipients, string? resourceId) - { - await AugmentRecipients( - recipients, - resourceId, - (recipient, userContactPoints) => + /// + public async Task AddSmsContactPoints(List recipients, string? resourceId) + { + await AugmentRecipients( + recipients, + resourceId, + (recipient, userContactPoints) => + { + if (!string.IsNullOrEmpty(userContactPoints.MobileNumber)) { - if (!string.IsNullOrEmpty(userContactPoints.MobileNumber)) - { - recipient.AddressInfo.Add(new SmsAddressPoint(userContactPoints.MobileNumber)); - } + recipient.AddressInfo.Add(new SmsAddressPoint(userContactPoints.MobileNumber)); + } - return recipient; - }, - (recipient, orgContactPoints) => + return recipient; + }, + (recipient, orgContactPoints) => + { + recipient.AddressInfo.AddRange(orgContactPoints.MobileNumberList + .Select(m => new SmsAddressPoint(m)) + .ToList()); + + recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints + .Where(u => !string.IsNullOrEmpty(u.MobileNumber)) + .Select(u => new SmsAddressPoint(u.MobileNumber)) + .ToList()); + return recipient; + }); + } + + /// + public async Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId) + { + await AugmentRecipients( + recipients, + resourceId, + (recipient, userContactPoints) => + { + if (channel == NotificationChannel.EmailPreferred) { - recipient.AddressInfo.AddRange(orgContactPoints.MobileNumberList - .Select(m => new SmsAddressPoint(m)) - .ToList()); - - recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints - .Where(u => !string.IsNullOrEmpty(u.MobileNumber)) - .Select(u => new SmsAddressPoint(u.MobileNumber)) - .ToList()); - return recipient; - }); - } + AddPreferredOrFallbackContactPoint( + recipient, + userContactPoints.Email, + userContactPoints.MobileNumber, + email => new EmailAddressPoint(email), + mobile => new SmsAddressPoint(mobile)); + } + else if (channel == NotificationChannel.SmsPreferred) + { + AddPreferredOrFallbackContactPoint( + recipient, + userContactPoints.MobileNumber, + userContactPoints.Email, + mobile => new SmsAddressPoint(mobile), + email => new EmailAddressPoint(email)); + } - /// - public async Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId) - { - await AugmentRecipients( - recipients, - resourceId, - (recipient, userContactPoints) => + return recipient; + }, + (recipient, orgContactPoints) => + { + if (channel == NotificationChannel.EmailPreferred) { - if (channel == NotificationChannel.EmailPreferred) + AddPreferredOrFallbackContactPointList( + recipient, + orgContactPoints.EmailList, + orgContactPoints.MobileNumberList, + e => new EmailAddressPoint(e), + m => new SmsAddressPoint(m)); + + foreach (var userContact in orgContactPoints.UserContactPoints) { AddPreferredOrFallbackContactPoint( recipient, - userContactPoints.Email, - userContactPoints.MobileNumber, + userContact.Email, + userContact.MobileNumber, email => new EmailAddressPoint(email), mobile => new SmsAddressPoint(mobile)); } - else if (channel == NotificationChannel.SmsPreferred) + } + else if (channel == NotificationChannel.SmsPreferred) + { + AddPreferredOrFallbackContactPointList( + recipient, + orgContactPoints.MobileNumberList, + orgContactPoints.EmailList, + m => new SmsAddressPoint(m), + e => new EmailAddressPoint(e)); + + foreach (var userContact in orgContactPoints.UserContactPoints) { AddPreferredOrFallbackContactPoint( - recipient, - userContactPoints.MobileNumber, - userContactPoints.Email, - mobile => new SmsAddressPoint(mobile), - email => new EmailAddressPoint(email)); + recipient, + userContact.MobileNumber, + userContact.Email, + mobile => new SmsAddressPoint(mobile), + email => new EmailAddressPoint(email)); } + } - return recipient; - }, - (recipient, orgContactPoints) => - { - if (channel == NotificationChannel.EmailPreferred) - { - AddPreferredOrFallbackContactPointList( - recipient, - orgContactPoints.EmailList, - orgContactPoints.MobileNumberList, - e => new EmailAddressPoint(e), - m => new SmsAddressPoint(m)); - - foreach (var userContact in orgContactPoints.UserContactPoints) - { - AddPreferredOrFallbackContactPoint( - recipient, - userContact.Email, - userContact.MobileNumber, - email => new EmailAddressPoint(email), - mobile => new SmsAddressPoint(mobile)); - } - } - else if (channel == NotificationChannel.SmsPreferred) - { - AddPreferredOrFallbackContactPointList( - recipient, - orgContactPoints.MobileNumberList, - orgContactPoints.EmailList, - m => new SmsAddressPoint(m), - e => new EmailAddressPoint(e)); - - foreach (var userContact in orgContactPoints.UserContactPoints) - { - AddPreferredOrFallbackContactPoint( - recipient, - userContact.MobileNumber, - userContact.Email, - mobile => new SmsAddressPoint(mobile), - email => new EmailAddressPoint(email)); - } - } + return recipient; + }); + } - return recipient; - }); + private static void AddPreferredOrFallbackContactPointList( + Recipient recipient, + List preferredList, + List fallbackList, + Func preferredSelector, + Func fallbackSelector) + { + if (preferredList.Count > 0) + { + recipient.AddressInfo.AddRange(preferredList.Select(preferredSelector).ToList()); } + else + { + recipient.AddressInfo.AddRange(fallbackList.Select(fallbackSelector).ToList()); + } + } - private static void AddPreferredOrFallbackContactPointList( + private static void AddPreferredOrFallbackContactPoint( Recipient recipient, - List preferredList, - List fallbackList, + TPreferred preferredContact, + TFallback fallbackContact, Func preferredSelector, Func fallbackSelector) + { + if (!string.IsNullOrEmpty(preferredContact?.ToString())) { - if (preferredList.Count > 0) - { - recipient.AddressInfo.AddRange(preferredList.Select(preferredSelector).ToList()); - } - else - { - recipient.AddressInfo.AddRange(fallbackList.Select(fallbackSelector).ToList()); - } + recipient.AddressInfo.Add(preferredSelector(preferredContact)); + } + else if (!string.IsNullOrEmpty(fallbackContact?.ToString())) + { + recipient.AddressInfo.Add(fallbackSelector(fallbackContact)); } + } + + private async Task> AugmentRecipients( + List recipients, + string? resourceId, + Func createUserContactPoint, + Func createOrgContactPoint) + { + List augmentedRecipients = []; - private static void AddPreferredOrFallbackContactPoint( - Recipient recipient, - TPreferred preferredContact, - TFallback fallbackContact, - Func preferredSelector, - Func fallbackSelector) + var userLookupTask = LookupPersonContactPoints(recipients); + var orgLookupTask = LookupOrganizationContactPoints(recipients, resourceId); + await Task.WhenAll(userLookupTask, orgLookupTask); + + List userContactPointsList = userLookupTask.Result; + List organizationContactPointList = orgLookupTask.Result; + + foreach (Recipient recipient in recipients) { - if (!string.IsNullOrEmpty(preferredContact?.ToString())) + if (!string.IsNullOrEmpty(recipient.NationalIdentityNumber)) { - recipient.AddressInfo.Add(preferredSelector(preferredContact)); + UserContactPoints? userContactPoints = userContactPointsList! + .Find(u => u.NationalIdentityNumber == recipient.NationalIdentityNumber); + + if (userContactPoints != null) + { + recipient.IsReserved = userContactPoints.IsReserved; + augmentedRecipients.Add(createUserContactPoint(recipient, userContactPoints)); + } } - else if (!string.IsNullOrEmpty(fallbackContact?.ToString())) + else if (!string.IsNullOrEmpty(recipient.OrganizationNumber)) { - recipient.AddressInfo.Add(fallbackSelector(fallbackContact)); + OrganizationContactPoints? organizationContactPoints = organizationContactPointList! + .Find(o => o.OrganizationNumber == recipient.OrganizationNumber); + + if (organizationContactPoints != null) + { + augmentedRecipients.Add(createOrgContactPoint(recipient, organizationContactPoints)); + } } } - private async Task> AugmentRecipients( - List recipients, - string? resourceId, - Func createUserContactPoint, - Func createOrgContactPoint) + return augmentedRecipients; + } + + private async Task> LookupPersonContactPoints(List recipients) + { + List nins = recipients + .Where(r => !string.IsNullOrEmpty(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .ToList(); + + if (nins.Count == 0) + { + return []; + } + + List contactPoints = await _profileClient.GetUserContactPoints(nins); + + contactPoints.ForEach(contactPoint => + { + contactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(contactPoint.MobileNumber); + }); + + return contactPoints; + } + + private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId) + { + List orgNos = recipients + .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); + + if (orgNos.Count == 0) { - List augmentedRecipients = []; + return []; + } + + Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); + List authorizedUserContactPoints = new(); - var userLookupTask = LookupPersonContactPoints(recipients); - var orgLookupTask = LookupOrganizationContactPoints(recipients, resourceId); - await Task.WhenAll(userLookupTask, orgLookupTask); + if (!string.IsNullOrEmpty(resourceId)) + { + var allUserContactPoints = await _profileClient.GetUserRegisteredContactPoints(orgNos, resourceId); + authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); + } - List userContactPointsList = userLookupTask.Result; - List organizationContactPointList = orgLookupTask.Result; + List contactPoints = await registerTask; - foreach (Recipient recipient in recipients) + if (!string.IsNullOrEmpty(resourceId)) + { + foreach (var userContactPoint in authorizedUserContactPoints) { - if (!string.IsNullOrEmpty(recipient.NationalIdentityNumber)) + userContactPoint.UserContactPoints.ForEach(userContactPoint => { - UserContactPoints? userContactPoints = userContactPointsList! - .Find(u => u.NationalIdentityNumber == recipient.NationalIdentityNumber); + userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); + }); - if (userContactPoints != null) - { - recipient.IsReserved = userContactPoints.IsReserved; - augmentedRecipients.Add(createUserContactPoint(recipient, userContactPoints)); - } + var existingContactPoint = contactPoints.Find(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); + + if (existingContactPoint != null) + { + existingContactPoint.UserContactPoints.AddRange(userContactPoint.UserContactPoints); } - else if (!string.IsNullOrEmpty(recipient.OrganizationNumber)) + else { - OrganizationContactPoints? organizationContactPoints = organizationContactPointList! - .Find(o => o.OrganizationNumber == recipient.OrganizationNumber); - - if (organizationContactPoints != null) - { - augmentedRecipients.Add(createOrgContactPoint(recipient, organizationContactPoints)); - } + contactPoints.Add(userContactPoint); } } - - return augmentedRecipients; } - private async Task> LookupPersonContactPoints(List recipients) + contactPoints.ForEach(contactPoint => { - List nins = recipients - .Where(r => !string.IsNullOrEmpty(r.NationalIdentityNumber)) - .Select(r => r.NationalIdentityNumber!) - .ToList(); + contactPoint.MobileNumberList = contactPoint.MobileNumberList + .Select(mobileNumber => + { + return MobileNumberHelper.EnsureCountryCodeIfValidNumber(mobileNumber); + }) + .ToList(); + }); - if (nins.Count == 0) - { - return []; - } + return contactPoints; + } - List contactPoints = await _profileClient.GetUserContactPoints(nins); + /// + /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. + /// + /// + /// A list of objects to which the name components will be added. + /// Recipients must have their populated. + /// + /// + /// A representing the asynchronous operation to add name components to the recipients. + /// + public async Task AddRecipientNameComponents(List recipients) + { + if (recipients == null || recipients.Count == 0) + { + return; + } - contactPoints.ForEach(contactPoint => - { - contactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(contactPoint.MobileNumber); - }); + var nationalIdentityNumbers = recipients + .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .Distinct() + .ToList(); - return contactPoints; + if (nationalIdentityNumbers.Count == 0) + { + return; } - private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId) + var partyDetails = await _registerClient.GetPartyDetails(nationalIdentityNumbers); + if (partyDetails == null || partyDetails.Count == 0) { - List orgNos = recipients - .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) - .Select(r => r.OrganizationNumber!) - .ToList(); - - if (orgNos.Count == 0) - { - return []; - } - - Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); - List authorizedUserContactPoints = new(); - - if (!string.IsNullOrEmpty(resourceId)) - { - var allUserContactPoints = await _profileClient.GetUserRegisteredContactPoints(orgNos, resourceId); - authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); - } + return; + } - List contactPoints = await registerTask; + var partyLookup = partyDetails + .Where(static p => !string.IsNullOrWhiteSpace(p.NationalIdentityNumber)) + .ToDictionary(p => p.NationalIdentityNumber!, p => p); - if (!string.IsNullOrEmpty(resourceId)) + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber != null && partyLookup.TryGetValue(recipient.NationalIdentityNumber, out var party)) { - foreach (var userContactPoint in authorizedUserContactPoints) + recipient.NameComponents = new RecipientNameComponents { - userContactPoint.UserContactPoints.ForEach(userContactPoint => - { - userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); - }); - - var existingContactPoint = contactPoints.Find(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); - - if (existingContactPoint != null) - { - existingContactPoint.UserContactPoints.AddRange(userContactPoint.UserContactPoints); - } - else - { - contactPoints.Add(userContactPoint); - } - } + Name = party.Name, + LastName = party.PersonName?.LastName, + FirstName = party.PersonName?.FirstName, + MiddleName = party.PersonName?.MiddleName, + }; } - - contactPoints.ForEach(contactPoint => - { - contactPoint.MobileNumberList = contactPoint.MobileNumberList - .Select(mobileNumber => - { - return MobileNumberHelper.EnsureCountryCodeIfValidNumber(mobileNumber); - }) - .ToList(); - }); - - return contactPoints; } } + } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs index 8a0e1eb3..b9b8033f 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs @@ -1,39 +1,50 @@ using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Parties; -namespace Altinn.Notifications.Core.Services.Interfaces +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Service for retrieving contact points for recipients. +/// +public interface IContactPointService { /// - /// Service for retrieving contact points for recipients + /// Looks up and adds the email contact points for recipients based on their national identity number or organization number. + /// + /// List of recipients to retrieve contact points for. + /// The resource to find contact points in relation to. + /// A task representing the asynchronous operation. + /// Implementation alters the recipient reference object directly. + public Task AddEmailContactPoints(List recipients, string? resourceId); + + /// + /// Looks up and adds the SMS contact points for recipients based on their national identity number or organization number. /// - public interface IContactPointService - { - /// - /// Looks up and adds the email contact points for recipients based on their national identity number or organization number - /// - /// List of recipients to retrieve contact points for - /// The resource to find contact points in relation to - /// The list of recipients augumented with email address points where available - /// Implementation alters the recipient reference object directly - public Task AddEmailContactPoints(List recipients, string? resourceId); + /// List of recipients to retrieve contact points for. + /// The resource to find contact points in relation to. + /// A task representing the asynchronous operation. + /// Implementation alters the recipient reference object directly. + public Task AddSmsContactPoints(List recipients, string? resourceId); - /// - /// Looks up and adds the SMS contact points for recipients based on their national identity number or organization number - /// - /// List of recipients to retrieve contact points for - /// The resource to find contact points in relation to - /// The list of recipients augumented with SMS address points where available - /// Implementation alters the recipient reference object directly - public Task AddSmsContactPoints(List recipients, string? resourceId); + /// + /// Looks up and adds the preferred contact points for recipients based on their national identity number or organization number. + /// + /// The notification channel specifying which channel is preferred. + /// List of recipients to retrieve contact points for. + /// The resource to find contact points in relation to. + /// A task representing the asynchronous operation. + /// Implementation alters the recipient reference object directly. + public Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId); - /// - /// Looks up and adds the SMS contact points for recipients based on their national identity number or organization number - /// - /// The notification channel specifying which channel is preferred - /// List of recipients to retrieve contact points for - /// The resource to find contact points in relation to - /// The list of recipients augumented with SMS address points where available - /// Implementation alters the recipient reference object directly - public Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId); - } + /// + /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. + /// + /// + /// A list of objects to which the name components will be added. + /// + /// + /// A representing the asynchronous operation to add name components to the recipients. + /// + public Task AddRecipientNameComponents(List recipients); } diff --git a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs index e45cd877..24be4333 100644 --- a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs @@ -51,6 +51,12 @@ public async Task RegisterNotificationOrder(No var templates = SetSenderIfNotDefined(orderRequest.Templates); + var containsRecipientNamePlaceholders = orderRequest.Templates.Exists(e => e.HasRecipientNamePlaceholders); + if (containsRecipientNamePlaceholders) + { + await _contactPointService.AddRecipientNameComponents(orderRequest.Recipients); + } + var order = new NotificationOrder( orderId, orderRequest.SendersReference, diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index 56dcd777..fca0208f 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -1,52 +1,93 @@ using System.Text; using System.Text.Json; -using Altinn.Notifications.Core; using Altinn.Notifications.Core.Integrations; using Altinn.Notifications.Core.Models.ContactPoints; +using Altinn.Notifications.Core.Models.Parties; using Altinn.Notifications.Core.Shared; using Altinn.Notifications.Integrations.Configuration; using Microsoft.Extensions.Options; -namespace Altinn.Notifications.Integrations.Register +namespace Altinn.Notifications.Integrations.Register; + +/// +/// Implementation of the to retrieve information for organizations and individuals. +/// +public class RegisterClient : IRegisterClient { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly string _nameComponentsLookupEndpoint = "parties/nameslookup"; + private readonly string _contactPointLookupEndpoint = "organizations/contactpoint/lookup"; + /// - /// Implementation of the + /// Initializes a new instance of the class. /// - public class RegisterClient : IRegisterClient + /// The HTTP client used to make requests to the register service. + /// The platform settings containing the API endpoints. + public RegisterClient(HttpClient client, IOptions settings) { - private readonly HttpClient _client; + _client = client; + _client.BaseAddress = new Uri(settings.Value.ApiRegisterEndpoint); + _jsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + } - /// - /// Initializes a new instance of the class. - /// - public RegisterClient(HttpClient client, IOptions settings) + /// + /// Asynchronously retrieves contact point details for the specified organizations. + /// + /// A collection of organization numbers for which contact point details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the contact points of the specified organizations. + /// + public async Task> GetOrganizationContactPoints(List organizationNumbers) + { + var lookupObject = new OrgContactPointLookup { - _client = client; - _client.BaseAddress = new Uri(settings.Value.ApiRegisterEndpoint); - } + OrganizationNumbers = organizationNumbers + }; + + HttpContent content = new StringContent(JsonSerializer.Serialize(lookupObject, _jsonSerializerOptions), Encoding.UTF8, "application/json"); + + var response = await _client.PostAsync(_contactPointLookupEndpoint, content); - /// - public async Task> GetOrganizationContactPoints(List organizationNumbers) + if (!response.IsSuccessStatusCode) { - var lookupObject = new OrgContactPointLookup - { - OrganizationNumbers = organizationNumbers - }; + throw await PlatformHttpException.CreateAsync(response); + } - HttpContent content = new StringContent(JsonSerializer.Serialize(lookupObject, JsonSerializerOptionsProvider.Options), Encoding.UTF8, "application/json"); + string responseContent = await response.Content.ReadAsStringAsync(); + var contactPoints = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions)?.ContactPointsList; + return contactPoints ?? []; + } - var response = await _client.PostAsync("organizations/contactpoint/lookup", content); + /// + /// Asynchronously retrieves the party details for the specified social security numbers. + /// + /// A collection of social security numbers for which party details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified parties. + /// + public async Task> GetPartyDetails(List socialSecurityNumbers) + { + var partyDetailsLookupBatch = new PartyDetailsLookupBatch + { + PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest { SocialSecurityNumber = ssn }).ToList() + }; + + HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); - if (!response.IsSuccessStatusCode) - { - throw await PlatformHttpException.CreateAsync(response); - } + var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}?partyComponentOption=person-name", content); - string responseContent = await response.Content.ReadAsStringAsync(); - List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; - return contactPoints!; + if (!response.IsSuccessStatusCode) + { + throw await PlatformHttpException.CreateAsync(response); } + + string responseContent = await response.Content.ReadAsStringAsync(); + var partyNamesLookupResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + return partyNamesLookupResponse?.PartyDetailsList ?? []; } } From 9b5cd953b4dbd2214cb0f81fbbda766528c10674 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Wed, 20 Nov 2024 15:38:52 +0100 Subject: [PATCH 05/75] Implemented a new function to retrieve party details for organizations. --- .../Integrations/IRegisterClient.cs | 16 +- .../Services/ContactPointService.cs | 184 ++++++++++++------ .../Register/RegisterClient.cs | 45 ++++- 3 files changed, 182 insertions(+), 63 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs index 40998dac..085a3755 100644 --- a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs @@ -19,12 +19,22 @@ public interface IRegisterClient Task> GetOrganizationContactPoints(List organizationNumbers); /// - /// Asynchronously retrieves the party details for the specified social security numbers. + /// Asynchronously retrieves detailed information about parties based on their social security numbers. /// /// A collection of social security numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified parties. + /// The task result contains a list of representing the details of the specified individuals. /// - Task> GetPartyDetails(List socialSecurityNumbers); + Task> GetPartyDetailsForPersons(List socialSecurityNumbers); + + /// + /// Asynchronously retrieves detailed information about parties based on their organization numbers. + /// + /// A collection of organization numbers for which party details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified organizations. + /// + Task> GetPartyDetailsForOrganizations(List organizationNumbers); } diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 5d8577fe..bfd9ad47 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -53,7 +53,7 @@ await AugmentRecipients( .Select(u => new EmailAddressPoint(u.Email)) .ToList()); return recipient; - }); + }).ConfigureAwait(false); } /// @@ -82,7 +82,131 @@ await AugmentRecipients( .Select(u => new SmsAddressPoint(u.MobileNumber)) .ToList()); return recipient; - }); + }).ConfigureAwait(false); + } + + /// + /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. + /// + /// + /// A list of objects to which the name components will be added. + /// Recipients must have their populated. + /// + /// + /// A representing the asynchronous operation to add name components to the recipients. + /// + public async Task AddRecipientNameComponents(List recipients) + { + if (recipients == null || recipients.Count == 0) + { + return; + } + + await AddRecipientNameComponentsForPersons(recipients).ConfigureAwait(false); + + await AddRecipientNameComponentsForOrganizations(recipients).ConfigureAwait(false); + } + + /// + /// Adds name components to the recipients based on their national identity numbers. + /// + /// The list of recipients to augment with name components. + private async Task AddRecipientNameComponentsForPersons(List recipients) + { + if (recipients == null || recipients.Count == 0) + { + return; + } + + var nationalIdentityNumbers = recipients + .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .ToList(); + + if (nationalIdentityNumbers.Count == 0) + { + return; + } + + var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers).ConfigureAwait(false); + if (partyDetails == null || partyDetails.Count == 0) + { + return; + } + + var partyLookup = partyDetails + .Where(static p => !string.IsNullOrWhiteSpace(p.NationalIdentityNumber)) + .ToDictionary(p => p.NationalIdentityNumber!, p => p); + + foreach (var recipient in recipients) + { + if (string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber)) + { + continue; + } + + if (partyLookup.TryGetValue(recipient.NationalIdentityNumber, out var party)) + { + recipient.NameComponents = new RecipientNameComponents + { + Name = party.Name, + LastName = party.PersonName?.LastName, + FirstName = party.PersonName?.FirstName, + MiddleName = party.PersonName?.MiddleName, + }; + } + } + } + + /// + /// Adds name components to the recipients based on their organization numbers. + /// + /// The list of recipients to augment with name components. + private async Task AddRecipientNameComponentsForOrganizations(List recipients) + { + if (recipients == null || recipients.Count == 0) + { + return; + } + + var organizationNumbers = recipients + .Where(r => !string.IsNullOrWhiteSpace(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); + + if (organizationNumbers.Count == 0) + { + return; + } + + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers).ConfigureAwait(false); + if (partyDetails == null || partyDetails.Count == 0) + { + return; + } + + var partyLookup = partyDetails + .Where(static p => !string.IsNullOrWhiteSpace(p.OrganizationNumber)) + .ToDictionary(p => p.OrganizationNumber!, p => p); + + foreach (var recipient in recipients) + { + if (string.IsNullOrWhiteSpace(recipient.OrganizationNumber)) + { + continue; + } + + if (partyLookup.TryGetValue(recipient.OrganizationNumber, out var party)) + { + recipient.NameComponents = new RecipientNameComponents + { + LastName = null, + FirstName = null, + MiddleName = null, + Name = party.Name, + }; + } + } } /// @@ -270,8 +394,8 @@ private async Task> LookupOrganizationContactPoi return []; } + List authorizedUserContactPoints = []; Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); - List authorizedUserContactPoints = new(); if (!string.IsNullOrEmpty(resourceId)) { @@ -315,58 +439,4 @@ private async Task> LookupOrganizationContactPoi return contactPoints; } - - /// - /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. - /// - /// - /// A list of objects to which the name components will be added. - /// Recipients must have their populated. - /// - /// - /// A representing the asynchronous operation to add name components to the recipients. - /// - public async Task AddRecipientNameComponents(List recipients) - { - if (recipients == null || recipients.Count == 0) - { - return; - } - - var nationalIdentityNumbers = recipients - .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) - .Select(r => r.NationalIdentityNumber!) - .Distinct() - .ToList(); - - if (nationalIdentityNumbers.Count == 0) - { - return; - } - - var partyDetails = await _registerClient.GetPartyDetails(nationalIdentityNumbers); - if (partyDetails == null || partyDetails.Count == 0) - { - return; - } - - var partyLookup = partyDetails - .Where(static p => !string.IsNullOrWhiteSpace(p.NationalIdentityNumber)) - .ToDictionary(p => p.NationalIdentityNumber!, p => p); - - foreach (var recipient in recipients) - { - if (recipient.NationalIdentityNumber != null && partyLookup.TryGetValue(recipient.NationalIdentityNumber, out var party)) - { - recipient.NameComponents = new RecipientNameComponents - { - Name = party.Name, - LastName = party.PersonName?.LastName, - FirstName = party.PersonName?.FirstName, - MiddleName = party.PersonName?.MiddleName, - }; - } - } - } - } diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index fca0208f..efe3f99d 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -63,15 +63,20 @@ public async Task> GetOrganizationContactPoints( } /// - /// Asynchronously retrieves the party details for the specified social security numbers. + /// Asynchronously retrieves detailed information about parties based on their social security numbers. /// /// A collection of social security numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified parties. + /// The task result contains a list of representing the details of the specified individuals. /// - public async Task> GetPartyDetails(List socialSecurityNumbers) + public async Task> GetPartyDetailsForPersons(List socialSecurityNumbers) { + if (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0) + { + return []; + } + var partyDetailsLookupBatch = new PartyDetailsLookupBatch { PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest { SocialSecurityNumber = ssn }).ToList() @@ -90,4 +95,38 @@ public async Task> GetPartyDetails(List socialSecurit var partyNamesLookupResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); return partyNamesLookupResponse?.PartyDetailsList ?? []; } + + /// + /// Asynchronously retrieves detailed information about parties based on their organization numbers. + /// + /// A collection of organization numbers for which party details are requested. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified organizations. + /// + public async Task> GetPartyDetailsForOrganizations(List organizationNumbers) + { + if (organizationNumbers == null || organizationNumbers.Count == 0) + { + return []; + } + + var partyDetailsLookupBatch = new PartyDetailsLookupBatch + { + PartyDetailsLookupRequestList = organizationNumbers.Select(orgNumber => new PartyDetailsLookupRequest { OrganizationNumber = orgNumber }).ToList() + }; + + HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); + + var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}", content); + + if (!response.IsSuccessStatusCode) + { + throw await PlatformHttpException.CreateAsync(response); + } + + string responseContent = await response.Content.ReadAsStringAsync(); + var partyNamesLookupResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + return partyNamesLookupResponse?.PartyDetailsList ?? []; + } } From 625a92fa5333beae1d567104160cf7356536a89d Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 11:09:29 +0100 Subject: [PATCH 06/75] Extend the lookup logic to check for the recipientNumber keyword --- .../Extensions/StringExtensions.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs index 06798bfa..76e1990c 100644 --- a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs @@ -10,7 +10,12 @@ public static partial class StringExtensions /// /// The regex pattern used to identify recipient name placeholders in a string. /// - private static readonly Regex _recipientNamePlaceholdersRegex = RecipientNamePlaceholdersKeywordsRegex(); + private static readonly Regex _recipientNamePlaceholdersKeywordsRegex = RecipientNamePlaceholdersKeywordsRegexPattern(); + + /// + /// The regex pattern used to identify recipient number placeholders in a string. + /// + private static readonly Regex _recipientNumberPlaceholdersKeywordsRegex = RecipientNumberPlaceholdersKeywordsRegexPattern(); /// /// Checks whether the specified string contains any recipient name placeholders. @@ -33,9 +38,33 @@ public static bool ContainsRecipientNamePlaceholders(this string value) return false; } - return _recipientNamePlaceholdersRegex.IsMatch(value); + return _recipientNamePlaceholdersKeywordsRegex.IsMatch(value); + } + + /// + /// Checks whether the specified string contains any recipient number placeholders. + /// + /// The string to check. + /// true if the string contains one or more recipient number placeholders; otherwise, false. + /// + /// The following recipient number placeholders are supported: + /// + /// recipientNumber - The organization number when recipient is an organization. + /// + /// + public static bool ContainsRecipientNumberPlaceholders(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + return _recipientNumberPlaceholdersKeywordsRegex.IsMatch(value); } [GeneratedRegex(@"\$recipient(FirstName|MiddleName|LastName|Name)\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex RecipientNamePlaceholdersKeywordsRegex(); + private static partial Regex RecipientNamePlaceholdersKeywordsRegexPattern(); + + [GeneratedRegex(@"\$recipient(Number)\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex RecipientNumberPlaceholdersKeywordsRegexPattern(); } From 4f172caf18e341c8e5606defbb45d05199f474d8 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 11:18:24 +0100 Subject: [PATCH 07/75] Improve the members documentation --- .../Integrations/IRegisterClient.cs | 16 ++++---- .../Register/RegisterClient.cs | 41 +++++++++++-------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs index 085a3755..677a7db5 100644 --- a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs @@ -19,22 +19,22 @@ public interface IRegisterClient Task> GetOrganizationContactPoints(List organizationNumbers); /// - /// Asynchronously retrieves detailed information about parties based on their social security numbers. + /// Asynchronously retrieves party details for the specified organizations. /// - /// A collection of social security numbers for which party details are requested. + /// A collection of organization numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified individuals. + /// The task result contains a list of representing the details of the specified organizations. /// - Task> GetPartyDetailsForPersons(List socialSecurityNumbers); + Task> GetPartyDetailsForOrganizations(List organizationNumbers); /// - /// Asynchronously retrieves detailed information about parties based on their organization numbers. + /// Asynchronously retrieves party details for the specified persons. /// - /// A collection of organization numbers for which party details are requested. + /// A collection of social security numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified organizations. + /// The task result contains a list of representing the details of the specified individuals. /// - Task> GetPartyDetailsForOrganizations(List organizationNumbers); + Task> GetPartyDetailsForPersons(List socialSecurityNumbers); } diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index efe3f99d..7109d7f3 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -38,11 +38,16 @@ public RegisterClient(HttpClient client, IOptions settings) /// /// A collection of organization numbers for which contact point details are requested. /// - /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the contact points of the specified organizations. + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the contact points of the specified organizations. /// public async Task> GetOrganizationContactPoints(List organizationNumbers) { + if (organizationNumbers == null || organizationNumbers.Count == 0) + { + return []; + } + var lookupObject = new OrgContactPointLookup { OrganizationNumbers = organizationNumbers @@ -63,28 +68,28 @@ public async Task> GetOrganizationContactPoints( } /// - /// Asynchronously retrieves detailed information about parties based on their social security numbers. + /// Asynchronously retrieves party details for the specified organizations. /// - /// A collection of social security numbers for which party details are requested. + /// A collection of organization numbers for which party details are requested. /// - /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified individuals. + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified organizations. /// - public async Task> GetPartyDetailsForPersons(List socialSecurityNumbers) + public async Task> GetPartyDetailsForOrganizations(List organizationNumbers) { - if (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0) + if (organizationNumbers == null || organizationNumbers.Count == 0) { return []; } var partyDetailsLookupBatch = new PartyDetailsLookupBatch { - PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest { SocialSecurityNumber = ssn }).ToList() + PartyDetailsLookupRequestList = organizationNumbers.Select(orgNumber => new PartyDetailsLookupRequest { OrganizationNumber = orgNumber }).ToList() }; HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); - var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}?partyComponentOption=person-name", content); + var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}", content); if (!response.IsSuccessStatusCode) { @@ -97,28 +102,28 @@ public async Task> GetPartyDetailsForPersons(List soc } /// - /// Asynchronously retrieves detailed information about parties based on their organization numbers. + /// Asynchronously retrieves party details for the specified persons. /// - /// A collection of organization numbers for which party details are requested. + /// A collection of social security numbers for which party details are requested. /// - /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified organizations. + /// A task that represents the asynchronous operation. + /// The task result contains a list of representing the details of the specified individuals. /// - public async Task> GetPartyDetailsForOrganizations(List organizationNumbers) + public async Task> GetPartyDetailsForPersons(List socialSecurityNumbers) { - if (organizationNumbers == null || organizationNumbers.Count == 0) + if (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0) { return []; } var partyDetailsLookupBatch = new PartyDetailsLookupBatch { - PartyDetailsLookupRequestList = organizationNumbers.Select(orgNumber => new PartyDetailsLookupRequest { OrganizationNumber = orgNumber }).ToList() + PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest { SocialSecurityNumber = ssn }).ToList() }; HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); - var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}", content); + var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}?partyComponentOption=person-name", content); if (!response.IsSuccessStatusCode) { From cb5cf44b4983c4871904ef7e38bceee2e2177288 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 12:11:29 +0100 Subject: [PATCH 08/75] Add a new property to whether the template contains any recipient number placeholders --- .../NotificationTemplate/EmailTemplate.cs | 17 ++++++++++------- .../INotificationTemplate.cs | 10 ++++++++-- .../Models/NotificationTemplate/SmsTemplate.cs | 9 +++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs index 35c4aee5..42355df3 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs @@ -32,13 +32,16 @@ public class EmailTemplate : INotificationTemplate /// true if the email body or subject contains any recipient name placeholders; otherwise, false. /// [JsonIgnore] - public bool HasRecipientNamePlaceholders - { - get - { - return Subject.ContainsRecipientNamePlaceholders() || Body.ContainsRecipientNamePlaceholders(); - } - } + public bool HasRecipientNamePlaceholders => Subject.ContainsRecipientNamePlaceholders() || Body.ContainsRecipientNamePlaceholders(); + + /// + /// Gets a value indicating whether the email body contains any recipient number placeholders. + /// + /// + /// true if the email body contains any recipient number placeholders; otherwise, false. + /// + [JsonIgnore] + public bool HasRecipientNumberPlaceholders => Body.ContainsRecipientNumberPlaceholders(); /// /// Gets the subject of the email. diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs index ae0785f7..9a7e1b96 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs @@ -13,13 +13,19 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; public interface INotificationTemplate { /// - /// Indicates whether the notification contains any recipient name placeholders. + /// Gets a value indicating whether the email body or subject contains any recipient name placeholders. /// [JsonIgnore] bool HasRecipientNamePlaceholders { get; } /// - /// The type of the notification template. + /// Gets a value indicating whether the email body contains any recipient number placeholders. + /// + [JsonIgnore] + bool HasRecipientNumberPlaceholders { get; } + + /// + /// Gets the type of the notification template. /// NotificationTemplateType Type { get; } } diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs index 5ce62a65..6fa05b0f 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs @@ -24,6 +24,15 @@ public class SmsTemplate : INotificationTemplate [JsonIgnore] public bool HasRecipientNamePlaceholders => Body.ContainsRecipientNamePlaceholders(); + /// + /// Gets a value indicating whether the SMS body contains any recipient number placeholders. + /// + /// + /// true if the SMS body contains any recipient number placeholders; otherwise, false. + /// + [JsonIgnore] + public bool HasRecipientNumberPlaceholders => false; + /// /// Gets the number from which the SMS is sent. /// From 5919ddbddd921a7125a154e91f1026c794d00717 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 12:38:37 +0100 Subject: [PATCH 09/75] Improve type documentation --- .../Models/RecipientNameComponents.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs b/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs index aba2e390..cad00f96 100644 --- a/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs +++ b/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs @@ -8,22 +8,22 @@ namespace Altinn.Notifications.Core.Models; public class RecipientNameComponents { /// - /// Gets the first name. + /// Gets or sets the first name. /// public string? FirstName { get; init; } /// - /// Gets the full name. + /// Gets or sets the full name. /// public string? Name { get; init; } /// - /// Gets the last name (surname). + /// Gets or sets the last name (surname). /// public string? LastName { get; init; } /// - /// Gets the middle name. + /// Gets or sets the middle name. /// public string? MiddleName { get; init; } } From f01b2c8fcf9c53156a266c67d2fd97ca75c6d8b6 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 12:56:15 +0100 Subject: [PATCH 10/75] Eliminate the functions responsible for retrieving the individual name components. --- .../Services/ContactPointService.cs | 124 ------------------ .../Interfaces/IContactPointService.cs | 12 -- .../Services/OrderRequestService.cs | 6 - 3 files changed, 142 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index bfd9ad47..5b17fe12 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -85,130 +85,6 @@ await AugmentRecipients( }).ConfigureAwait(false); } - /// - /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. - /// - /// - /// A list of objects to which the name components will be added. - /// Recipients must have their populated. - /// - /// - /// A representing the asynchronous operation to add name components to the recipients. - /// - public async Task AddRecipientNameComponents(List recipients) - { - if (recipients == null || recipients.Count == 0) - { - return; - } - - await AddRecipientNameComponentsForPersons(recipients).ConfigureAwait(false); - - await AddRecipientNameComponentsForOrganizations(recipients).ConfigureAwait(false); - } - - /// - /// Adds name components to the recipients based on their national identity numbers. - /// - /// The list of recipients to augment with name components. - private async Task AddRecipientNameComponentsForPersons(List recipients) - { - if (recipients == null || recipients.Count == 0) - { - return; - } - - var nationalIdentityNumbers = recipients - .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) - .Select(r => r.NationalIdentityNumber!) - .ToList(); - - if (nationalIdentityNumbers.Count == 0) - { - return; - } - - var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers).ConfigureAwait(false); - if (partyDetails == null || partyDetails.Count == 0) - { - return; - } - - var partyLookup = partyDetails - .Where(static p => !string.IsNullOrWhiteSpace(p.NationalIdentityNumber)) - .ToDictionary(p => p.NationalIdentityNumber!, p => p); - - foreach (var recipient in recipients) - { - if (string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber)) - { - continue; - } - - if (partyLookup.TryGetValue(recipient.NationalIdentityNumber, out var party)) - { - recipient.NameComponents = new RecipientNameComponents - { - Name = party.Name, - LastName = party.PersonName?.LastName, - FirstName = party.PersonName?.FirstName, - MiddleName = party.PersonName?.MiddleName, - }; - } - } - } - - /// - /// Adds name components to the recipients based on their organization numbers. - /// - /// The list of recipients to augment with name components. - private async Task AddRecipientNameComponentsForOrganizations(List recipients) - { - if (recipients == null || recipients.Count == 0) - { - return; - } - - var organizationNumbers = recipients - .Where(r => !string.IsNullOrWhiteSpace(r.OrganizationNumber)) - .Select(r => r.OrganizationNumber!) - .ToList(); - - if (organizationNumbers.Count == 0) - { - return; - } - - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers).ConfigureAwait(false); - if (partyDetails == null || partyDetails.Count == 0) - { - return; - } - - var partyLookup = partyDetails - .Where(static p => !string.IsNullOrWhiteSpace(p.OrganizationNumber)) - .ToDictionary(p => p.OrganizationNumber!, p => p); - - foreach (var recipient in recipients) - { - if (string.IsNullOrWhiteSpace(recipient.OrganizationNumber)) - { - continue; - } - - if (partyLookup.TryGetValue(recipient.OrganizationNumber, out var party)) - { - recipient.NameComponents = new RecipientNameComponents - { - LastName = null, - FirstName = null, - MiddleName = null, - Name = party.Name, - }; - } - } - } - /// public async Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId) { diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs index b9b8033f..2796b36a 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs @@ -1,6 +1,5 @@ using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models; -using Altinn.Notifications.Core.Models.Parties; namespace Altinn.Notifications.Core.Services.Interfaces; @@ -36,15 +35,4 @@ public interface IContactPointService /// A task representing the asynchronous operation. /// Implementation alters the recipient reference object directly. public Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId); - - /// - /// Adds the name components (e.g., name, first name, middle name, last name) to the specified recipients. - /// - /// - /// A list of objects to which the name components will be added. - /// - /// - /// A representing the asynchronous operation to add name components to the recipients. - /// - public Task AddRecipientNameComponents(List recipients); } diff --git a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs index 24be4333..e45cd877 100644 --- a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs @@ -51,12 +51,6 @@ public async Task RegisterNotificationOrder(No var templates = SetSenderIfNotDefined(orderRequest.Templates); - var containsRecipientNamePlaceholders = orderRequest.Templates.Exists(e => e.HasRecipientNamePlaceholders); - if (containsRecipientNamePlaceholders) - { - await _contactPointService.AddRecipientNameComponents(orderRequest.Recipients); - } - var order = new NotificationOrder( orderId, orderRequest.SendersReference, From 93e511e15c0b0aa90b915f48968a310e02c7f596 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 21 Nov 2024 15:52:27 +0100 Subject: [PATCH 11/75] Allow the user to send in the components of recipient name when creating an order of any type --- .../Mappers/OrderMapper.cs | 10 +++++++- .../Models/RecipientExt.cs | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Altinn.Notifications/Mappers/OrderMapper.cs b/src/Altinn.Notifications/Mappers/OrderMapper.cs index 0bb80cb3..6f6ff9dc 100644 --- a/src/Altinn.Notifications/Mappers/OrderMapper.cs +++ b/src/Altinn.Notifications/Mappers/OrderMapper.cs @@ -24,6 +24,14 @@ public static NotificationOrderRequest MapToOrderRequest(this NotificationOrderR { List addresses = []; + RecipientNameComponents nameComponents = new() + { + Name = r.Name, + LastName = r.LastName, + FirstName = r.FirstName, + MiddleName = r.MiddleName + }; + if (!string.IsNullOrEmpty(r.EmailAddress)) { addresses.Add(new EmailAddressPoint(r.EmailAddress)); @@ -34,7 +42,7 @@ public static NotificationOrderRequest MapToOrderRequest(this NotificationOrderR addresses.Add(new SmsAddressPoint(r.MobileNumber)); } - return new Recipient(addresses, r.OrganizationNumber, r.NationalIdentityNumber); + return new Recipient(addresses, r.OrganizationNumber, r.NationalIdentityNumber, nameComponents); }) .ToList(); diff --git a/src/Altinn.Notifications/Models/RecipientExt.cs b/src/Altinn.Notifications/Models/RecipientExt.cs index 307b921f..de7dad32 100644 --- a/src/Altinn.Notifications/Models/RecipientExt.cs +++ b/src/Altinn.Notifications/Models/RecipientExt.cs @@ -10,6 +10,30 @@ namespace Altinn.Notifications.Models; /// public class RecipientExt { + /// + /// Gets or sets the full name. + /// + [JsonPropertyName("name")] + public string? Name { get; init; } + + /// + /// Gets or sets the first name. + /// + [JsonPropertyName("firstName")] + public string? FirstName { get; init; } + + /// + /// Gets or sets the middle name. + /// + [JsonPropertyName("middleName")] + public string? MiddleName { get; init; } + + /// + /// Gets or sets the last name (surname). + /// + [JsonPropertyName("lastName")] + public string? LastName { get; init; } + /// /// Gets or sets the email address of the recipient. /// From 0fb50a23a2066533c329d8a47d5dc53315289945 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Tue, 26 Nov 2024 14:17:30 +0100 Subject: [PATCH 12/75] Simplify the keywords detection logic --- .../Extensions/StringExtensions.cs | 56 +++++++------------ .../NotificationTemplate/EmailTemplate.cs | 23 +------- .../INotificationTemplate.cs | 12 ---- .../NotificationTemplate/SmsTemplate.cs | 21 ------- 4 files changed, 21 insertions(+), 91 deletions(-) diff --git a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs index 76e1990c..ab68d9cf 100644 --- a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs @@ -7,64 +7,48 @@ namespace Altinn.Notifications.Core.Extensions; /// public static partial class StringExtensions { - /// - /// The regex pattern used to identify recipient name placeholders in a string. - /// - private static readonly Regex _recipientNamePlaceholdersKeywordsRegex = RecipientNamePlaceholdersKeywordsRegexPattern(); - - /// - /// The regex pattern used to identify recipient number placeholders in a string. - /// - private static readonly Regex _recipientNumberPlaceholdersKeywordsRegex = RecipientNumberPlaceholdersKeywordsRegexPattern(); + private static readonly Regex _recipientNamePlaceholderRegex = RecipientNamePlaceholderKeywordRegex(); + private static readonly Regex _recipientNumberPlaceholderRegex = RecipientNumberPlaceholderKeywordRegex(); /// - /// Checks whether the specified string contains any recipient name placeholders. + /// Checks whether the specified string contains the placeholder keyword $recipientName$. /// /// The string to check. - /// true if the string contains one or more recipient name placeholders; otherwise, false. - /// - /// The following recipient name placeholders are supported: - /// - /// $recipientFirstName$ - The first name of the recipient. - /// $recipientMiddleName$ - The middle name of the recipient. - /// $recipientLastName$ - The last name of the recipient. - /// $recipientName$ - The full name of the recipient or organization. - /// - /// - public static bool ContainsRecipientNamePlaceholders(this string value) + /// true if the string contains the placeholder keyword $recipientName$; otherwise, false. + public static bool ContainsRecipientNamePlaceholder(this string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } - return _recipientNamePlaceholdersKeywordsRegex.IsMatch(value); + return _recipientNamePlaceholderRegex.IsMatch(value); } /// - /// Checks whether the specified string contains any recipient number placeholders. + /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. /// /// The string to check. - /// true if the string contains one or more recipient number placeholders; otherwise, false. - /// - /// The following recipient number placeholders are supported: - /// - /// recipientNumber - The organization number when recipient is an organization. - /// - /// - public static bool ContainsRecipientNumberPlaceholders(this string value) + /// true if the string contains the placeholder keyword $recipientNumber$; otherwise, false. + public static bool ContainsRecipientNumberPlaceholder(this string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } - return _recipientNumberPlaceholdersKeywordsRegex.IsMatch(value); + return _recipientNumberPlaceholderRegex.IsMatch(value); } - [GeneratedRegex(@"\$recipient(FirstName|MiddleName|LastName|Name)\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex RecipientNamePlaceholdersKeywordsRegexPattern(); + /// + /// The regex pattern used to identify $recipientName$ in a string. + /// + [GeneratedRegex(@"\$recipientName\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex RecipientNamePlaceholderKeywordRegex(); - [GeneratedRegex(@"\$recipient(Number)\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex RecipientNumberPlaceholdersKeywordsRegexPattern(); + /// + /// The regex pattern used to identify $recipientNumber$ in a string. + /// + [GeneratedRegex(@"\$recipientNumber\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex RecipientNumberPlaceholderKeywordRegex(); } diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs index 42355df3..2d140d45 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs @@ -1,7 +1,4 @@ -using System.Text.Json.Serialization; - -using Altinn.Notifications.Core.Enums; -using Altinn.Notifications.Core.Extensions; +using Altinn.Notifications.Core.Enums; namespace Altinn.Notifications.Core.Models.NotificationTemplate; @@ -25,24 +22,6 @@ public class EmailTemplate : INotificationTemplate /// public string FromAddress { get; internal set; } = string.Empty; - /// - /// Gets a value indicating whether the email body or subject contains any recipient name placeholders. - /// - /// - /// true if the email body or subject contains any recipient name placeholders; otherwise, false. - /// - [JsonIgnore] - public bool HasRecipientNamePlaceholders => Subject.ContainsRecipientNamePlaceholders() || Body.ContainsRecipientNamePlaceholders(); - - /// - /// Gets a value indicating whether the email body contains any recipient number placeholders. - /// - /// - /// true if the email body contains any recipient number placeholders; otherwise, false. - /// - [JsonIgnore] - public bool HasRecipientNumberPlaceholders => Body.ContainsRecipientNumberPlaceholders(); - /// /// Gets the subject of the email. /// diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs index 9a7e1b96..cd5fbba2 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/INotificationTemplate.cs @@ -12,18 +12,6 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; [JsonPolymorphic(TypeDiscriminatorPropertyName = "$")] public interface INotificationTemplate { - /// - /// Gets a value indicating whether the email body or subject contains any recipient name placeholders. - /// - [JsonIgnore] - bool HasRecipientNamePlaceholders { get; } - - /// - /// Gets a value indicating whether the email body contains any recipient number placeholders. - /// - [JsonIgnore] - bool HasRecipientNumberPlaceholders { get; } - /// /// Gets the type of the notification template. /// diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs index 6fa05b0f..1dfc1ed6 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs @@ -1,7 +1,4 @@ -using System.Text.Json.Serialization; - using Altinn.Notifications.Core.Enums; -using Altinn.Notifications.Core.Extensions; namespace Altinn.Notifications.Core.Models.NotificationTemplate; @@ -15,24 +12,6 @@ public class SmsTemplate : INotificationTemplate /// public string Body { get; internal set; } = string.Empty; - /// - /// Gets a value indicating whether the SMS body contains any recipient name placeholders. - /// - /// - /// true if the SMS body contains any recipient name placeholders; otherwise, false. - /// - [JsonIgnore] - public bool HasRecipientNamePlaceholders => Body.ContainsRecipientNamePlaceholders(); - - /// - /// Gets a value indicating whether the SMS body contains any recipient number placeholders. - /// - /// - /// true if the SMS body contains any recipient number placeholders; otherwise, false. - /// - [JsonIgnore] - public bool HasRecipientNumberPlaceholders => false; - /// /// Gets the number from which the SMS is sent. /// From e3ceadcdbed5f212d492cae8ce6255c3aff6f8a2 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Tue, 26 Nov 2024 14:23:06 +0100 Subject: [PATCH 13/75] Remove the RecipientNameComponents type. --- .../Models/Recipient.cs | 11 ++----- .../Models/RecipientNameComponents.cs | 29 ------------------- .../Mappers/OrderMapper.cs | 10 +------ 3 files changed, 3 insertions(+), 47 deletions(-) delete mode 100644 src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs diff --git a/src/Altinn.Notifications.Core/Models/Recipient.cs b/src/Altinn.Notifications.Core/Models/Recipient.cs index e503279e..570d51b0 100644 --- a/src/Altinn.Notifications.Core/Models/Recipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipient.cs @@ -12,18 +12,13 @@ public class Recipient /// /// Gets or sets the list of address points for the recipient. /// - public List AddressInfo { get; set; } = new(); + public List AddressInfo { get; set; } = []; /// /// Gets or sets a value indicating whether the recipient is reserved from digital communication. /// public bool? IsReserved { get; set; } - /// - /// Gets or sets the recipient name components. - /// - public RecipientNameComponents? NameComponents { get; set; } - /// /// Gets or sets the recipient's national identity number. /// @@ -47,11 +42,9 @@ public Recipient() /// The list of address points for the recipient. /// The recipient's organization number. /// The recipient's national identity number. - /// The recipient name components. - public Recipient(List addressInfo, string? organizationNumber = null, string? nationalIdentityNumber = null, RecipientNameComponents? nameComponents = null) + public Recipient(List addressInfo, string? organizationNumber = null, string? nationalIdentityNumber = null) { AddressInfo = addressInfo; - NameComponents = nameComponents; OrganizationNumber = organizationNumber; NationalIdentityNumber = nationalIdentityNumber; } diff --git a/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs b/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs deleted file mode 100644 index cad00f96..00000000 --- a/src/Altinn.Notifications.Core/Models/RecipientNameComponents.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Altinn.Notifications.Core.Models; - -/// -/// Represents the components of a recipient's name. -/// -public class RecipientNameComponents -{ - /// - /// Gets or sets the first name. - /// - public string? FirstName { get; init; } - - /// - /// Gets or sets the full name. - /// - public string? Name { get; init; } - - /// - /// Gets or sets the last name (surname). - /// - public string? LastName { get; init; } - - /// - /// Gets or sets the middle name. - /// - public string? MiddleName { get; init; } -} diff --git a/src/Altinn.Notifications/Mappers/OrderMapper.cs b/src/Altinn.Notifications/Mappers/OrderMapper.cs index 6f6ff9dc..0bb80cb3 100644 --- a/src/Altinn.Notifications/Mappers/OrderMapper.cs +++ b/src/Altinn.Notifications/Mappers/OrderMapper.cs @@ -24,14 +24,6 @@ public static NotificationOrderRequest MapToOrderRequest(this NotificationOrderR { List addresses = []; - RecipientNameComponents nameComponents = new() - { - Name = r.Name, - LastName = r.LastName, - FirstName = r.FirstName, - MiddleName = r.MiddleName - }; - if (!string.IsNullOrEmpty(r.EmailAddress)) { addresses.Add(new EmailAddressPoint(r.EmailAddress)); @@ -42,7 +34,7 @@ public static NotificationOrderRequest MapToOrderRequest(this NotificationOrderR addresses.Add(new SmsAddressPoint(r.MobileNumber)); } - return new Recipient(addresses, r.OrganizationNumber, r.NationalIdentityNumber, nameComponents); + return new Recipient(addresses, r.OrganizationNumber, r.NationalIdentityNumber); }) .ToList(); From 8cc12837e8cb80f6cd75a6489631691c594a6203 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Tue, 26 Nov 2024 14:28:31 +0100 Subject: [PATCH 14/75] Remove the name components --- .../Models/RecipientExt.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Altinn.Notifications/Models/RecipientExt.cs b/src/Altinn.Notifications/Models/RecipientExt.cs index de7dad32..307b921f 100644 --- a/src/Altinn.Notifications/Models/RecipientExt.cs +++ b/src/Altinn.Notifications/Models/RecipientExt.cs @@ -10,30 +10,6 @@ namespace Altinn.Notifications.Models; /// public class RecipientExt { - /// - /// Gets or sets the full name. - /// - [JsonPropertyName("name")] - public string? Name { get; init; } - - /// - /// Gets or sets the first name. - /// - [JsonPropertyName("firstName")] - public string? FirstName { get; init; } - - /// - /// Gets or sets the middle name. - /// - [JsonPropertyName("middleName")] - public string? MiddleName { get; init; } - - /// - /// Gets or sets the last name (surname). - /// - [JsonPropertyName("lastName")] - public string? LastName { get; init; } - /// /// Gets or sets the email address of the recipient. /// From dbe1a774e662d3f04beca8ae79bab1184b501114 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Tue, 26 Nov 2024 14:39:20 +0100 Subject: [PATCH 15/75] Remove the unnecessary ConfigureAwait --- src/Altinn.Notifications.Core/Services/ContactPointService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 5b17fe12..30319993 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -53,7 +53,7 @@ await AugmentRecipients( .Select(u => new EmailAddressPoint(u.Email)) .ToList()); return recipient; - }).ConfigureAwait(false); + }); } /// @@ -82,7 +82,7 @@ await AugmentRecipients( .Select(u => new SmsAddressPoint(u.MobileNumber)) .ToList()); return recipient; - }).ConfigureAwait(false); + }); } /// From a1d13cea522418fd9ad7af694e960570096fc82a Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Wed, 27 Nov 2024 10:33:38 +0100 Subject: [PATCH 16/75] Create a new keyword service to inject actual value wherever a keyword is found --- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Extensions/StringExtensions.cs | 54 --- src/Altinn.Notifications.Core/Models/Email.cs | 20 +- src/Altinn.Notifications.Core/Models/Sms.cs | 20 +- .../Services/EmailNotificationService.cs | 6 +- .../Services/Interfaces/IKeywordsService.cs | 38 ++ .../Services/KeywordsService.cs | 417 ++++++++++++++++++ .../Services/SmsNotificationService.cs | 8 +- .../Repository/EmailNotificationRepository.cs | 8 +- .../Repository/SmsNotificationRepository.cs | 4 +- .../TestingModels/EmailTests.cs | 2 +- .../EmailNotificationServiceTests.cs | 11 +- .../SmsNotificationServiceTests.cs | 11 +- 13 files changed, 530 insertions(+), 70 deletions(-) delete mode 100644 src/Altinn.Notifications.Core/Extensions/StringExtensions.cs create mode 100644 src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs create mode 100644 src/Altinn.Notifications.Core/Services/KeywordsService.cs diff --git a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs index 70d932ca..7c2d3c14 100644 --- a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs @@ -46,6 +46,7 @@ public static void AddCoreServices(this IServiceCollection services, IConfigurat .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .Configure(config.GetSection("KafkaSettings")) .Configure(config.GetSection("NotificationConfig")); } diff --git a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs b/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs deleted file mode 100644 index ab68d9cf..00000000 --- a/src/Altinn.Notifications.Core/Extensions/StringExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Altinn.Notifications.Core.Extensions; - -/// -/// Provides extension methods for the class. -/// -public static partial class StringExtensions -{ - private static readonly Regex _recipientNamePlaceholderRegex = RecipientNamePlaceholderKeywordRegex(); - private static readonly Regex _recipientNumberPlaceholderRegex = RecipientNumberPlaceholderKeywordRegex(); - - /// - /// Checks whether the specified string contains the placeholder keyword $recipientName$. - /// - /// The string to check. - /// true if the string contains the placeholder keyword $recipientName$; otherwise, false. - public static bool ContainsRecipientNamePlaceholder(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - return _recipientNamePlaceholderRegex.IsMatch(value); - } - - /// - /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. - /// - /// The string to check. - /// true if the string contains the placeholder keyword $recipientNumber$; otherwise, false. - public static bool ContainsRecipientNumberPlaceholder(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - return _recipientNumberPlaceholderRegex.IsMatch(value); - } - - /// - /// The regex pattern used to identify $recipientName$ in a string. - /// - [GeneratedRegex(@"\$recipientName\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex RecipientNamePlaceholderKeywordRegex(); - - /// - /// The regex pattern used to identify $recipientNumber$ in a string. - /// - [GeneratedRegex(@"\$recipientNumber\$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex RecipientNumberPlaceholderKeywordRegex(); -} diff --git a/src/Altinn.Notifications.Core/Models/Email.cs b/src/Altinn.Notifications.Core/Models/Email.cs index dcab68d8..81514a64 100644 --- a/src/Altinn.Notifications.Core/Models/Email.cs +++ b/src/Altinn.Notifications.Core/Models/Email.cs @@ -34,6 +34,22 @@ public class Email /// public string ToAddress { get; set; } + /// + /// Gets or sets the national identity number. + /// + /// + /// The national identity number. + /// + public string NationalIdentityNumber { get; set; } + + /// + /// Gets or sets the organization number. + /// + /// + /// The organization number. + /// + public string OrganizationNumber { get; set; } + /// /// Gets or sets the content type of the email. /// @@ -42,7 +58,7 @@ public class Email /// /// Initializes a new instance of the class. /// - public Email(Guid notificationId, string subject, string body, string fromAddress, string toAddress, EmailContentType contentType) + public Email(Guid notificationId, string subject, string body, string fromAddress, string toAddress, EmailContentType contentType, string nationalIdentityNumber, string organizationNumber) { NotificationId = notificationId; Subject = subject; @@ -50,6 +66,8 @@ public Email(Guid notificationId, string subject, string body, string fromAddres FromAddress = fromAddress; ToAddress = toAddress; ContentType = contentType; + NationalIdentityNumber = nationalIdentityNumber; + OrganizationNumber = organizationNumber; } /// diff --git a/src/Altinn.Notifications.Core/Models/Sms.cs b/src/Altinn.Notifications.Core/Models/Sms.cs index c3139cbe..859f51fe 100644 --- a/src/Altinn.Notifications.Core/Models/Sms.cs +++ b/src/Altinn.Notifications.Core/Models/Sms.cs @@ -30,15 +30,33 @@ public class Sms /// public string Message { get; set; } + /// + /// Gets or sets the national identity number. + /// + /// + /// The national identity number. + /// + public string NationalIdentityNumber { get; set; } + + /// + /// Gets or sets the organization number. + /// + /// + /// The organization number. + /// + public string OrganizationNumber { get; set; } + /// /// Initializes a new instance of the class. /// - public Sms(Guid notificationId, string sender, string recipient, string message) + public Sms(Guid notificationId, string sender, string recipient, string message, string nationalIdentityNumber, string organizationNumber) { NotificationId = notificationId; Recipient = recipient; Sender = sender; Message = message; + NationalIdentityNumber = nationalIdentityNumber; + OrganizationNumber = organizationNumber; } /// diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index 5f4bbe8d..4477e2bd 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -21,6 +21,7 @@ public class EmailNotificationService : IEmailNotificationService private readonly IDateTimeService _dateTime; private readonly IEmailNotificationRepository _repository; private readonly IKafkaProducer _producer; + private readonly IKeywordsService _keywordsService; private readonly string _emailQueueTopicName; /// @@ -31,13 +32,15 @@ public EmailNotificationService( IDateTimeService dateTime, IEmailNotificationRepository repository, IKafkaProducer producer, - IOptions kafkaSettings) + IOptions kafkaSettings, + IKeywordsService keywordsService) { _guid = guid; _dateTime = dateTime; _repository = repository; _producer = producer; _emailQueueTopicName = kafkaSettings.Value.EmailQueueTopicName; + _keywordsService = keywordsService; } /// @@ -79,6 +82,7 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R public async Task SendNotifications() { List emails = await _repository.GetNewNotifications(); + emails = await _keywordsService.ReplaceKeywordsAsync(emails); foreach (Email email in emails) { diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs new file mode 100644 index 00000000..a8ce91f9 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -0,0 +1,38 @@ +using Altinn.Notifications.Core.Models; + +namespace Altinn.Notifications.Core.Services.Interfaces +{ + /// + /// Provides methods for handling keyword placeholders in collections of or . + /// + public interface IKeywordsService + { + /// + /// Checks whether the specified string contains the placeholder keyword $recipientName$. + /// + /// The string to check. + /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. + bool ContainsRecipientNamePlaceholder(string value); + + /// + /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. + /// + /// The string to check. + /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. + bool ContainsRecipientNumberPlaceholder(string value); + + /// + /// Replaces placeholder keywords in a collection of with actual values. + /// + /// The collection of to process. + /// A task that represents the asynchronous operation. The task result contains the collection of with replaced keywords. + Task> ReplaceKeywordsAsync(List smsList); + + /// + /// Replaces placeholder keywords in a collection of with actual values. + /// + /// The collection of to process. + /// A task that represents the asynchronous operation. The task result contains the collection of with replaced keywords. + Task> ReplaceKeywordsAsync(List emailList); + } +} diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs new file mode 100644 index 00000000..028c96ef --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -0,0 +1,417 @@ +using System.Text.RegularExpressions; + +using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services +{ + /// + /// Provides methods for handling keyword placeholders in collections of or . + /// + public class KeywordsService : IKeywordsService + { + private readonly IRegisterClient _registerClient; + + private const string _recipientNamePlaceholder = "$recipientName$"; + private const string _recipientNumberPlaceholder = "$recipientNumber$"; + + private static readonly Lazy _recipientNamePlaceholderRegex = + new(() => new Regex(Regex.Escape(_recipientNamePlaceholder), RegexOptions.Compiled | RegexOptions.CultureInvariant)); + + private static readonly Lazy _recipientNumberPlaceholderRegex = + new(() => new Regex(Regex.Escape(_recipientNumberPlaceholder), RegexOptions.Compiled | RegexOptions.CultureInvariant)); + + /// + /// Initializes a new instance of the class. + /// + /// The register client to interact with the register service. + /// Thrown if is null. + public KeywordsService(IRegisterClient registerClient) + { + _registerClient = registerClient ?? throw new ArgumentNullException(nameof(registerClient)); + } + + /// + public bool ContainsRecipientNamePlaceholder(string value) + { + return !string.IsNullOrWhiteSpace(value) && _recipientNamePlaceholderRegex.Value.IsMatch(value); + } + + /// + public bool ContainsRecipientNumberPlaceholder(string value) + { + return !string.IsNullOrWhiteSpace(value) && _recipientNumberPlaceholderRegex.Value.IsMatch(value); + } + + /// + public async Task> ReplaceKeywordsAsync(List smsList) + { + ArgumentNullException.ThrowIfNull(smsList); + + if (smsList.Count == 0) + { + return smsList; + } + + smsList = await InjectPersonNameAsync(smsList); + smsList = InjectNationalIdentityNumbers(smsList); + + smsList = InjectOrganizationNumbers(smsList); + smsList = await InjectOrganizationNameAsync(smsList); + + return smsList; + } + + /// + public async Task> ReplaceKeywordsAsync(List emailList) + { + ArgumentNullException.ThrowIfNull(emailList); + + if (emailList.Count == 0) + { + return emailList; + } + + emailList = await InjectPersonNameAsync(emailList); + emailList = InjectNationalIdentityNumbers(emailList); + + emailList = InjectOrganizationNumbers(emailList); + emailList = await InjectOrganizationNameAsync(emailList); + + return emailList; + } + + /// + /// Injects the recipient's name into the SMS where the $recipientName$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private async Task> InjectPersonNameAsync(List smsList) + { + ArgumentNullException.ThrowIfNull(smsList); + + if (smsList.Count == 0) + { + return smsList; + } + + var nationalIdentityNumbers = smsList + .Where(e => ContainsRecipientNamePlaceholder(e.Message)) + .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) + .Select(e => e.NationalIdentityNumber) + .Distinct() + .ToList(); + + if (nationalIdentityNumbers.Count == 0) + { + return smsList; + } + + var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers); + if (partyDetails == null || partyDetails.Count == 0) + { + return smsList; + } + + foreach (var partyDetail in partyDetails) + { + var sms = smsList.Find(e => e.NationalIdentityNumber == partyDetail.NationalIdentityNumber); + if (sms == null) + { + continue; + } + + sms.Message = sms.Message.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + } + + return smsList; + } + + /// + /// Injects the recipient's name into the email where the $recipientName$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private async Task> InjectPersonNameAsync(List emailList) + { + ArgumentNullException.ThrowIfNull(emailList); + + if (emailList.Count == 0) + { + return emailList; + } + + var nationalIdentityNumbers = emailList + .Where(e => ContainsRecipientNamePlaceholder(e.Subject) || ContainsRecipientNamePlaceholder(e.Body)) + .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) + .Select(e => e.NationalIdentityNumber) + .Distinct() + .ToList(); + + if (nationalIdentityNumbers.Count == 0) + { + return emailList; + } + + var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers); + if (partyDetails == null || partyDetails.Count == 0) + { + return emailList; + } + + foreach (var partyDetail in partyDetails) + { + var email = emailList.Find(e => e.NationalIdentityNumber == partyDetail.NationalIdentityNumber); + if (email == null) + { + continue; + } + + email.Body = email.Body.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + email.Subject = email.Subject.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + } + + return emailList; + } + + /// + /// Injects the recipient's national identity number into the SMS where the $recipientNumber$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private List InjectNationalIdentityNumbers(List smsList) + { + ArgumentNullException.ThrowIfNull(smsList); + + if (smsList.Count == 0) + { + return smsList; + } + + var smsWithNationalIdentityNumber = smsList + .Where(e => ContainsRecipientNumberPlaceholder(e.Message)) + .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) + .Distinct() + .ToList(); + + foreach (var smsWithKeyword in smsWithNationalIdentityNumber) + { + var sms = smsList.Find(e => e.NationalIdentityNumber == smsWithKeyword.NationalIdentityNumber); + if (sms == null) + { + continue; + } + + sms.Message = sms.Message.Replace(_recipientNumberPlaceholder, sms.NationalIdentityNumber ?? string.Empty); + } + + return smsList; + } + + /// + /// Injects the recipient's national identity number into the email where the $recipientNumber$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private List InjectNationalIdentityNumbers(List emailList) + { + ArgumentNullException.ThrowIfNull(emailList); + + if (emailList.Count == 0) + { + return emailList; + } + + var emailWithNationalIdentityNumber = emailList + .Where(e => ContainsRecipientNumberPlaceholder(e.Subject) || ContainsRecipientNumberPlaceholder(e.Body)) + .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) + .Distinct() + .ToList(); + + foreach (var emailWithKeyword in emailWithNationalIdentityNumber) + { + var email = emailList.Find(e => e.NationalIdentityNumber == emailWithKeyword.NationalIdentityNumber); + if (email == null) + { + continue; + } + + email.Body = email.Body.Replace(_recipientNumberPlaceholder, email.NationalIdentityNumber ?? string.Empty); + email.Subject = email.Subject.Replace(_recipientNumberPlaceholder, email.NationalIdentityNumber ?? string.Empty); + } + + return emailList; + } + + /// + /// Injects the recipient's organization number into the SMS where the $recipientNumber$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private List InjectOrganizationNumbers(List smsList) + { + ArgumentNullException.ThrowIfNull(smsList); + + if (smsList.Count == 0) + { + return smsList; + } + + var smsWithNationalIdentityNumber = smsList + .Where(e => ContainsRecipientNumberPlaceholder(e.Message)) + .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) + .Distinct() + .ToList(); + + foreach (var smsWithKeyword in smsWithNationalIdentityNumber) + { + var sms = smsList.Find(e => e.OrganizationNumber == smsWithKeyword.OrganizationNumber); + if (sms == null) + { + continue; + } + + sms.Message = sms.Message.Replace(_recipientNumberPlaceholder, sms.OrganizationNumber ?? string.Empty); + } + + return smsList; + } + + /// + /// Injects the recipient's organization number into the email where the $recipientNumber$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private List InjectOrganizationNumbers(List emailList) + { + ArgumentNullException.ThrowIfNull(emailList); + + if (emailList.Count == 0) + { + return emailList; + } + + var emailWithNationalIdentityNumber = emailList + .Where(e => ContainsRecipientNumberPlaceholder(e.Subject) || ContainsRecipientNumberPlaceholder(e.Body)) + .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) + .Distinct() + .ToList(); + + foreach (var emailWithKeyword in emailWithNationalIdentityNumber) + { + var email = emailList.Find(e => e.OrganizationNumber == emailWithKeyword.OrganizationNumber); + if (email == null) + { + continue; + } + + email.Body = email.Body.Replace(_recipientNumberPlaceholder, email.OrganizationNumber ?? string.Empty); + email.Subject = email.Subject.Replace(_recipientNumberPlaceholder, email.OrganizationNumber ?? string.Empty); + } + + return emailList; + } + + /// + /// Injects the recipient's organization name into the SMS where the $recipientName$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private async Task> InjectOrganizationNameAsync(List smsList) + { + ArgumentNullException.ThrowIfNull(smsList); + + if (smsList.Count == 0) + { + return smsList; + } + + var organizationNumbers = smsList + .Where(e => ContainsRecipientNamePlaceholder(e.Message)) + .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) + .Select(e => e.OrganizationNumber) + .Distinct() + .ToList(); + + if (organizationNumbers.Count == 0) + { + return smsList; + } + + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers); + if (partyDetails == null || partyDetails.Count == 0) + { + return smsList; + } + + foreach (var partyDetail in partyDetails) + { + var sms = smsList.Find(e => e.OrganizationNumber == partyDetail.OrganizationNumber); + if (sms == null) + { + continue; + } + + sms.Message = sms.Message.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + } + + return smsList; + } + + /// + /// Injects the recipient's organization name into the email where the $recipientName$ placeholder is found. + /// + /// The list of . + /// The updated list of . + /// Thrown if is null. + private async Task> InjectOrganizationNameAsync(List emailList) + { + ArgumentNullException.ThrowIfNull(emailList); + + if (emailList.Count == 0) + { + return emailList; + } + + var organizationNumbers = emailList + .Where(e => ContainsRecipientNamePlaceholder(e.Subject) || ContainsRecipientNamePlaceholder(e.Body)) + .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) + .Select(e => e.OrganizationNumber) + .Distinct() + .ToList(); + + if (organizationNumbers.Count == 0) + { + return emailList; + } + + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers); + if (partyDetails == null || partyDetails.Count == 0) + { + return emailList; + } + + foreach (var partyDetail in partyDetails) + { + var email = emailList.Find(e => e.OrganizationNumber == partyDetail.OrganizationNumber); + if (email == null) + { + continue; + } + + email.Body = email.Body.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + email.Subject = email.Subject.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + } + + return emailList; + } + } +} diff --git a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs index a9d9383b..c13c3a95 100644 --- a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -21,6 +21,7 @@ public class SmsNotificationService : ISmsNotificationService private readonly IDateTimeService _dateTime; private readonly ISmsNotificationRepository _repository; private readonly IKafkaProducer _producer; + private readonly IKeywordsService _keywordsService; private readonly string _smsQueueTopicName; /// @@ -31,12 +32,14 @@ public SmsNotificationService( IDateTimeService dateTime, ISmsNotificationRepository repository, IKafkaProducer producer, - IOptions kafkaSettings) + IOptions kafkaSettings, + IKeywordsService keywordsService) { _guid = guid; _dateTime = dateTime; _repository = repository; _producer = producer; + _keywordsService = keywordsService; _smsQueueTopicName = kafkaSettings.Value.SmsQueueTopicName; } @@ -77,7 +80,8 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R /// public async Task SendNotifications() { - List smsList = await _repository.GetNewNotifications(); + var smsList = await _repository.GetNewNotifications(); + smsList = await _keywordsService.ReplaceKeywordsAsync(smsList); foreach (Sms sms in smsList) { diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 3f68b070..8db968b1 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -30,7 +30,7 @@ public class EmailNotificationRepository : IEmailNotificationRepository resulttime = now(), operationid = $2 WHERE alternateid = $3 OR operationid = $2;"; // (_result, _operationid, _alternateid) - + /// /// Initializes a new instance of the class. /// @@ -80,7 +80,9 @@ public async Task> GetNewNotifications() reader.GetValue("body"), reader.GetValue("fromaddress"), reader.GetValue("toaddress"), - emailContentType); + emailContentType, + reader.GetValue("recipientnin"), + reader.GetValue("recipientorgno")); searchResult.Add(email); } @@ -114,7 +116,7 @@ public async Task> GetRecipients(Guid orderId) { while (await reader.ReadAsync()) { - searchResult.Add(new EmailRecipient() + searchResult.Add(new EmailRecipient() { OrganizationNumber = reader.GetValue("recipientorgno"), NationalIdentityNumber = reader.GetValue("recipientnin"), diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index 9d7c47f4..0802c871 100644 --- a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs @@ -103,7 +103,9 @@ public async Task> GetNewNotifications() reader.GetValue("alternateid"), reader.GetValue("sendernumber"), reader.GetValue("mobilenumber"), - reader.GetValue("body")); + reader.GetValue("body"), + reader.GetValue("recipientnin"), + reader.GetValue("recipientorgno")); searchResult.Add(sms); } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs index 754fde84..5f910ee2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs @@ -16,7 +16,7 @@ public class EmailTests public EmailTests() { Guid id = Guid.NewGuid(); - _email = new Email(id, "subject", "body", "from@domain.com", "to@domain.com", EmailContentType.Html); + _email = new Email(id, "subject", "body", "from@domain.com", "to@domain.com", EmailContentType.Html, "Test organization number", "Test national identity number"); _serializedEmail = new JsonObject() { { "notificationId", id }, diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index 6266a682..c70f7a5b 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class EmailNotificationServiceTests { private const string _emailQueueTopicName = "email.queue"; - private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", Altinn.Notifications.Core.Enums.EmailContentType.Plain); + private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", EmailContentType.Plain, "Test organization number", "Test national identity number"); [Fact] public async Task SendNotifications_ProducerCalledOnceForEachRetrievedEmail() @@ -288,7 +288,7 @@ public async Task UpdateSendStatus_TransientErrorResult_ConvertedToNew() repoMock.Verify(); } - private static EmailNotificationService GetTestService(IEmailNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) + private static EmailNotificationService GetTestService(IEmailNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null, IKeywordsService? keywordsService = null) { var guidService = new Mock(); guidService @@ -311,6 +311,11 @@ private static EmailNotificationService GetTestService(IEmailNotificationReposit producer = producerMock.Object; } - return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName })); + if (keywordsService == null) + { + keywordsService = new Mock().Object; + } + + return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName }), keywordsService); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index a972cab3..cda424be 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class SmsNotificationServiceTests { private const string _smsQueueTopicName = "test.sms.queue"; - private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message"); + private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message", "Test national identity number", "Test organization number"); [Fact] public async Task CreateNotifications_NewSmsNotification_RepositoryCalledOnce() @@ -281,7 +281,7 @@ public async Task UpdateSendStatus_SendResultDefined_Succeded() repoMock.Verify(); } - private static SmsNotificationService GetTestService(ISmsNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) + private static SmsNotificationService GetTestService(ISmsNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null, IKeywordsService? keywordsService = null) { var guidService = new Mock(); guidService @@ -304,6 +304,11 @@ private static SmsNotificationService GetTestService(ISmsNotificationRepository? producer = producerMock.Object; } - return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName })); + if (keywordsService == null) + { + keywordsService = new Mock().Object; + } + + return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName }), keywordsService); } } From ea80c4974444f17b8a4941365e4f5d1b46c21db4 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Wed, 27 Nov 2024 10:37:17 +0100 Subject: [PATCH 17/75] Update the test data --- .../TestingServices/EmailNotificationServiceTests.cs | 2 +- .../TestingServices/SmsNotificationServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index c70f7a5b..e5e8e6cc 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class EmailNotificationServiceTests { private const string _emailQueueTopicName = "email.queue"; - private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", EmailContentType.Plain, "Test organization number", "Test national identity number"); + private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", EmailContentType.Plain, "18874198354", "313441571"); [Fact] public async Task SendNotifications_ProducerCalledOnceForEachRetrievedEmail() diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index cda424be..e3d72b0f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class SmsNotificationServiceTests { private const string _smsQueueTopicName = "test.sms.queue"; - private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message", "Test national identity number", "Test organization number"); + private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message", "10825795702 ", "310679941"); [Fact] public async Task CreateNotifications_NewSmsNotification_RepositoryCalledOnce() From 7c6b4277168f77d286755e7844e86d8f7d2413e2 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Wed, 27 Nov 2024 12:01:43 +0100 Subject: [PATCH 18/75] Update two functions to retrieve recipient number and recipient name --- .../getemailsstatusnewupdatestatus.sql | 8 ++++---- .../getsmsstatusnewupdatestatus.sql | 9 ++++----- .../Migration/v0.36/01-functions-and-procedures.sql | 1 + 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql index 93856b6e..d857e2cd 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql @@ -1,5 +1,5 @@ CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text, recipientorgno text, recipientnin text) LANGUAGE 'plpgsql' AS $BODY$ DECLARE @@ -8,7 +8,7 @@ BEGIN SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); IF latest_email_timeout IS NOT NULL THEN IF latest_email_timeout >= NOW() THEN - RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype, NULL::text AS recipientorgno, NULL::text AS recipientnin WHERE FALSE; RETURN; ELSE UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); @@ -20,8 +20,8 @@ BEGIN UPDATE notifications.emailnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress) - SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.recipientorgno, notifications.emailnotifications.recipientnin) + SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype, u.recipientorgno, u.recipientnin FROM updated u, notifications.emailtexts et WHERE u._orderid = et._orderid; END; diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql index 91dafe24..8bb94b7b 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql @@ -1,17 +1,16 @@ CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text, recipientorgno text, recipientnin text) LANGUAGE 'plpgsql' AS $BODY$ BEGIN - RETURN query WITH updated AS ( UPDATE notifications.smsnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber) - SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body + RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber, notifications.smsnotifications.recipientorgno, notifications.smsnotifications.recipientnin) + SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body, u.recipientorgno, u.recipientnin FROM updated u, notifications.smstexts st - WHERE u._orderid = st._orderid; + WHERE u._orderid = st._orderid; END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql new file mode 100644 index 00000000..5f088eea --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql @@ -0,0 +1 @@ +-- This script is autogenerated from the tool DbTools. Do not edit manually. \ No newline at end of file From 6ffb6db3635732890eeafb3013f3452e830939b8 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 07:18:47 +0100 Subject: [PATCH 19/75] Update the sender email address. --- src/Altinn.Notifications/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Notifications/appsettings.json b/src/Altinn.Notifications/appsettings.json index 1dc15a38..54d39b70 100644 --- a/src/Altinn.Notifications/appsettings.json +++ b/src/Altinn.Notifications/appsettings.json @@ -13,7 +13,7 @@ "EnableDBConnection": true }, "NotificationConfig": { - "DefaultEmailFromAddress": "noreply@altinn.no", + "DefaultEmailFromAddress": "noreply@altinn.cloud", "DefaultSmsSenderNumber": "Altinn", "SmsSendWindowStartHour": 9, "SmsSendWindowEndHour": 17 From d5d264dfc6fba450b6eb8156d0f345df07f599a9 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 11:57:11 +0100 Subject: [PATCH 20/75] Save the customized email subject and body in the database --- .../Models/Recipients/EmailRecipient.cs | 26 ++- .../Services/EmailNotificationService.cs | 18 +- .../Services/EmailOrderProcessingService.cs | 4 +- .../Interfaces/IEmailNotificationService.cs | 2 +- .../Services/Interfaces/IKeywordsService.cs | 13 +- .../Services/KeywordsService.cs | 214 ++++++++---------- .../insertemailnotification.sql | 31 ++- .../Repository/EmailNotificationRepository.cs | 4 +- 8 files changed, 162 insertions(+), 150 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs index bb99c76c..a5ce0594 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs @@ -1,27 +1,37 @@ namespace Altinn.Notifications.Core.Models.Recipients; /// -/// Class representing an email recipient +/// Class representing an email recipient. /// public class EmailRecipient { /// - /// Gets or sets the recipient's organization number + /// Gets or sets the customized body of the email. /// - public string? OrganizationNumber { get; set; } = null; + public string? CustomizedBody { get; set; } = null; + + /// + /// Gets or sets the customized subject of the email. + /// + public string? CustomizedSubject { get; set; } = null; + + /// + /// Gets or sets a value indicating whether the recipient is reserved from digital communication. + /// + public bool? IsReserved { get; set; } /// - /// Gets or sets the recipient's national identity number + /// Gets or sets the recipient's national identity number. /// public string? NationalIdentityNumber { get; set; } = null; /// - /// Gets or sets the toaddress + /// Gets or sets the recipient's organization number. /// - public string ToAddress { get; set; } = string.Empty; + public string? OrganizationNumber { get; set; } = null; /// - /// Gets or sets a value indicating whether the recipient is reserved from digital communication + /// Gets or sets the email address of the recipient. /// - public bool? IsReserved { get; set; } + public string ToAddress { get; set; } = string.Empty; } diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index 4477e2bd..06dfd8a6 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -21,8 +21,8 @@ public class EmailNotificationService : IEmailNotificationService private readonly IDateTimeService _dateTime; private readonly IEmailNotificationRepository _repository; private readonly IKafkaProducer _producer; - private readonly IKeywordsService _keywordsService; private readonly string _emailQueueTopicName; + private readonly IKeywordsService _keywordsService; /// /// Initializes a new instance of the class. @@ -44,7 +44,7 @@ public EmailNotificationService( } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false) + public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false, string? emailBody = null, string? emailSubject = null) { List emailAddresses = recipient.AddressInfo .Where(a => a.AddressType == AddressType.Email) @@ -54,11 +54,15 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R EmailRecipient emailRecipient = new() { + IsReserved = recipient.IsReserved, OrganizationNumber = recipient.OrganizationNumber, NationalIdentityNumber = recipient.NationalIdentityNumber, - IsReserved = recipient.IsReserved + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailBody) || _keywordsService.ContainsRecipientNamePlaceholder(emailBody)) ? emailBody : null, + CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailSubject) || _keywordsService.ContainsRecipientNamePlaceholder(emailSubject)) ? emailSubject : null, }; + emailRecipient = await _keywordsService.ReplaceKeywordsAsync(emailRecipient); + if (recipient.IsReserved.HasValue && recipient.IsReserved.Value && !ignoreReservation) { emailRecipient.ToAddress = string.Empty; // not persisting email address for reserved recipients @@ -74,15 +78,15 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R foreach (EmailAddressPoint addressPoint in emailAddresses) { emailRecipient.ToAddress = addressPoint.EmailAddress; + await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.New); - } + } } /// public async Task SendNotifications() { List emails = await _repository.GetNewNotifications(); - emails = await _keywordsService.ReplaceKeywordsAsync(emails); foreach (Email email in emails) { @@ -110,10 +114,10 @@ private async Task CreateNotificationForRecipient(Guid orderId, DateTime request { var emailNotification = new EmailNotification() { - Id = _guid.NewGuid(), OrderId = orderId, - RequestedSendTime = requestedSendTime, + Id = _guid.NewGuid(), Recipient = recipient, + RequestedSendTime = requestedSendTime, SendResult = new(result, _dateTime.UtcNow()) }; diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index e271bd11..573926d6 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -1,6 +1,7 @@ using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.NotificationTemplate; using Altinn.Notifications.Core.Models.Orders; using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; @@ -44,9 +45,10 @@ public async Task ProcessOrder(NotificationOrder order) /// public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) { + var emailTemplate = order.Templates[0] as EmailTemplate; foreach (Recipient recipient in recipients) { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false); + await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false, emailTemplate?.Body, emailTemplate?.Subject); } } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs index 70f9b68a..d60efaef 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs @@ -11,7 +11,7 @@ public interface IEmailNotificationService /// /// Creates a new email notification based on the provided orderId and recipient /// - public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false); + public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false, string? emailBody = null, string? emailSubject = null); /// /// Starts the process of sending all ready email notifications diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs index a8ce91f9..d3bbb9f9 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -1,4 +1,5 @@ using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Recipients; namespace Altinn.Notifications.Core.Services.Interfaces { @@ -12,14 +13,14 @@ public interface IKeywordsService /// /// The string to check. /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. - bool ContainsRecipientNamePlaceholder(string value); + bool ContainsRecipientNamePlaceholder(string? value); /// /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. /// /// The string to check. /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. - bool ContainsRecipientNumberPlaceholder(string value); + bool ContainsRecipientNumberPlaceholder(string? value); /// /// Replaces placeholder keywords in a collection of with actual values. @@ -29,10 +30,10 @@ public interface IKeywordsService Task> ReplaceKeywordsAsync(List smsList); /// - /// Replaces placeholder keywords in a collection of with actual values. + /// Replaces placeholder keywords in an with actual values. /// - /// The collection of to process. - /// A task that represents the asynchronous operation. The task result contains the collection of with replaced keywords. - Task> ReplaceKeywordsAsync(List emailList); + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with actual values. + Task ReplaceKeywordsAsync(EmailRecipient emailRecipient); } } diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 028c96ef..2a4bf33c 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -2,6 +2,7 @@ using Altinn.Notifications.Core.Integrations; using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Services.Interfaces; namespace Altinn.Notifications.Core.Services @@ -33,13 +34,13 @@ public KeywordsService(IRegisterClient registerClient) } /// - public bool ContainsRecipientNamePlaceholder(string value) + public bool ContainsRecipientNamePlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && _recipientNamePlaceholderRegex.Value.IsMatch(value); } /// - public bool ContainsRecipientNumberPlaceholder(string value) + public bool ContainsRecipientNumberPlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && _recipientNumberPlaceholderRegex.Value.IsMatch(value); } @@ -64,22 +65,17 @@ public async Task> ReplaceKeywordsAsync(List smsList) } /// - public async Task> ReplaceKeywordsAsync(List emailList) + public async Task ReplaceKeywordsAsync(EmailRecipient emailRecipient) { - ArgumentNullException.ThrowIfNull(emailList); + ArgumentNullException.ThrowIfNull(emailRecipient); - if (emailList.Count == 0) - { - return emailList; - } + emailRecipient = await InjectPersonNameAsync(emailRecipient); + emailRecipient = InjectNationalIdentityNumbers(emailRecipient); - emailList = await InjectPersonNameAsync(emailList); - emailList = InjectNationalIdentityNumbers(emailList); + emailRecipient = InjectOrganizationNumbers(emailRecipient); + emailRecipient = await InjectOrganizationNameAsync(emailRecipient); - emailList = InjectOrganizationNumbers(emailList); - emailList = await InjectOrganizationNameAsync(emailList); - - return emailList; + return emailRecipient; } /// @@ -130,51 +126,47 @@ private async Task> InjectPersonNameAsync(List smsList) } /// - /// Injects the recipient's name into the email where the $recipientName$ placeholder is found. + /// Injects the recipient's name wherever the $recipientName$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private async Task> InjectPersonNameAsync(List emailList) + /// The list of . + /// The updated list of . + /// Thrown if is null. + private async Task InjectPersonNameAsync(EmailRecipient emailRecipient) { - ArgumentNullException.ThrowIfNull(emailList); + ArgumentNullException.ThrowIfNull(emailRecipient); - if (emailList.Count == 0) + // If the recipient does not contain the recipient name placeholder, we do not need to look up the person name. + bool containsRecipientNamePlaceholder = ContainsRecipientNamePlaceholder(emailRecipient.CustomizedBody) || + ContainsRecipientNamePlaceholder(emailRecipient.CustomizedSubject); + if (!containsRecipientNamePlaceholder) { - return emailList; + return emailRecipient; } - var nationalIdentityNumbers = emailList - .Where(e => ContainsRecipientNamePlaceholder(e.Subject) || ContainsRecipientNamePlaceholder(e.Body)) - .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) - .Select(e => e.NationalIdentityNumber) - .Distinct() - .ToList(); - - if (nationalIdentityNumbers.Count == 0) + // If the recipient does not have an person number, we do not need to look up the person name. + if (string.IsNullOrWhiteSpace(emailRecipient.NationalIdentityNumber)) { - return emailList; + return emailRecipient; } - var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers); + // Look up the person name and replace the recipient name placeholder with the person name. + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([emailRecipient.NationalIdentityNumber]); if (partyDetails == null || partyDetails.Count == 0) { - return emailList; + return emailRecipient; } - foreach (var partyDetail in partyDetails) + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) { - var email = emailList.Find(e => e.NationalIdentityNumber == partyDetail.NationalIdentityNumber); - if (email == null) - { - continue; - } + emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); + } - email.Body = email.Body.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); - email.Subject = email.Subject.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) + { + emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); } - return emailList; + return emailRecipient; } /// @@ -213,39 +205,38 @@ private List InjectNationalIdentityNumbers(List smsList) } /// - /// Injects the recipient's national identity number into the email where the $recipientNumber$ placeholder is found. + /// Injects the recipient's national identity number wherever the $recipientNumber$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private List InjectNationalIdentityNumbers(List emailList) + /// The list of . + /// The updated list of . + /// Thrown if is null. + private EmailRecipient InjectNationalIdentityNumbers(EmailRecipient emailRecipient) { - ArgumentNullException.ThrowIfNull(emailList); + ArgumentNullException.ThrowIfNull(emailRecipient); - if (emailList.Count == 0) + bool containsRecipientNumberPlaceholder = ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedBody) || + ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedSubject); + if (!containsRecipientNumberPlaceholder) { - return emailList; + return emailRecipient; } - var emailWithNationalIdentityNumber = emailList - .Where(e => ContainsRecipientNumberPlaceholder(e.Subject) || ContainsRecipientNumberPlaceholder(e.Body)) - .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) - .Distinct() - .ToList(); + if (string.IsNullOrWhiteSpace(emailRecipient.NationalIdentityNumber)) + { + return emailRecipient; + } - foreach (var emailWithKeyword in emailWithNationalIdentityNumber) + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) { - var email = emailList.Find(e => e.NationalIdentityNumber == emailWithKeyword.NationalIdentityNumber); - if (email == null) - { - continue; - } + emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, emailRecipient.NationalIdentityNumber ?? string.Empty); + } - email.Body = email.Body.Replace(_recipientNumberPlaceholder, email.NationalIdentityNumber ?? string.Empty); - email.Subject = email.Subject.Replace(_recipientNumberPlaceholder, email.NationalIdentityNumber ?? string.Empty); + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) + { + emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNumberPlaceholder, emailRecipient.NationalIdentityNumber ?? string.Empty); } - return emailList; + return emailRecipient; } /// @@ -284,39 +275,38 @@ private List InjectOrganizationNumbers(List smsList) } /// - /// Injects the recipient's organization number into the email where the $recipientNumber$ placeholder is found. + /// Injects the recipient's organization number wherever the $recipientNumber$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private List InjectOrganizationNumbers(List emailList) + /// The list of . + /// The updated list of . + /// Thrown if is null. + private EmailRecipient InjectOrganizationNumbers(EmailRecipient emailRecipient) { - ArgumentNullException.ThrowIfNull(emailList); + ArgumentNullException.ThrowIfNull(emailRecipient); - if (emailList.Count == 0) + bool containsRecipientNumberPlaceholder = ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedBody) || + ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedSubject); + if (!containsRecipientNumberPlaceholder) { - return emailList; + return emailRecipient; } - var emailWithNationalIdentityNumber = emailList - .Where(e => ContainsRecipientNumberPlaceholder(e.Subject) || ContainsRecipientNumberPlaceholder(e.Body)) - .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) - .Distinct() - .ToList(); + if (string.IsNullOrWhiteSpace(emailRecipient.OrganizationNumber)) + { + return emailRecipient; + } - foreach (var emailWithKeyword in emailWithNationalIdentityNumber) + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) { - var email = emailList.Find(e => e.OrganizationNumber == emailWithKeyword.OrganizationNumber); - if (email == null) - { - continue; - } + emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, emailRecipient.OrganizationNumber ?? string.Empty); + } - email.Body = email.Body.Replace(_recipientNumberPlaceholder, email.OrganizationNumber ?? string.Empty); - email.Subject = email.Subject.Replace(_recipientNumberPlaceholder, email.OrganizationNumber ?? string.Empty); + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) + { + emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNumberPlaceholder, emailRecipient.OrganizationNumber ?? string.Empty); } - return emailList; + return emailRecipient; } /// @@ -367,51 +357,47 @@ private async Task> InjectOrganizationNameAsync(List smsList) } /// - /// Injects the recipient's organization name into the email where the $recipientName$ placeholder is found. + /// Injects the recipient's organization name wherever the $recipientName$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private async Task> InjectOrganizationNameAsync(List emailList) + /// The . + /// The updated . + /// Thrown if is null. + private async Task InjectOrganizationNameAsync(EmailRecipient emailRecipient) { - ArgumentNullException.ThrowIfNull(emailList); + ArgumentNullException.ThrowIfNull(emailRecipient); - if (emailList.Count == 0) + // If the recipient does not contain the recipient name placeholder, we do not need to look up the organization name. + bool containsRecipientNamePlaceholder = ContainsRecipientNamePlaceholder(emailRecipient.CustomizedBody) || + ContainsRecipientNamePlaceholder(emailRecipient.CustomizedSubject); + if (!containsRecipientNamePlaceholder) { - return emailList; + return emailRecipient; } - var organizationNumbers = emailList - .Where(e => ContainsRecipientNamePlaceholder(e.Subject) || ContainsRecipientNamePlaceholder(e.Body)) - .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) - .Select(e => e.OrganizationNumber) - .Distinct() - .ToList(); - - if (organizationNumbers.Count == 0) + // If the recipient does not have an organization number, we do not need to look up the organization name. + if (string.IsNullOrWhiteSpace(emailRecipient.OrganizationNumber)) { - return emailList; + return emailRecipient; } - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers); + // Look up the organization name and replace the recipient name placeholder with the organization name. + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([emailRecipient.OrganizationNumber]); if (partyDetails == null || partyDetails.Count == 0) { - return emailList; + return emailRecipient; } - foreach (var partyDetail in partyDetails) + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) { - var email = emailList.Find(e => e.OrganizationNumber == partyDetail.OrganizationNumber); - if (email == null) - { - continue; - } + emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); + } - email.Body = email.Body.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); - email.Subject = email.Subject.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) + { + emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); } - return emailList; + return emailRecipient; } } } diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql index 3e377d3a..80dd8d35 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql @@ -1,11 +1,13 @@ CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( -_orderid uuid, -_alternateid uuid, +_orderid uuid, +_alternateid uuid, _recipientorgno TEXT, _recipientnin TEXT, -_toaddress TEXT, -_result text, -_resulttime timestamptz, +_toaddress TEXT, +_customizedbody TEXT, +_customizedsubject TEXT, +_result TEXT, +_resulttime timestamptz, _expirytime timestamptz) LANGUAGE 'plpgsql' AS $BODY$ @@ -15,19 +17,24 @@ __orderid BIGINT := (SELECT _id from notifications.orders BEGIN INSERT INTO notifications.emailnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -toaddress, result, -resulttime, +_orderid, +alternateid, +recipientorgno, +recipientnin, +toaddress, +customizedBody, +customizedSubject, +result, +resulttime, expirytime) VALUES ( -__orderid, +__orderid, _alternateid, _recipientorgno, _recipientnin, _toaddress, +_customizedbody, +_customizedsubject, _result::emailnotificationresulttype, _resulttime, _expirytime); diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 8db968b1..03a028bc 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -21,7 +21,7 @@ public class EmailNotificationRepository : IEmailNotificationRepository private readonly NpgsqlDataSource _dataSource; private readonly TelemetryClient? _telemetryClient; - private const string _insertEmailNotificationSql = "call notifications.insertemailnotification($1, $2, $3, $4, $5, $6, $7, $8)"; // (__orderid, _alternateid, _recipientorgno, _recipientnin, _toaddress, _result, _resulttime, _expirytime) + private const string _insertEmailNotificationSql = "call notifications.insertemailnotification($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"; // (__orderid, _alternateid, _recipientorgno, _recipientnin, _toaddress, _customizedbody, _customizedsubject, _result, _resulttime, _expirytime) private const string _getEmailNotificationsSql = "select * from notifications.getemails_statusnew_updatestatus()"; private const string _getEmailRecipients = "select * from notifications.getemailrecipients_v2($1)"; // (_orderid) private const string _updateEmailStatus = @@ -53,6 +53,8 @@ public async Task AddNotification(EmailNotification notification, DateTime expir pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.OrganizationNumber ?? (object)DBNull.Value); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.NationalIdentityNumber ?? (object)DBNull.Value); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.ToAddress); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.CustomizedBody ?? (object)DBNull.Value); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.CustomizedSubject ?? (object)DBNull.Value); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.SendResult.Result.ToString()); pgcom.Parameters.AddWithValue(NpgsqlDbType.TimestampTz, notification.SendResult.ResultTime); pgcom.Parameters.AddWithValue(NpgsqlDbType.TimestampTz, expiry); From 3feb865a434093b7537573c08cde07e01c23cec1 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 12:37:55 +0100 Subject: [PATCH 21/75] Pass customized body and subject when trying to send Emails again --- .../Services/EmailOrderProcessingService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index 573926d6..54288bdd 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -66,6 +66,7 @@ public async Task ProcessOrderRetry(NotificationOrder order) /// public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { + var emailTemplate = order.Templates[0] as EmailTemplate; List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); foreach (Recipient recipient in recipients) @@ -77,7 +78,7 @@ public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, && er.OrganizationNumber == recipient.OrganizationNumber && er.ToAddress == addressPoint?.EmailAddress)) { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false); + await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false, emailTemplate?.Body, emailTemplate?.Subject); } } } From 918808d635adaee7fec992568f82e88265b59429 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 14:08:44 +0100 Subject: [PATCH 22/75] Add support for placeholder keywords in SMS --- .../Models/Recipients/SmsRecipient.cs | 5 + .../Services/Interfaces/IKeywordsService.cs | 8 +- .../Interfaces/ISmsNotificationService.cs | 2 +- .../Services/KeywordsService.cs | 183 +++++++----------- .../Services/SmsNotificationService.cs | 13 +- .../Services/SmsOrderProcessingService.cs | 4 +- .../getsmsstatusnewupdatestatus.sql | 2 +- .../insertsmsnotification.sql | 3 + .../Repository/SmsNotificationRepository.cs | 5 +- 9 files changed, 99 insertions(+), 126 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs index 70e7cf60..ffc9aa6e 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs @@ -20,6 +20,11 @@ public class SmsRecipient /// public string MobileNumber { get; set; } = string.Empty; + /// + /// Gets or sets the customized body of the SMS. + /// + public string? CustomizedBody { get; set; } = null; + /// /// Gets or sets a value indicating whether the recipient is reserved from digital communication /// diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs index d3bbb9f9..9437b55c 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -23,11 +23,11 @@ public interface IKeywordsService bool ContainsRecipientNumberPlaceholder(string? value); /// - /// Replaces placeholder keywords in a collection of with actual values. + /// Replaces placeholder keywords in an with actual values. /// - /// The collection of to process. - /// A task that represents the asynchronous operation. The task result contains the collection of with replaced keywords. - Task> ReplaceKeywordsAsync(List smsList); + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with actual values. + Task ReplaceKeywordsAsync(SmsRecipient smsRecipient); /// /// Replaces placeholder keywords in an with actual values. diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs index 099c7550..101fb79a 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs @@ -11,7 +11,7 @@ public interface ISmsNotificationService /// /// Creates a new sms notification based on the provided orderId and recipient /// - public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false); + public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false, string? body = null); /// /// Starts the process of sending all ready sms notifications diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 2a4bf33c..be122703 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -46,22 +46,17 @@ public bool ContainsRecipientNumberPlaceholder(string? value) } /// - public async Task> ReplaceKeywordsAsync(List smsList) + public async Task ReplaceKeywordsAsync(SmsRecipient smsRecipient) { - ArgumentNullException.ThrowIfNull(smsList); + ArgumentNullException.ThrowIfNull(smsRecipient); - if (smsList.Count == 0) - { - return smsList; - } - - smsList = await InjectPersonNameAsync(smsList); - smsList = InjectNationalIdentityNumbers(smsList); + smsRecipient = await InjectPersonNameAsync(smsRecipient); + smsRecipient = InjectNationalIdentityNumbers(smsRecipient); - smsList = InjectOrganizationNumbers(smsList); - smsList = await InjectOrganizationNameAsync(smsList); + smsRecipient = InjectOrganizationNumbers(smsRecipient); + smsRecipient = await InjectOrganizationNameAsync(smsRecipient); - return smsList; + return smsRecipient; } /// @@ -79,50 +74,40 @@ public async Task ReplaceKeywordsAsync(EmailRecipient emailRecip } /// - /// Injects the recipient's name into the SMS where the $recipientName$ placeholder is found. + /// Injects the recipient's name wherever the $recipientName$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private async Task> InjectPersonNameAsync(List smsList) + /// The . + /// The updated . + /// Thrown if is null. + private async Task InjectPersonNameAsync(SmsRecipient smsRecipient) { - ArgumentNullException.ThrowIfNull(smsList); + ArgumentNullException.ThrowIfNull(smsRecipient); - if (smsList.Count == 0) + // If the recipient does not contain the recipient name placeholder, we do not need to look up the person name. + if (!ContainsRecipientNamePlaceholder(smsRecipient.CustomizedBody)) { - return smsList; + return smsRecipient; } - var nationalIdentityNumbers = smsList - .Where(e => ContainsRecipientNamePlaceholder(e.Message)) - .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) - .Select(e => e.NationalIdentityNumber) - .Distinct() - .ToList(); - - if (nationalIdentityNumbers.Count == 0) + // If the recipient does not have an person number, we do not need to look up the person name. + if (string.IsNullOrWhiteSpace(smsRecipient.NationalIdentityNumber)) { - return smsList; + return smsRecipient; } - var partyDetails = await _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers); + // Look up the person name and replace the recipient name placeholder with the person name. + var partyDetails = await _registerClient.GetPartyDetailsForPersons([smsRecipient.NationalIdentityNumber]); if (partyDetails == null || partyDetails.Count == 0) { - return smsList; + return smsRecipient; } - foreach (var partyDetail in partyDetails) + if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) { - var sms = smsList.Find(e => e.NationalIdentityNumber == partyDetail.NationalIdentityNumber); - if (sms == null) - { - continue; - } - - sms.Message = sms.Message.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); } - return smsList; + return smsRecipient; } /// @@ -172,36 +157,29 @@ private async Task InjectPersonNameAsync(EmailRecipient emailRec /// /// Injects the recipient's national identity number into the SMS where the $recipientNumber$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private List InjectNationalIdentityNumbers(List smsList) + /// The list of . + /// The updated list of . + /// Thrown if is null. + private SmsRecipient InjectNationalIdentityNumbers(SmsRecipient smsRecipient) { - ArgumentNullException.ThrowIfNull(smsList); + ArgumentNullException.ThrowIfNull(smsRecipient); - if (smsList.Count == 0) + if (!ContainsRecipientNumberPlaceholder(smsRecipient.CustomizedBody)) { - return smsList; + return smsRecipient; } - var smsWithNationalIdentityNumber = smsList - .Where(e => ContainsRecipientNumberPlaceholder(e.Message)) - .Where(e => !string.IsNullOrEmpty(e.NationalIdentityNumber)) - .Distinct() - .ToList(); - - foreach (var smsWithKeyword in smsWithNationalIdentityNumber) + if (string.IsNullOrWhiteSpace(smsRecipient.NationalIdentityNumber)) { - var sms = smsList.Find(e => e.NationalIdentityNumber == smsWithKeyword.NationalIdentityNumber); - if (sms == null) - { - continue; - } + return smsRecipient; + } - sms.Message = sms.Message.Replace(_recipientNumberPlaceholder, sms.NationalIdentityNumber ?? string.Empty); + if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) + { + smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, smsRecipient.NationalIdentityNumber ?? string.Empty); } - return smsList; + return smsRecipient; } /// @@ -240,38 +218,31 @@ private EmailRecipient InjectNationalIdentityNumbers(EmailRecipient emailRecipie } /// - /// Injects the recipient's organization number into the SMS where the $recipientNumber$ placeholder is found. + /// Injects the recipient's organization number wherever the $recipientNumber$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private List InjectOrganizationNumbers(List smsList) + /// The . + /// The updated . + /// Thrown if is null. + private SmsRecipient InjectOrganizationNumbers(SmsRecipient smsRecipient) { - ArgumentNullException.ThrowIfNull(smsList); + ArgumentNullException.ThrowIfNull(smsRecipient); - if (smsList.Count == 0) + if (!ContainsRecipientNumberPlaceholder(smsRecipient.CustomizedBody)) { - return smsList; + return smsRecipient; } - var smsWithNationalIdentityNumber = smsList - .Where(e => ContainsRecipientNumberPlaceholder(e.Message)) - .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) - .Distinct() - .ToList(); - - foreach (var smsWithKeyword in smsWithNationalIdentityNumber) + if (string.IsNullOrWhiteSpace(smsRecipient.OrganizationNumber)) { - var sms = smsList.Find(e => e.OrganizationNumber == smsWithKeyword.OrganizationNumber); - if (sms == null) - { - continue; - } + return smsRecipient; + } - sms.Message = sms.Message.Replace(_recipientNumberPlaceholder, sms.OrganizationNumber ?? string.Empty); + if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) + { + smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, smsRecipient.OrganizationNumber ?? string.Empty); } - return smsList; + return smsRecipient; } /// @@ -310,50 +281,40 @@ private EmailRecipient InjectOrganizationNumbers(EmailRecipient emailRecipient) } /// - /// Injects the recipient's organization name into the SMS where the $recipientName$ placeholder is found. + /// Injects the recipient's organization name wherever the $recipientName$ placeholder is found. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private async Task> InjectOrganizationNameAsync(List smsList) + /// The . + /// The updated . + /// Thrown if is null. + private async Task InjectOrganizationNameAsync(SmsRecipient smsRecipient) { - ArgumentNullException.ThrowIfNull(smsList); + ArgumentNullException.ThrowIfNull(smsRecipient); - if (smsList.Count == 0) + // If the recipient does not contain the recipient name placeholder, we do not need to look up the organization name. + if (!ContainsRecipientNamePlaceholder(smsRecipient.CustomizedBody)) { - return smsList; + return smsRecipient; } - var organizationNumbers = smsList - .Where(e => ContainsRecipientNamePlaceholder(e.Message)) - .Where(e => !string.IsNullOrEmpty(e.OrganizationNumber)) - .Select(e => e.OrganizationNumber) - .Distinct() - .ToList(); - - if (organizationNumbers.Count == 0) + // If the recipient does not have an organization number, we do not need to look up the organization name. + if (string.IsNullOrWhiteSpace(smsRecipient.OrganizationNumber)) { - return smsList; + return smsRecipient; } - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(organizationNumbers); + // Look up the organization name and replace the recipient name placeholder with the organization name. + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([smsRecipient.OrganizationNumber]); if (partyDetails == null || partyDetails.Count == 0) { - return smsList; + return smsRecipient; } - foreach (var partyDetail in partyDetails) + if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) { - var sms = smsList.Find(e => e.OrganizationNumber == partyDetail.OrganizationNumber); - if (sms == null) - { - continue; - } - - sms.Message = sms.Message.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); } - return smsList; + return smsRecipient; } /// diff --git a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs index c13c3a95..2af18ce3 100644 --- a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -21,8 +21,8 @@ public class SmsNotificationService : ISmsNotificationService private readonly IDateTimeService _dateTime; private readonly ISmsNotificationRepository _repository; private readonly IKafkaProducer _producer; - private readonly IKeywordsService _keywordsService; private readonly string _smsQueueTopicName; + private readonly IKeywordsService _keywordsService; /// /// Initializes a new instance of the class. @@ -39,12 +39,12 @@ public SmsNotificationService( _dateTime = dateTime; _repository = repository; _producer = producer; - _keywordsService = keywordsService; _smsQueueTopicName = kafkaSettings.Value.SmsQueueTopicName; + _keywordsService = keywordsService; } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false) + public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false, string? smsBody = null) { List smsAddresses = recipient.AddressInfo .Where(a => a.AddressType == AddressType.Sms) @@ -54,11 +54,14 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R SmsRecipient smsRecipient = new() { + IsReserved = recipient.IsReserved, OrganizationNumber = recipient.OrganizationNumber, NationalIdentityNumber = recipient.NationalIdentityNumber, - IsReserved = recipient.IsReserved + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(smsBody) || _keywordsService.ContainsRecipientNamePlaceholder(smsBody)) ? smsBody : null, }; + smsRecipient = await _keywordsService.ReplaceKeywordsAsync(smsRecipient); + if (recipient.IsReserved.HasValue && recipient.IsReserved.Value && !ignoreReservation) { await CreateNotificationForRecipient(orderId, requestedSendTime, smsRecipient, SmsNotificationResultType.Failed_RecipientReserved); @@ -81,8 +84,6 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R public async Task SendNotifications() { var smsList = await _repository.GetNewNotifications(); - smsList = await _keywordsService.ReplaceKeywordsAsync(smsList); - foreach (Sms sms in smsList) { bool success = await _producer.ProduceAsync(_smsQueueTopicName, sms.Serialize()); diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs index 688da616..37069fe0 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -45,9 +45,11 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List { int smsCount = GetSmsCountForOrder(order); + var smsTemplate = order.Templates[0] as SmsTemplate; + foreach (Recipient recipient in recipients) { - await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount, order.IgnoreReservation ?? false); + await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount, order.IgnoreReservation ?? false, smsTemplate?.Body); } } diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql index 8bb94b7b..7b496819 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql @@ -9,7 +9,7 @@ BEGIN SET result = 'Sending', resulttime = now() WHERE result = 'New' RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber, notifications.smsnotifications.recipientorgno, notifications.smsnotifications.recipientnin) - SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body, u.recipientorgno, u.recipientnin + SELECT u.alternateid, st.sendernumber, u.mobilenumber, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body, u.recipientorgno, u.recipientnin FROM updated u, notifications.smstexts st WHERE u._orderid = st._orderid; END; diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql index 5f60838c..85d7ec01 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql @@ -4,6 +4,7 @@ _alternateid uuid, _recipientorgno TEXT, _recipientnin TEXT, _mobilenumber TEXT, +_customizedbody TEXT, _result text, _smscount integer, _resulttime timestamptz, @@ -22,6 +23,7 @@ alternateid, recipientorgno, recipientnin, mobilenumber, +customizedbody, result, smscount, resulttime, @@ -32,6 +34,7 @@ _alternateid, _recipientorgno, _recipientnin, _mobilenumber, +_customizedbody, _result::smsnotificationresulttype, _smscount, _resulttime, diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index 0802c871..3af37d47 100644 --- a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs @@ -21,7 +21,7 @@ public class SmsNotificationRepository : ISmsNotificationRepository private readonly NpgsqlDataSource _dataSource; private readonly TelemetryClient? _telemetryClient; - private const string _insertSmsNotificationSql = "call notifications.insertsmsnotification($1, $2, $3, $4, $5, $6, $7, $8, $9)"; // (__orderid, _alternateid, _recipientorgno, _recipientnin, _mobilenumber, _result, _smscount, _resulttime, _expirytime) + private const string _insertSmsNotificationSql = "call notifications.insertsmsnotification($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"; // (__orderid, _alternateid, _recipientorgno, _recipientnin, _mobilenumber, _customizedbody, _result, _smscount, _resulttime, _expirytime) private const string _getSmsNotificationsSql = "select * from notifications.getsms_statusnew_updatestatus()"; private const string _getSmsRecipients = "select * from notifications.getsmsrecipients_v2($1)"; // (_orderid) @@ -52,8 +52,9 @@ public async Task AddNotification(SmsNotification notification, DateTime expiry, pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, notification.OrderId); pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, notification.Id); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, (object)DBNull.Value); // recipientorgno - pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.NationalIdentityNumber ?? (object)DBNull.Value); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.NationalIdentityNumber ?? (object)DBNull.Value); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.MobileNumber); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.Recipient.CustomizedBody ?? (object)DBNull.Value); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, notification.SendResult.Result.ToString()); pgcom.Parameters.AddWithValue(NpgsqlDbType.Integer, smsCount); pgcom.Parameters.AddWithValue(NpgsqlDbType.TimestampTz, notification.SendResult.ResultTime); From d1391adb59976e60f7481ea92ef131c744f99ab6 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 14:42:21 +0100 Subject: [PATCH 23/75] Improve the keyword replacement logic --- .../Services/Interfaces/IKeywordsService.cs | 25 +- .../Services/KeywordsService.cs | 347 +++--------------- 2 files changed, 64 insertions(+), 308 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs index 9437b55c..20b21855 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -1,39 +1,38 @@ -using Altinn.Notifications.Core.Models; -using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Models.Recipients; namespace Altinn.Notifications.Core.Services.Interfaces { /// - /// Provides methods for handling keyword placeholders in collections of or . + /// Provides methods for handling keyword placeholders in and . /// public interface IKeywordsService { /// - /// Checks whether the specified string contains the placeholder keyword $recipientName$. + /// Checks whether the specified string contains the placeholder keyword $recipientName$. /// /// The string to check. - /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. + /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. bool ContainsRecipientNamePlaceholder(string? value); /// - /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. + /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. /// /// The string to check. - /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. + /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. bool ContainsRecipientNumberPlaceholder(string? value); /// - /// Replaces placeholder keywords in an with actual values. + /// Replaces placeholder keywords in an with actual values. /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with actual values. + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. Task ReplaceKeywordsAsync(SmsRecipient smsRecipient); /// - /// Replaces placeholder keywords in an with actual values. + /// Replaces placeholder keywords in an with actual values. /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with actual values. + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. Task ReplaceKeywordsAsync(EmailRecipient emailRecipient); } } diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index be122703..ef5ef1a0 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -1,14 +1,13 @@ using System.Text.RegularExpressions; using Altinn.Notifications.Core.Integrations; -using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Services.Interfaces; namespace Altinn.Notifications.Core.Services { /// - /// Provides methods for handling keyword placeholders in collections of or . + /// Provides methods for handling keyword placeholders in and . /// public class KeywordsService : IKeywordsService { @@ -50,11 +49,7 @@ public async Task ReplaceKeywordsAsync(SmsRecipient smsRecipient) { ArgumentNullException.ThrowIfNull(smsRecipient); - smsRecipient = await InjectPersonNameAsync(smsRecipient); - smsRecipient = InjectNationalIdentityNumbers(smsRecipient); - - smsRecipient = InjectOrganizationNumbers(smsRecipient); - smsRecipient = await InjectOrganizationNameAsync(smsRecipient); + smsRecipient = await ReplaceKeywordsAsync(smsRecipient, r => r.CustomizedBody, (r, v) => r.CustomizedBody = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); return smsRecipient; } @@ -64,301 +59,63 @@ public async Task ReplaceKeywordsAsync(EmailRecipient emailRecip { ArgumentNullException.ThrowIfNull(emailRecipient); - emailRecipient = await InjectPersonNameAsync(emailRecipient); - emailRecipient = InjectNationalIdentityNumbers(emailRecipient); - - emailRecipient = InjectOrganizationNumbers(emailRecipient); - emailRecipient = await InjectOrganizationNameAsync(emailRecipient); + emailRecipient = await ReplaceKeywordsAsync(emailRecipient, r => r.CustomizedBody, (r, v) => r.CustomizedBody = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); + emailRecipient = await ReplaceKeywordsAsync(emailRecipient, r => r.CustomizedSubject, (r, v) => r.CustomizedSubject = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); return emailRecipient; } /// - /// Injects the recipient's name wherever the $recipientName$ placeholder is found. - /// - /// The . - /// The updated . - /// Thrown if is null. - private async Task InjectPersonNameAsync(SmsRecipient smsRecipient) - { - ArgumentNullException.ThrowIfNull(smsRecipient); - - // If the recipient does not contain the recipient name placeholder, we do not need to look up the person name. - if (!ContainsRecipientNamePlaceholder(smsRecipient.CustomizedBody)) - { - return smsRecipient; - } - - // If the recipient does not have an person number, we do not need to look up the person name. - if (string.IsNullOrWhiteSpace(smsRecipient.NationalIdentityNumber)) - { - return smsRecipient; - } - - // Look up the person name and replace the recipient name placeholder with the person name. - var partyDetails = await _registerClient.GetPartyDetailsForPersons([smsRecipient.NationalIdentityNumber]); - if (partyDetails == null || partyDetails.Count == 0) - { - return smsRecipient; - } - - if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) - { - smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - return smsRecipient; - } - - /// - /// Injects the recipient's name wherever the $recipientName$ placeholder is found. + /// Replaces placeholder keywords with actual values. /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private async Task InjectPersonNameAsync(EmailRecipient emailRecipient) + /// The type of the recipient. + /// The recipient to process. + /// A function to get the body of the recipient. + /// A function to set the body of the recipient. + /// A function to get the national identity number of the recipient. + /// A function to get the organization number of the recipient. + /// A task that represents the asynchronous operation. The task result contains the processed recipient. + private async Task ReplaceKeywordsAsync(T recipient, Func getBody, Action setBody, Func nationalIdentityNumberGetter, Func organizationNumberGetter) { - ArgumentNullException.ThrowIfNull(emailRecipient); - - // If the recipient does not contain the recipient name placeholder, we do not need to look up the person name. - bool containsRecipientNamePlaceholder = ContainsRecipientNamePlaceholder(emailRecipient.CustomizedBody) || - ContainsRecipientNamePlaceholder(emailRecipient.CustomizedSubject); - if (!containsRecipientNamePlaceholder) - { - return emailRecipient; - } - - // If the recipient does not have an person number, we do not need to look up the person name. - if (string.IsNullOrWhiteSpace(emailRecipient.NationalIdentityNumber)) - { - return emailRecipient; - } - - // Look up the person name and replace the recipient name placeholder with the person name. - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([emailRecipient.NationalIdentityNumber]); - if (partyDetails == null || partyDetails.Count == 0) - { - return emailRecipient; - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) - { - emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) - { - emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - return emailRecipient; - } - - /// - /// Injects the recipient's national identity number into the SMS where the $recipientNumber$ placeholder is found. - /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private SmsRecipient InjectNationalIdentityNumbers(SmsRecipient smsRecipient) - { - ArgumentNullException.ThrowIfNull(smsRecipient); - - if (!ContainsRecipientNumberPlaceholder(smsRecipient.CustomizedBody)) - { - return smsRecipient; - } - - if (string.IsNullOrWhiteSpace(smsRecipient.NationalIdentityNumber)) - { - return smsRecipient; - } - - if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) - { - smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, smsRecipient.NationalIdentityNumber ?? string.Empty); - } - - return smsRecipient; - } - - /// - /// Injects the recipient's national identity number wherever the $recipientNumber$ placeholder is found. - /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private EmailRecipient InjectNationalIdentityNumbers(EmailRecipient emailRecipient) - { - ArgumentNullException.ThrowIfNull(emailRecipient); - - bool containsRecipientNumberPlaceholder = ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedBody) || - ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedSubject); - if (!containsRecipientNumberPlaceholder) - { - return emailRecipient; - } - - if (string.IsNullOrWhiteSpace(emailRecipient.NationalIdentityNumber)) - { - return emailRecipient; - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) - { - emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, emailRecipient.NationalIdentityNumber ?? string.Empty); - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) - { - emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNumberPlaceholder, emailRecipient.NationalIdentityNumber ?? string.Empty); - } - - return emailRecipient; - } - - /// - /// Injects the recipient's organization number wherever the $recipientNumber$ placeholder is found. - /// - /// The . - /// The updated . - /// Thrown if is null. - private SmsRecipient InjectOrganizationNumbers(SmsRecipient smsRecipient) - { - ArgumentNullException.ThrowIfNull(smsRecipient); - - if (!ContainsRecipientNumberPlaceholder(smsRecipient.CustomizedBody)) - { - return smsRecipient; - } - - if (string.IsNullOrWhiteSpace(smsRecipient.OrganizationNumber)) - { - return smsRecipient; - } - - if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) - { - smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, smsRecipient.OrganizationNumber ?? string.Empty); - } - - return smsRecipient; - } - - /// - /// Injects the recipient's organization number wherever the $recipientNumber$ placeholder is found. - /// - /// The list of . - /// The updated list of . - /// Thrown if is null. - private EmailRecipient InjectOrganizationNumbers(EmailRecipient emailRecipient) - { - ArgumentNullException.ThrowIfNull(emailRecipient); - - bool containsRecipientNumberPlaceholder = ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedBody) || - ContainsRecipientNumberPlaceholder(emailRecipient.CustomizedSubject); - if (!containsRecipientNumberPlaceholder) - { - return emailRecipient; - } - - if (string.IsNullOrWhiteSpace(emailRecipient.OrganizationNumber)) - { - return emailRecipient; - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) - { - emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNumberPlaceholder, emailRecipient.OrganizationNumber ?? string.Empty); - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) - { - emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNumberPlaceholder, emailRecipient.OrganizationNumber ?? string.Empty); - } - - return emailRecipient; - } - - /// - /// Injects the recipient's organization name wherever the $recipientName$ placeholder is found. - /// - /// The . - /// The updated . - /// Thrown if is null. - private async Task InjectOrganizationNameAsync(SmsRecipient smsRecipient) - { - ArgumentNullException.ThrowIfNull(smsRecipient); - - // If the recipient does not contain the recipient name placeholder, we do not need to look up the organization name. - if (!ContainsRecipientNamePlaceholder(smsRecipient.CustomizedBody)) - { - return smsRecipient; - } - - // If the recipient does not have an organization number, we do not need to look up the organization name. - if (string.IsNullOrWhiteSpace(smsRecipient.OrganizationNumber)) - { - return smsRecipient; - } - - // Look up the organization name and replace the recipient name placeholder with the organization name. - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([smsRecipient.OrganizationNumber]); - if (partyDetails == null || partyDetails.Count == 0) - { - return smsRecipient; - } - - if (!string.IsNullOrWhiteSpace(smsRecipient.CustomizedBody)) - { - smsRecipient.CustomizedBody = smsRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - return smsRecipient; - } - - /// - /// Injects the recipient's organization name wherever the $recipientName$ placeholder is found. - /// - /// The . - /// The updated . - /// Thrown if is null. - private async Task InjectOrganizationNameAsync(EmailRecipient emailRecipient) - { - ArgumentNullException.ThrowIfNull(emailRecipient); - - // If the recipient does not contain the recipient name placeholder, we do not need to look up the organization name. - bool containsRecipientNamePlaceholder = ContainsRecipientNamePlaceholder(emailRecipient.CustomizedBody) || - ContainsRecipientNamePlaceholder(emailRecipient.CustomizedSubject); - if (!containsRecipientNamePlaceholder) - { - return emailRecipient; - } - - // If the recipient does not have an organization number, we do not need to look up the organization name. - if (string.IsNullOrWhiteSpace(emailRecipient.OrganizationNumber)) - { - return emailRecipient; - } - - // Look up the organization name and replace the recipient name placeholder with the organization name. - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([emailRecipient.OrganizationNumber]); - if (partyDetails == null || partyDetails.Count == 0) - { - return emailRecipient; - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedBody)) - { - emailRecipient.CustomizedBody = emailRecipient.CustomizedBody.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - if (!string.IsNullOrWhiteSpace(emailRecipient.CustomizedSubject)) - { - emailRecipient.CustomizedSubject = emailRecipient.CustomizedSubject.Replace(_recipientNamePlaceholder, partyDetails[0].Name ?? string.Empty); - } - - return emailRecipient; + if (ContainsRecipientNamePlaceholder(getBody(recipient))) + { + var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + { + var partyDetails = await _registerClient.GetPartyDetailsForPersons([nationalIdentityNumber]); + if (partyDetails != null && partyDetails.Count > 0) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); + } + } + + var organizationNumber = organizationNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(organizationNumber)) + { + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([organizationNumber]); + if (partyDetails != null && partyDetails.Count > 0) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); + } + } + } + + if (ContainsRecipientNumberPlaceholder(getBody(recipient))) + { + var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, nationalIdentityNumber)); + } + + var organizationNumber = organizationNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(organizationNumber)) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, organizationNumber)); + } + } + + return recipient; } } } From 2bb81022430708c0ddbc10cfd837042eed1d1ab8 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 14:51:15 +0100 Subject: [PATCH 24/75] Replace Regex checks with string.Contains --- .../Services/KeywordsService.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index ef5ef1a0..fb19b1fd 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -1,6 +1,4 @@ -using System.Text.RegularExpressions; - -using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Integrations; using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Services.Interfaces; @@ -16,12 +14,6 @@ public class KeywordsService : IKeywordsService private const string _recipientNamePlaceholder = "$recipientName$"; private const string _recipientNumberPlaceholder = "$recipientNumber$"; - private static readonly Lazy _recipientNamePlaceholderRegex = - new(() => new Regex(Regex.Escape(_recipientNamePlaceholder), RegexOptions.Compiled | RegexOptions.CultureInvariant)); - - private static readonly Lazy _recipientNumberPlaceholderRegex = - new(() => new Regex(Regex.Escape(_recipientNumberPlaceholder), RegexOptions.Compiled | RegexOptions.CultureInvariant)); - /// /// Initializes a new instance of the class. /// @@ -35,13 +27,13 @@ public KeywordsService(IRegisterClient registerClient) /// public bool ContainsRecipientNamePlaceholder(string? value) { - return !string.IsNullOrWhiteSpace(value) && _recipientNamePlaceholderRegex.Value.IsMatch(value); + return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); } /// public bool ContainsRecipientNumberPlaceholder(string? value) { - return !string.IsNullOrWhiteSpace(value) && _recipientNumberPlaceholderRegex.Value.IsMatch(value); + return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); } /// From a4fb9d58e35425f7f6906d63388d34ecc52a1d1d Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 15:54:06 +0100 Subject: [PATCH 25/75] Improve the request that we use to retrieve unit details. --- .../Register/RegisterClient.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index 7109d7f3..96d13cab 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -18,8 +18,8 @@ public class RegisterClient : IRegisterClient { private readonly HttpClient _client; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly string _nameComponentsLookupEndpoint = "parties/nameslookup"; private readonly string _contactPointLookupEndpoint = "organizations/contactpoint/lookup"; + private readonly string _nameComponentsLookupEndpoint = "parties/nameslookup"; /// /// Initializes a new instance of the class. @@ -39,7 +39,7 @@ public RegisterClient(HttpClient client, IOptions settings) /// A collection of organization numbers for which contact point details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the contact points of the specified organizations. + /// The task result contains a list of representing the contact points of the specified organizations. /// public async Task> GetOrganizationContactPoints(List organizationNumbers) { @@ -73,7 +73,7 @@ public async Task> GetOrganizationContactPoints( /// A collection of organization numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified organizations. + /// The task result contains a list of representing the details of the specified organizations. /// public async Task> GetPartyDetailsForOrganizations(List organizationNumbers) { @@ -107,7 +107,7 @@ public async Task> GetPartyDetailsForOrganizations(ListA collection of social security numbers for which party details are requested. /// /// A task that represents the asynchronous operation. - /// The task result contains a list of representing the details of the specified individuals. + /// The task result contains a list of representing the details of the specified individuals. /// public async Task> GetPartyDetailsForPersons(List socialSecurityNumbers) { @@ -123,7 +123,13 @@ public async Task> GetPartyDetailsForPersons(List soc HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); - var response = await _client.PostAsync($"{_nameComponentsLookupEndpoint}?partyComponentOption=person-name", content); + var request = new HttpRequestMessage(HttpMethod.Post, $"{_nameComponentsLookupEndpoint}") + { + Content = content + }; + request.Headers.Add("partyComponentOption", "person-name"); + + var response = await _client.SendAsync(request); if (!response.IsSuccessStatusCode) { From 300363073513277210ba2bbfcad187368c36e387 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:04:13 +0100 Subject: [PATCH 26/75] Add validation logic to ensure that either organizationNumber or socialSecurityNumber is set at a time. --- .../Parties/PartyDetailsLookupRequest.cs | 26 ++++++++++++++++--- .../Register/RegisterClient.cs | 4 +-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs index 1fa3bf14..8f04dde5 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs @@ -8,18 +8,36 @@ namespace Altinn.Notifications.Core.Models.Parties; public record PartyDetailsLookupRequest { /// - /// Gets or sets the organization number of the party. + /// Initializes a new instance of the class. + /// Ensures that only one of or is set. + /// + /// The organization number of the party. + /// The social security number of the party. + /// Thrown when both and are set. + public PartyDetailsLookupRequest(string? organizationNumber = null, string? socialSecurityNumber = null) + { + if (!string.IsNullOrEmpty(organizationNumber) && !string.IsNullOrEmpty(socialSecurityNumber)) + { + throw new ArgumentException("Only one of OrganizationNumber or SocialSecurityNumber can be set."); + } + + OrganizationNumber = organizationNumber; + SocialSecurityNumber = socialSecurityNumber; + } + + /// + /// Gets the organization number of the party. /// /// The organization number of the party. [JsonPropertyName("orgNo")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? OrganizationNumber { get; init; } + public string? OrganizationNumber { get; } /// - /// Gets or sets the social security number of the party. + /// Gets the social security number of the party. /// /// The social security number of the party. [JsonPropertyName("ssn")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SocialSecurityNumber { get; init; } + public string? SocialSecurityNumber { get; } } diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index 96d13cab..bcb492af 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -84,7 +84,7 @@ public async Task> GetPartyDetailsForOrganizations(List new PartyDetailsLookupRequest { OrganizationNumber = orgNumber }).ToList() + PartyDetailsLookupRequestList = organizationNumbers.Select(orgNumber => new PartyDetailsLookupRequest(organizationNumber: orgNumber)).ToList() }; HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); @@ -118,7 +118,7 @@ public async Task> GetPartyDetailsForPersons(List soc var partyDetailsLookupBatch = new PartyDetailsLookupBatch { - PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest { SocialSecurityNumber = ssn }).ToList() + PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn)).ToList() }; HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); From 0f3bd1c19a6da9c1e60cda52899f948325b1ebe8 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:13:13 +0100 Subject: [PATCH 27/75] Added a constructor for batch creation from existing lists to simplify usage in client code. --- .../Models/Parties/PartyDetailsLookupBatch.cs | 28 ++++++++++++++++++- .../Parties/PartyDetailsLookupResult.cs | 2 +- .../Models/Parties/PersonNameComponents.cs | 2 +- .../Register/RegisterClient.cs | 10 ++----- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs index 490eee27..e8221066 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -7,9 +7,35 @@ namespace Altinn.Notifications.Core.Models.Parties; /// public class PartyDetailsLookupBatch { + /// + /// Initializes a new instance of the class. + /// + /// A list of organization numbers to look up. + /// A list of social security numbers to look up. + /// Thrown when both and are null or empty. + public PartyDetailsLookupBatch(List? organizationNumbers = null, List? socialSecurityNumbers = null) + { + if ((organizationNumbers == null || organizationNumbers.Count == 0) && (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0)) + { + throw new ArgumentException("At least one of organizationNumbers or socialSecurityNumbers must be provided."); + } + + PartyDetailsLookupRequestList = []; + + if (organizationNumbers != null) + { + PartyDetailsLookupRequestList.AddRange(organizationNumbers.Select(orgNumber => new PartyDetailsLookupRequest(organizationNumber: orgNumber))); + } + + if (socialSecurityNumbers != null) + { + PartyDetailsLookupRequestList.AddRange(socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn))); + } + } + /// /// Gets or sets the list of lookup criteria for parties. /// [JsonPropertyName("parties")] - public List? PartyDetailsLookupRequestList { get; set; } + public List? PartyDetailsLookupRequestList { get; } } diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs index 3b99eff6..47e89ff3 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupResult.cs @@ -11,5 +11,5 @@ public class PartyDetailsLookupResult /// Gets or sets the list of party details. /// [JsonPropertyName("partyNames")] - public List? PartyDetailsList { get; set; } + public List? PartyDetailsList { get; set; } = []; } diff --git a/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs b/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs index 8365ff66..fd41be2d 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs @@ -16,7 +16,7 @@ public record PersonNameComponents public string? MiddleName { get; init; } /// - /// Gets the sure name. + /// Gets the surname. /// public string? LastName { get; init; } } diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index bcb492af..38bd3b00 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -82,10 +82,7 @@ public async Task> GetPartyDetailsForOrganizations(List new PartyDetailsLookupRequest(organizationNumber: orgNumber)).ToList() - }; + var partyDetailsLookupBatch = new PartyDetailsLookupBatch(organizationNumbers: organizationNumbers); HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); @@ -116,10 +113,7 @@ public async Task> GetPartyDetailsForPersons(List soc return []; } - var partyDetailsLookupBatch = new PartyDetailsLookupBatch - { - PartyDetailsLookupRequestList = socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn)).ToList() - }; + var partyDetailsLookupBatch = new PartyDetailsLookupBatch(socialSecurityNumbers: socialSecurityNumbers); HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); From 7b49cbbec0261b860b1a53a17cd5ac0baf537118 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:42:25 +0100 Subject: [PATCH 28/75] Update the function that is used to retrieve emails --- src/Altinn.Notifications.Core/Models/Email.cs | 20 +------------------ .../getemailsstatusnewupdatestatus.sql | 8 ++++---- .../Repository/EmailNotificationRepository.cs | 4 +--- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Email.cs b/src/Altinn.Notifications.Core/Models/Email.cs index 81514a64..dcab68d8 100644 --- a/src/Altinn.Notifications.Core/Models/Email.cs +++ b/src/Altinn.Notifications.Core/Models/Email.cs @@ -34,22 +34,6 @@ public class Email /// public string ToAddress { get; set; } - /// - /// Gets or sets the national identity number. - /// - /// - /// The national identity number. - /// - public string NationalIdentityNumber { get; set; } - - /// - /// Gets or sets the organization number. - /// - /// - /// The organization number. - /// - public string OrganizationNumber { get; set; } - /// /// Gets or sets the content type of the email. /// @@ -58,7 +42,7 @@ public class Email /// /// Initializes a new instance of the class. /// - public Email(Guid notificationId, string subject, string body, string fromAddress, string toAddress, EmailContentType contentType, string nationalIdentityNumber, string organizationNumber) + public Email(Guid notificationId, string subject, string body, string fromAddress, string toAddress, EmailContentType contentType) { NotificationId = notificationId; Subject = subject; @@ -66,8 +50,6 @@ public Email(Guid notificationId, string subject, string body, string fromAddres FromAddress = fromAddress; ToAddress = toAddress; ContentType = contentType; - NationalIdentityNumber = nationalIdentityNumber; - OrganizationNumber = organizationNumber; } /// diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql index d857e2cd..edd1b235 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql @@ -1,5 +1,5 @@ CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text, recipientorgno text, recipientnin text) + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) LANGUAGE 'plpgsql' AS $BODY$ DECLARE @@ -8,7 +8,7 @@ BEGIN SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); IF latest_email_timeout IS NOT NULL THEN IF latest_email_timeout >= NOW() THEN - RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype, NULL::text AS recipientorgno, NULL::text AS recipientnin WHERE FALSE; + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; RETURN; ELSE UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); @@ -20,8 +20,8 @@ BEGIN UPDATE notifications.emailnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.recipientorgno, notifications.emailnotifications.recipientnin) - SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype, u.recipientorgno, u.recipientnin + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.customizedsubject, notifications.emailnotifications.customizedbody) + SELECT u.alternateid, CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, et.fromaddress, u.toaddress, et.contenttype FROM updated u, notifications.emailtexts et WHERE u._orderid = et._orderid; END; diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 03a028bc..ace06084 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -82,9 +82,7 @@ public async Task> GetNewNotifications() reader.GetValue("body"), reader.GetValue("fromaddress"), reader.GetValue("toaddress"), - emailContentType, - reader.GetValue("recipientnin"), - reader.GetValue("recipientorgno")); + emailContentType); searchResult.Add(email); } From 602b804d3e7713aa22916e5230793d68ce5ef791 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:44:56 +0100 Subject: [PATCH 29/75] Fix build errors --- .../Notifications.Core/TestingModels/EmailTests.cs | 2 +- .../TestingServices/EmailNotificationServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs index 5f910ee2..754fde84 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/EmailTests.cs @@ -16,7 +16,7 @@ public class EmailTests public EmailTests() { Guid id = Guid.NewGuid(); - _email = new Email(id, "subject", "body", "from@domain.com", "to@domain.com", EmailContentType.Html, "Test organization number", "Test national identity number"); + _email = new Email(id, "subject", "body", "from@domain.com", "to@domain.com", EmailContentType.Html); _serializedEmail = new JsonObject() { { "notificationId", id }, diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index e5e8e6cc..1dfc7996 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class EmailNotificationServiceTests { private const string _emailQueueTopicName = "email.queue"; - private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", EmailContentType.Plain, "18874198354", "313441571"); + private readonly Email _email = new(Guid.NewGuid(), "email.subject", "email.body", "from@domain.com", "to@domain.com", EmailContentType.Plain); [Fact] public async Task SendNotifications_ProducerCalledOnceForEachRetrievedEmail() From dfb5241112ac77f16261de7286f6188552e17931 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:53:02 +0100 Subject: [PATCH 30/75] Pass a default value for the new parameters --- .../v0.36/01-functions-and-procedures.sql | 475 +++++++++++++++++- .../EmailOrderProcessingServiceTests.cs | 18 +- .../SmsOrderProcessingServiceTests.cs | 15 +- 3 files changed, 492 insertions(+), 16 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql index 5f088eea..966ba813 100644 --- a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql @@ -1 +1,474 @@ --- This script is autogenerated from the tool DbTools. Do not edit manually. \ No newline at end of file +-- This script is autogenerated from the tool DbTools. Do not edit manually. + +-- cancelorder.sql: +CREATE OR REPLACE FUNCTION notifications.cancelorder( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + cancelallowed boolean, + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + ignorereservation boolean, + resourceid text, + conditionendpoint text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE plpgsql +AS $$ +DECLARE + order_record RECORD; +BEGIN + -- Retrieve the order and its status + SELECT o.requestedsendtime, o.processedstatus + INTO order_record + FROM notifications.orders o + WHERE o.alternateid = _alternateid AND o.creatorname = _creatorname; + + -- If no order is found, return an empty result set + IF NOT FOUND THEN + RETURN; + END IF; + + -- Check if order is already cancelled + IF order_record.processedstatus = 'Cancelled' THEN + RETURN QUERY + SELECT TRUE AS cancelallowed, + order_details.* + FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; + ELSEIF (order_record.requestedsendtime <= NOW() + INTERVAL '5 minutes' or order_record.processedstatus != 'Registered') THEN + RETURN QUERY + SELECT FALSE AS cancelallowed, NULL::uuid, NULL::text, NULL::text, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::orderprocessingstate, NULL::text, NULL::boolean, NULL::text, NULL::text, NULL::bigint, NULL::bigint, NULL::bigint, NULL::bigint; + ELSE + -- Cancel the order by updating its status + UPDATE notifications.orders + SET processedstatus = 'Cancelled', processed = NOW() + WHERE notifications.orders.alternateid = _alternateid; + + -- Retrieve the updated order details + RETURN QUERY + SELECT TRUE AS cancelallowed, + order_details.* + FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; + END IF; +END; +$$; + + +-- getemailrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + toaddress text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _alternateid); +BEGIN +RETURN query + SELECT e.recipientorgno, e.recipientnin, e.toaddress + FROM notifications.emailnotifications e + WHERE e._orderid = __orderid; +END; +$BODY$; + +-- getemailsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + latest_email_timeout TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN query + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.customizedsubject, notifications.emailnotifications.customizedbody) + SELECT u.alternateid, CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, et.fromaddress, u.toaddress, et.contenttype + FROM updated u, notifications.emailtexts et + WHERE u._orderid = et._orderid; +END; +$BODY$; + +-- getemailsummary.sql: +CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + toaddress text, + result emailnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime + FROM notifications.emailnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- getmetrics.sql: +CREATE OR REPLACE FUNCTION notifications.getmetrics( + month_input int, + year_input int +) +RETURNS TABLE ( + org text, + placed_orders bigint, + sent_emails bigint, + succeeded_emails bigint, + sent_sms bigint, + succeeded_sms bigint +) +AS $$ +BEGIN + RETURN QUERY + SELECT + o.creatorname, + COUNT(DISTINCT o._id) AS placed_orders, + SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails, + SUM(CASE WHEN e.result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END) AS succeeded_emails, + SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms, + SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms + FROM notifications.orders o + LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid + LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid + WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input + AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input + GROUP BY o.creatorname; +END; +$$ LANGUAGE plpgsql; + + +-- getorderincludestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v4( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + ignorereservation boolean, + resourceid text, + conditionendpoint text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + _target_orderid INTEGER; + _succeededEmailCount BIGINT; + _generatedEmailCount BIGINT; + _succeededSmsCount BIGINT; + _generatedSmsCount BIGINT; +BEGIN + SELECT _id INTO _target_orderid + FROM notifications.orders + WHERE orders.alternateid = _alternateid + AND orders.creatorname = _creatorname; + + SELECT + SUM(CASE WHEN result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END), + COUNT(1) AS generatedEmailCount + INTO _succeededEmailCount, _generatedEmailCount + FROM notifications.emailnotifications + WHERE _orderid = _target_orderid; + + SELECT + SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END), + COUNT(1) AS generatedSmsCount + INTO _succeededSmsCount, _generatedSmsCount + FROM notifications.smsnotifications + WHERE _orderid = _target_orderid; + + RETURN QUERY + SELECT + orders.alternateid, + orders.creatorname, + orders.sendersreference, + orders.created, + orders.requestedsendtime, + orders.processed, + orders.processedstatus, + orders.notificationorder->>'NotificationChannel', + CASE + WHEN orders.notificationorder->>'IgnoreReservation' IS NULL THEN NULL + ELSE (orders.notificationorder->>'IgnoreReservation')::BOOLEAN + END AS IgnoreReservation, + orders.notificationorder->>'ResourceId', + orders.notificationorder->>'ConditionEndpoint', + _generatedEmailCount, + _succeededEmailCount, + _generatedSmsCount, + _succeededSmsCount + FROM + notifications.orders AS orders + WHERE + orders.alternateid = _alternateid; +END; +$BODY$; + + +-- getorderspastsendtimeupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus() + RETURNS TABLE(notificationorders jsonb) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +RETURN QUERY + UPDATE notifications.orders + SET processedstatus = 'Processing' + WHERE _id IN (select _id + from notifications.orders + where processedstatus = 'Registered' + and requestedsendtime <= now() + INTERVAL '1 minute' + limit 50) + RETURNING notificationorder AS notificationorders; +END; +$BODY$; + +-- getsmsrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + mobilenumber text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN +RETURN query + SELECT s.recipientorgno, s.recipientnin, s.mobilenumber + FROM notifications.smsnotifications s + WHERE s._orderid = __orderid; +END; +$BODY$; + +-- getsmsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text, recipientorgno text, recipientnin text) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + RETURN query + WITH updated AS ( + UPDATE notifications.smsnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber, notifications.smsnotifications.recipientorgno, notifications.smsnotifications.recipientnin) + SELECT u.alternateid, st.sendernumber, u.mobilenumber, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body, u.recipientorgno, u.recipientnin + FROM updated u, notifications.smstexts st + WHERE u._orderid = st._orderid; +END; +$BODY$; + +-- getsmssummary.sql: +CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + mobilenumber text, + result smsnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime + FROM notifications.smsnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- insertemailnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_toaddress TEXT, +_customizedbody TEXT, +_customizedsubject TEXT, +_result TEXT, +_resulttime timestamptz, +_expirytime timestamptz) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.emailnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +toaddress, +customizedBody, +customizedSubject, +result, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_toaddress, +_customizedbody, +_customizedsubject, +_result::emailnotificationresulttype, +_resulttime, +_expirytime); +END; +$BODY$; + +-- insertemailtext.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype) + VALUES (__orderid, _fromaddress, _subject, _body, _contenttype); +END; +$BODY$; + + +-- insertorder.sql: +CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB) +RETURNS BIGINT + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +_orderid BIGINT; +BEGIN + INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder) + VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder) + RETURNING _id INTO _orderid; + + RETURN _orderid; +END; +$BODY$; + +-- insertsmsnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_mobilenumber TEXT, +_customizedbody TEXT, +_result text, +_smscount integer, +_resulttime timestamptz, +_expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.smsnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +mobilenumber, +customizedbody, +result, +smscount, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_mobilenumber, +_customizedbody, +_result::smsnotificationresulttype, +_smscount, +_resulttime, +_expirytime); +END; +$BODY$; + +-- updateemailstatus.sql: +CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + UPDATE notifications.emailnotifications + SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid + WHERE alternateid = _alternateid; +END; +$BODY$; + diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index 3a03818a..02ed6238 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -44,7 +44,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var service = GetTestService(emailService: serviceMock.Object); @@ -52,7 +52,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() await service.ProcessOrder(order); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -76,7 +76,7 @@ public async Task ProcessOrder_ExpectedInputToNotificationService() Recipient expectedRecipient = new(new List() { new EmailAddressPoint("test@test.com") }, organizationNumber: "skd-orgno"); var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny(), It.IsAny(), It.IsAny())); var service = GetTestService(emailService: serviceMock.Object); @@ -101,7 +101,7 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception()); var repoMock = new Mock(); @@ -113,7 +113,7 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC await Assert.ThrowsAsync(async () => await service.ProcessOrder(order)); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); } @@ -141,7 +141,9 @@ public async Task ProcessOrder_RecipientMissingEmail_ContactPointServiceCalled() It.IsAny(), It.IsAny(), It.Is(r => r.NationalIdentityNumber == "123456"), - It.IsAny())); + It.IsAny(), + It.IsAny(), + It.IsAny())); var contactPointServiceMock = new Mock(); contactPointServiceMock.Setup(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1), It.IsAny())); @@ -174,7 +176,7 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var emailRepoMock = new Mock(); emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() @@ -190,7 +192,7 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() // Assert emailRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } private static EmailOrderProcessingService GetTestService( diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs index c53b205d..81c28ef2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs @@ -44,7 +44,7 @@ public async Task ProcessOrder_ExpectedInputToService() Recipient expectedRecipient = new(new List() { new SmsAddressPoint("+4799999999") }, nationalIdentityNumber: "enduser-nin"); var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny(), It.IsAny(), It.IsAny())); var service = GetTestService(smsService: notificationServiceMock.Object); @@ -80,7 +80,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() }; var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var service = GetTestService(smsService: notificationServiceMock.Object); @@ -88,7 +88,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() await service.ProcessOrder(order); // Assert - notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -116,7 +116,8 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC It.IsAny(), It.Is(r => r.NationalIdentityNumber == "123456"), It.IsAny(), - It.IsAny())); + It.IsAny(), + It.IsAny())); var contactPointServiceMock = new Mock(); contactPointServiceMock.Setup(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1), It.IsAny())) @@ -124,7 +125,7 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC { Recipient augumentedRecipient = new() { AddressInfo = [new SmsAddressPoint("+4712345678")], NationalIdentityNumber = r[0].NationalIdentityNumber }; r.Clear(); - r.Add(augumentedRecipient); + r.Add(augumentedRecipient); }); var service = GetTestService(smsService: notificationServiceMock.Object, contactPointService: contactPointServiceMock.Object); @@ -156,7 +157,7 @@ public async Task ProcessOrderRetry_NotificationServiceCalledIfRecipientNotInDat }; var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var smsRepoMock = new Mock(); smsRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync( @@ -172,7 +173,7 @@ public async Task ProcessOrderRetry_NotificationServiceCalledIfRecipientNotInDat // Assert smsRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); - notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Theory] From 52b12b3132cb71ea6197dc2e1205491d1c9fba53 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:57:34 +0100 Subject: [PATCH 31/75] Breaking a large method into smaller, more focused private methods --- .../Services/KeywordsService.cs | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index fb19b1fd..447eab3c 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -25,18 +25,33 @@ public KeywordsService(IRegisterClient registerClient) } /// + /// + /// Checks whether the specified string contains the placeholder keyword $recipientName$. + /// + /// The string to check. + /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. public bool ContainsRecipientNamePlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); } /// + /// + /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. + /// + /// The string to check. + /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. public bool ContainsRecipientNumberPlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); } /// + /// + /// Replaces placeholder keywords in an with actual values. + /// + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. public async Task ReplaceKeywordsAsync(SmsRecipient smsRecipient) { ArgumentNullException.ThrowIfNull(smsRecipient); @@ -47,6 +62,11 @@ public async Task ReplaceKeywordsAsync(SmsRecipient smsRecipient) } /// + /// + /// Replaces placeholder keywords in an with actual values. + /// + /// The to process. + /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. public async Task ReplaceKeywordsAsync(EmailRecipient emailRecipient) { ArgumentNullException.ThrowIfNull(emailRecipient); @@ -67,47 +87,91 @@ public async Task ReplaceKeywordsAsync(EmailRecipient emailRecip /// A function to get the national identity number of the recipient. /// A function to get the organization number of the recipient. /// A task that represents the asynchronous operation. The task result contains the processed recipient. - private async Task ReplaceKeywordsAsync(T recipient, Func getBody, Action setBody, Func nationalIdentityNumberGetter, Func organizationNumberGetter) + private async Task ReplaceKeywordsAsync( + T recipient, + Func getBody, + Action setBody, + Func nationalIdentityNumberGetter, + Func organizationNumberGetter) { if (ContainsRecipientNamePlaceholder(getBody(recipient))) { - var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) - { - var partyDetails = await _registerClient.GetPartyDetailsForPersons([nationalIdentityNumber]); - if (partyDetails != null && partyDetails.Count > 0) - { - setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); - } - } - - var organizationNumber = organizationNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(organizationNumber)) - { - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations([organizationNumber]); - if (partyDetails != null && partyDetails.Count > 0) - { - setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); - } - } + await ReplaceRecipientNamePlaceholderAsync(recipient, getBody, setBody, nationalIdentityNumberGetter, organizationNumberGetter); } if (ContainsRecipientNumberPlaceholder(getBody(recipient))) { - var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + ReplaceRecipientNumberPlaceholder(recipient, getBody, setBody, nationalIdentityNumberGetter, organizationNumberGetter); + } + + return recipient; + } + + /// + /// Replaces the recipient name placeholder with the actual recipient name. + /// + /// The type of the recipient. + /// The recipient to process. + /// A function to get the body of the recipient. + /// A function to set the body of the recipient. + /// A function to get the national identity number of the recipient. + /// A function to get the organization number of the recipient. + /// A task that represents the asynchronous operation. + private async Task ReplaceRecipientNamePlaceholderAsync( + T recipient, + Func getBody, + Action setBody, + Func nationalIdentityNumberGetter, + Func organizationNumberGetter) + { + var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + { + var partyDetails = await _registerClient.GetPartyDetailsForPersons(new List { nationalIdentityNumber }); + if (partyDetails != null && partyDetails.Count > 0) { - setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, nationalIdentityNumber)); + setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); } + } - var organizationNumber = organizationNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(organizationNumber)) + var organizationNumber = organizationNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(organizationNumber)) + { + var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(new List { organizationNumber }); + if (partyDetails != null && partyDetails.Count > 0) { - setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, organizationNumber)); + setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); } } + } - return recipient; + /// + /// Replaces the recipient number placeholder with the actual recipient number. + /// + /// The type of the recipient. + /// The recipient to process. + /// A function to get the body of the recipient. + /// A function to set the body of the recipient. + /// A function to get the national identity number of the recipient. + /// A function to get the organization number of the recipient. + private void ReplaceRecipientNumberPlaceholder( + T recipient, + Func getBody, + Action setBody, + Func nationalIdentityNumberGetter, + Func organizationNumberGetter) + { + var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, nationalIdentityNumber)); + } + + var organizationNumber = organizationNumberGetter(recipient); + if (!string.IsNullOrWhiteSpace(organizationNumber)) + { + setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, organizationNumber)); + } } } } From 26728a04612794b99b42ab466c12515ec837fe81 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 28 Nov 2024 16:58:15 +0100 Subject: [PATCH 32/75] Make a method static --- src/Altinn.Notifications.Core/Services/KeywordsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 447eab3c..88683f27 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -154,7 +154,7 @@ private async Task ReplaceRecipientNamePlaceholderAsync( /// A function to set the body of the recipient. /// A function to get the national identity number of the recipient. /// A function to get the organization number of the recipient. - private void ReplaceRecipientNumberPlaceholder( + private static void ReplaceRecipientNumberPlaceholder( T recipient, Func getBody, Action setBody, From 62ee16fc6b3d1562b7a0a1346841369f7048ba18 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 07:17:31 +0100 Subject: [PATCH 33/75] Code refactoring --- .../Models/NotificationTemplate/EmailTemplate.cs | 6 +++--- .../Models/NotificationTemplate/SmsTemplate.cs | 6 +++--- .../Utils/TestdataUtil.cs | 2 -- .../TestingModels/NotificationOrderTests.cs | 1 - 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs index 2d140d45..a8e14f06 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs @@ -3,7 +3,7 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an email notification. +/// Represents a template for an email notification. /// public class EmailTemplate : INotificationTemplate { @@ -31,9 +31,9 @@ public class EmailTemplate : INotificationTemplate /// Gets the type of the notification template. /// /// - /// The type of the notification template, represented by the enum. + /// The type of the notification template, represented by the enum. /// - public NotificationTemplateType Type { get; internal set; } = NotificationTemplateType.Email; + public NotificationTemplateType Type { get; } = NotificationTemplateType.Email; /// /// Initializes a new instance of the class with the specified from address, subject, body, and content type. diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs index 1dfc1ed6..1658cba7 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs @@ -3,7 +3,7 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an SMS notification. +/// Represents a template for an SMS notification. /// public class SmsTemplate : INotificationTemplate { @@ -21,9 +21,9 @@ public class SmsTemplate : INotificationTemplate /// Gets the type of the notification template. /// /// - /// The type of the notification template, represented by the enum. + /// The type of the notification template, represented by the enum. /// - public NotificationTemplateType Type { get; internal set; } = NotificationTemplateType.Sms; + public NotificationTemplateType Type { get; } = NotificationTemplateType.Sms; /// /// Initializes a new instance of the class with the specified sender number and body. diff --git a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs index 3d89995e..3bf535fb 100644 --- a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs +++ b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs @@ -65,7 +65,6 @@ public static NotificationOrder NotificationOrder_EmailTemplate_OneRecipient() { new EmailTemplate() { - Type = NotificationTemplateType.Email, FromAddress = "sender@domain.com", Subject = "email-subject", Body = "email-body", @@ -105,7 +104,6 @@ public static NotificationOrder NotificationOrder_SmsTemplate_OneRecipient() { new SmsTemplate() { - Type = NotificationTemplateType.Sms, Body = "sms-body", SenderNumber = "Altinn local test" } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs index a32a51d2..8cbe32c3 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs @@ -31,7 +31,6 @@ public NotificationOrderTests() { new EmailTemplate() { - Type = NotificationTemplateType.Email, FromAddress = "sender@domain.com", Subject = "email-subject", Body = "email-body", From 3f13092507bf3f8105b32edc2dd744bafeeba520 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 07:22:43 +0100 Subject: [PATCH 34/75] Code refactoring --- .../Models/Recipient.cs | 4 +-- .../Models/Recipients/SmsRecipient.cs | 2 +- src/Altinn.Notifications.Core/Models/Sms.cs | 33 ++++++++++--------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Recipient.cs b/src/Altinn.Notifications.Core/Models/Recipient.cs index 570d51b0..8890b1fe 100644 --- a/src/Altinn.Notifications.Core/Models/Recipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipient.cs @@ -5,7 +5,7 @@ namespace Altinn.Notifications.Core.Models; /// -/// Class representing a notification recipient. +/// Represents a notification recipient. /// public class Recipient { @@ -37,7 +37,7 @@ public Recipient() } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified address information, organization number, and national identity number. /// /// The list of address points for the recipient. /// The recipient's organization number. diff --git a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs index ffc9aa6e..d33433da 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs @@ -1,7 +1,7 @@ namespace Altinn.Notifications.Core.Models.Recipients; /// -/// Class representing an sms recipient +/// Class representing an SMS recipient /// public class SmsRecipient { diff --git a/src/Altinn.Notifications.Core/Models/Sms.cs b/src/Altinn.Notifications.Core/Models/Sms.cs index 859f51fe..b5adc2bc 100644 --- a/src/Altinn.Notifications.Core/Models/Sms.cs +++ b/src/Altinn.Notifications.Core/Models/Sms.cs @@ -3,52 +3,52 @@ namespace Altinn.Notifications.Core.Models; /// -/// Class representing an sms +/// Represents an SMS message. /// public class Sms { /// - /// Gets or sets the id of the sms. + /// Gets or sets the ID of the SMS. /// public Guid NotificationId { get; set; } /// - /// Gets or sets the sender of the sms message + /// Gets or sets the sender of the SMS message. /// /// - /// Can be a literal string or a phone number + /// Can be a literal string or a phone number. /// public string Sender { get; set; } /// - /// Gets or sets the recipient of the sms message + /// Gets or sets the recipient of the SMS message. /// public string Recipient { get; set; } /// - /// Gets or sets the contents of the sms message + /// Gets or sets the contents of the SMS message. /// public string Message { get; set; } /// - /// Gets or sets the national identity number. + /// Gets or sets the national identity number of the recipient. /// - /// - /// The national identity number. - /// public string NationalIdentityNumber { get; set; } /// - /// Gets or sets the organization number. + /// Gets or sets the organization number of the recipient. /// - /// - /// The organization number. - /// public string OrganizationNumber { get; set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified parameters. /// + /// The ID of the SMS. + /// The sender of the SMS message. + /// The recipient of the SMS message. + /// The contents of the SMS message. + /// The national identity number of the recipient. + /// The organization number of the recipient. public Sms(Guid notificationId, string sender, string recipient, string message, string nationalIdentityNumber, string organizationNumber) { NotificationId = notificationId; @@ -60,8 +60,9 @@ public Sms(Guid notificationId, string sender, string recipient, string message, } /// - /// Json serializes the + /// Serializes the object to a JSON string. /// + /// A JSON string representation of the object. public string Serialize() { return JsonSerializer.Serialize(this, JsonSerializerOptionsProvider.Options); From 583d139defc21676011bf3b1f5ef14f98690f7e3 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 07:35:51 +0100 Subject: [PATCH 35/75] Fix typos --- .../Services/EmailNotificationService.cs | 2 +- .../Services/Interfaces/ISmsNotificationService.cs | 8 ++++---- .../Services/Interfaces/ISmsOrderProcessingService.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index 06dfd8a6..25794f1e 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -101,7 +101,7 @@ public async Task SendNotifications() /// public async Task UpdateSendStatus(EmailSendOperationResult sendOperationResult) { - // set to new to allow new iteration of regular proceessing if transient error + // set to new to allow new iteration of regular processing if transient error if (sendOperationResult.SendResult == EmailNotificationResultType.Failed_TransientError) { sendOperationResult.SendResult = EmailNotificationResultType.New; diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs index 101fb79a..af3cb922 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs @@ -4,22 +4,22 @@ namespace Altinn.Notifications.Core.Services.Interfaces; /// -/// Interface for sms notification service +/// Interface for SMS notification service /// public interface ISmsNotificationService { /// - /// Creates a new sms notification based on the provided orderId and recipient + /// Creates a new SMS notification based on the provided orderId and recipient /// public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false, string? body = null); /// - /// Starts the process of sending all ready sms notifications + /// Starts the process of sending all ready SMS notifications /// public Task SendNotifications(); /// - /// Update send status for an sms notification + /// Update send status for an SMS notification /// public Task UpdateSendStatus(SmsSendOperationResult sendOperationResult); } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs index 06ab12f9..927d02fd 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs @@ -4,7 +4,7 @@ namespace Altinn.Notifications.Core.Services.Interfaces; /// -/// Interface for the order processing service speficic to sms orders +/// Interface for the order processing service specific to SMS orders /// public interface ISmsOrderProcessingService { @@ -25,7 +25,7 @@ public interface ISmsOrderProcessingService public Task ProcessOrderRetry(NotificationOrder order); /// - /// Retryprocessing of a notification order for the provided list of recipients + /// Retry processing of a notification order for the provided list of recipients /// without looking up additional recipient data /// public Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients); From 3763f01ce7ee5ff99f6b2cf33533eb0dfabe6fd5 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 07:53:29 +0100 Subject: [PATCH 36/75] Use explicit JOIN instead of the implicit join in the FROM clause and improve indentation for better readability. --- .../getemailsstatusnewupdatestatus.sql | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql index edd1b235..af2fc8da 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql @@ -5,24 +5,47 @@ AS $BODY$ DECLARE latest_email_timeout TIMESTAMP WITH TIME ZONE; BEGIN - SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - IF latest_email_timeout IS NOT NULL THEN - IF latest_email_timeout >= NOW() THEN - RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; - RETURN; - ELSE - UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - END IF; - END IF; - - RETURN query - WITH updated AS ( - UPDATE notifications.emailnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.customizedsubject, notifications.emailnotifications.customizedbody) - SELECT u.alternateid, CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, et.fromaddress, u.toaddress, et.contenttype - FROM updated u, notifications.emailtexts et - WHERE u._orderid = et._orderid; + SELECT emaillimittimeout + INTO latest_email_timeout + FROM notifications.resourcelimitlog + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY + SELECT NULL::uuid AS alternateid, + NULL::text AS subject, + NULL::text AS body, + NULL::text AS fromaddress, + NULL::text AS toaddress, + NULL::text AS contenttype + WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog + SET emaillimittimeout = NULL + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN QUERY + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, + _orderid, + notifications.emailnotifications.toaddress, + notifications.emailnotifications.customizedsubject, + notifications.emailnotifications.customizedbody + ) + SELECT u.alternateid, + CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, + et.fromaddress, + u.toaddress, + et.contenttype + FROM updated u + JOIN notifications.emailtexts et ON u._orderid = et._orderid; END; $BODY$; \ No newline at end of file From d85694467c7d4ebdb1afeacba7aac0125bcc5088 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 07:57:38 +0100 Subject: [PATCH 37/75] Use explicit JOIN instead of the implicit join in the FROM clause and ensure consistent indentation for better readability. --- .../getsmsstatusnewupdatestatus.sql | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql index 7b496819..44a107d8 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql @@ -1,16 +1,23 @@ CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text, recipientorgno text, recipientnin text) + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) LANGUAGE 'plpgsql' AS $BODY$ BEGIN - RETURN query + RETURN QUERY WITH updated AS ( UPDATE notifications.smsnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber, notifications.smsnotifications.recipientorgno, notifications.smsnotifications.recipientnin) - SELECT u.alternateid, st.sendernumber, u.mobilenumber, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body, u.recipientorgno, u.recipientnin - FROM updated u, notifications.smstexts st - WHERE u._orderid = st._orderid; + RETURNING notifications.smsnotifications.alternateid, + _orderid, + notifications.smsnotifications.mobilenumber, + notifications.smsnotifications.customizedbody + ) + SELECT u.alternateid, + st.sendernumber, + u.mobilenumber, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body + FROM updated u + JOIN notifications.smstexts st ON u._orderid = st._orderid; END; $BODY$; \ No newline at end of file From 3ab7891451ce78b4473cce099b1c9a04d545a37b Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:01:45 +0100 Subject: [PATCH 38/75] Use SELECT INTO for assigning values to the __orderid variable and improve indentation for better readability. --- .../insertemailnotification.sql | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql index 80dd8d35..1ebfc055 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql @@ -1,42 +1,44 @@ CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_toaddress TEXT, -_customizedbody TEXT, -_customizedsubject TEXT, -_result TEXT, -_resulttime timestamptz, -_expirytime timestamptz) + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _toaddress TEXT, + _customizedbody TEXT, + _customizedsubject TEXT, + _result TEXT, + _resulttime timestamptz, + _expirytime timestamptz) LANGUAGE 'plpgsql' AS $BODY$ DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); + __orderid BIGINT; BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; -INSERT INTO notifications.emailnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -toaddress, -customizedBody, -customizedSubject, -result, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_toaddress, -_customizedbody, -_customizedsubject, -_result::emailnotificationresulttype, -_resulttime, -_expirytime); + INSERT INTO notifications.emailnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + toaddress, + customizedBody, + customizedSubject, + result, + resulttime, + expirytime) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _toaddress, + _customizedbody, + _customizedsubject, + _result::emailnotificationresulttype, + _resulttime, + _expirytime); END; $BODY$; \ No newline at end of file From e0d494cda72143abf7413e21dde7bfa7c2a23390 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:03:29 +0100 Subject: [PATCH 39/75] Use SELECT INTO for assigning values to the __orderid variable and improve indentation for better readability. --- .../insertsmsnotification.sql | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql index 85d7ec01..b9328b8a 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql @@ -1,43 +1,45 @@ CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_mobilenumber TEXT, -_customizedbody TEXT, -_result text, -_smscount integer, -_resulttime timestamptz, -_expirytime timestamptz + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _mobilenumber TEXT, + _customizedbody TEXT, + _result text, + _smscount integer, + _resulttime timestamptz, + _expirytime timestamptz ) LANGUAGE 'plpgsql' AS $BODY$ DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); + __orderid BIGINT; BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; -INSERT INTO notifications.smsnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -mobilenumber, -customizedbody, -result, -smscount, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_mobilenumber, -_customizedbody, -_result::smsnotificationresulttype, -_smscount, -_resulttime, -_expirytime); + INSERT INTO notifications.smsnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + mobilenumber, + customizedbody, + result, + smscount, + resulttime, + expirytime) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _mobilenumber, + _customizedbody, + _result::smsnotificationresulttype, + _smscount, + _resulttime, + _expirytime); END; $BODY$; \ No newline at end of file From de659f96e7dfeb91df23b476465fdc892c33e126 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:17:57 +0100 Subject: [PATCH 40/75] Add a script to alter tables --- .../FunctionsAndProcedures/insertemailnotification.sql | 4 ++-- .../Migration/v0.36/01-alter-tables.sql | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/01-alter-tables.sql diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql index 1ebfc055..04cdd336 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql @@ -24,8 +24,8 @@ BEGIN recipientorgno, recipientnin, toaddress, - customizedBody, - customizedSubject, + customizedbody, + customizedsubject, result, resulttime, expirytime) diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-alter-tables.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-alter-tables.sql new file mode 100644 index 00000000..49d774a9 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-alter-tables.sql @@ -0,0 +1,8 @@ +-- Modify the table emailnotifications: Add two new columns customizedbody and customizedsubject +ALTER TABLE notifications.emailnotifications +ADD COLUMN IF NOT EXISTS customizedbody text, +ADD COLUMN IF NOT EXISTS customizedsubject text; + +-- Modify table smsnotifications: Add one new column customizedbody +ALTER TABLE notifications.smsnotifications +ADD COLUMN IF NOT EXISTS customizedbody text; From 4fef97b4d84a40fe8d293d73b0aad2167c2eed07 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:33:27 +0100 Subject: [PATCH 41/75] Undo changes made on the auto generated file and move them to new scripts --- .../getemailsstatusnewupdatestatus.sql | 61 +-- .../getsmsstatusnewupdatestatus.sql | 18 +- .../insertemailnotification.sql | 63 +-- .../insertsmsnotification.sql | 67 ++- .../v0.36/01-functions-and-procedures.sql | 474 ------------------ .../Migration/v0.36/02-alter-procedures.sql | 118 +++++ .../Migration/v0.36/03-alter-functions.sql | 79 +++ .../v0.36/04-functions-and-procedures.sql | 1 + 8 files changed, 281 insertions(+), 600 deletions(-) delete mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql index af2fc8da..93856b6e 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql @@ -5,47 +5,24 @@ AS $BODY$ DECLARE latest_email_timeout TIMESTAMP WITH TIME ZONE; BEGIN - SELECT emaillimittimeout - INTO latest_email_timeout - FROM notifications.resourcelimitlog - WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - - IF latest_email_timeout IS NOT NULL THEN - IF latest_email_timeout >= NOW() THEN - RETURN QUERY - SELECT NULL::uuid AS alternateid, - NULL::text AS subject, - NULL::text AS body, - NULL::text AS fromaddress, - NULL::text AS toaddress, - NULL::text AS contenttype - WHERE FALSE; - RETURN; - ELSE - UPDATE notifications.resourcelimitlog - SET emaillimittimeout = NULL - WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - END IF; - END IF; - - RETURN QUERY - WITH updated AS ( - UPDATE notifications.emailnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, - _orderid, - notifications.emailnotifications.toaddress, - notifications.emailnotifications.customizedsubject, - notifications.emailnotifications.customizedbody - ) - SELECT u.alternateid, - CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, - CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, - et.fromaddress, - u.toaddress, - et.contenttype - FROM updated u - JOIN notifications.emailtexts et ON u._orderid = et._orderid; + SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN query + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress) + SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype + FROM updated u, notifications.emailtexts et + WHERE u._orderid = et._orderid; END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql index 44a107d8..91dafe24 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql @@ -3,21 +3,15 @@ CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() LANGUAGE 'plpgsql' AS $BODY$ BEGIN - RETURN QUERY + + RETURN query WITH updated AS ( UPDATE notifications.smsnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, - _orderid, - notifications.smsnotifications.mobilenumber, - notifications.smsnotifications.customizedbody - ) - SELECT u.alternateid, - st.sendernumber, - u.mobilenumber, - CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body - FROM updated u - JOIN notifications.smstexts st ON u._orderid = st._orderid; + RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber) + SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body + FROM updated u, notifications.smstexts st + WHERE u._orderid = st._orderid; END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql index 04cdd336..3e377d3a 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql @@ -1,44 +1,35 @@ CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( - _orderid uuid, - _alternateid uuid, - _recipientorgno TEXT, - _recipientnin TEXT, - _toaddress TEXT, - _customizedbody TEXT, - _customizedsubject TEXT, - _result TEXT, - _resulttime timestamptz, - _expirytime timestamptz) +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_toaddress TEXT, +_result text, +_resulttime timestamptz, +_expirytime timestamptz) LANGUAGE 'plpgsql' AS $BODY$ DECLARE - __orderid BIGINT; +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); BEGIN - SELECT _id INTO __orderid - FROM notifications.orders - WHERE alternateid = _orderid; - INSERT INTO notifications.emailnotifications( - _orderid, - alternateid, - recipientorgno, - recipientnin, - toaddress, - customizedbody, - customizedsubject, - result, - resulttime, - expirytime) - VALUES ( - __orderid, - _alternateid, - _recipientorgno, - _recipientnin, - _toaddress, - _customizedbody, - _customizedsubject, - _result::emailnotificationresulttype, - _resulttime, - _expirytime); +INSERT INTO notifications.emailnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +toaddress, result, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_toaddress, +_result::emailnotificationresulttype, +_resulttime, +_expirytime); END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql index b9328b8a..5f60838c 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql @@ -1,45 +1,40 @@ CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( - _orderid uuid, - _alternateid uuid, - _recipientorgno TEXT, - _recipientnin TEXT, - _mobilenumber TEXT, - _customizedbody TEXT, - _result text, - _smscount integer, - _resulttime timestamptz, - _expirytime timestamptz +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_mobilenumber TEXT, +_result text, +_smscount integer, +_resulttime timestamptz, +_expirytime timestamptz ) LANGUAGE 'plpgsql' AS $BODY$ DECLARE - __orderid BIGINT; +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); BEGIN - SELECT _id INTO __orderid - FROM notifications.orders - WHERE alternateid = _orderid; - INSERT INTO notifications.smsnotifications( - _orderid, - alternateid, - recipientorgno, - recipientnin, - mobilenumber, - customizedbody, - result, - smscount, - resulttime, - expirytime) - VALUES ( - __orderid, - _alternateid, - _recipientorgno, - _recipientnin, - _mobilenumber, - _customizedbody, - _result::smsnotificationresulttype, - _smscount, - _resulttime, - _expirytime); +INSERT INTO notifications.smsnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +mobilenumber, +result, +smscount, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_mobilenumber, +_result::smsnotificationresulttype, +_smscount, +_resulttime, +_expirytime); END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql deleted file mode 100644 index 966ba813..00000000 --- a/src/Altinn.Notifications.Persistence/Migration/v0.36/01-functions-and-procedures.sql +++ /dev/null @@ -1,474 +0,0 @@ --- This script is autogenerated from the tool DbTools. Do not edit manually. - --- cancelorder.sql: -CREATE OR REPLACE FUNCTION notifications.cancelorder( - _alternateid uuid, - _creatorname text -) -RETURNS TABLE( - cancelallowed boolean, - alternateid uuid, - creatorname text, - sendersreference text, - created timestamp with time zone, - requestedsendtime timestamp with time zone, - processed timestamp with time zone, - processedstatus orderprocessingstate, - notificationchannel text, - ignorereservation boolean, - resourceid text, - conditionendpoint text, - generatedemailcount bigint, - succeededemailcount bigint, - generatedsmscount bigint, - succeededsmscount bigint -) -LANGUAGE plpgsql -AS $$ -DECLARE - order_record RECORD; -BEGIN - -- Retrieve the order and its status - SELECT o.requestedsendtime, o.processedstatus - INTO order_record - FROM notifications.orders o - WHERE o.alternateid = _alternateid AND o.creatorname = _creatorname; - - -- If no order is found, return an empty result set - IF NOT FOUND THEN - RETURN; - END IF; - - -- Check if order is already cancelled - IF order_record.processedstatus = 'Cancelled' THEN - RETURN QUERY - SELECT TRUE AS cancelallowed, - order_details.* - FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; - ELSEIF (order_record.requestedsendtime <= NOW() + INTERVAL '5 minutes' or order_record.processedstatus != 'Registered') THEN - RETURN QUERY - SELECT FALSE AS cancelallowed, NULL::uuid, NULL::text, NULL::text, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::orderprocessingstate, NULL::text, NULL::boolean, NULL::text, NULL::text, NULL::bigint, NULL::bigint, NULL::bigint, NULL::bigint; - ELSE - -- Cancel the order by updating its status - UPDATE notifications.orders - SET processedstatus = 'Cancelled', processed = NOW() - WHERE notifications.orders.alternateid = _alternateid; - - -- Retrieve the updated order details - RETURN QUERY - SELECT TRUE AS cancelallowed, - order_details.* - FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; - END IF; -END; -$$; - - --- getemailrecipients.sql: -CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid) -RETURNS TABLE( - recipientorgno text, - recipientnin text, - toaddress text -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _alternateid); -BEGIN -RETURN query - SELECT e.recipientorgno, e.recipientnin, e.toaddress - FROM notifications.emailnotifications e - WHERE e._orderid = __orderid; -END; -$BODY$; - --- getemailsstatusnewupdatestatus.sql: -CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) - LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE - latest_email_timeout TIMESTAMP WITH TIME ZONE; -BEGIN - SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - IF latest_email_timeout IS NOT NULL THEN - IF latest_email_timeout >= NOW() THEN - RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; - RETURN; - ELSE - UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - END IF; - END IF; - - RETURN query - WITH updated AS ( - UPDATE notifications.emailnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress, notifications.emailnotifications.customizedsubject, notifications.emailnotifications.customizedbody) - SELECT u.alternateid, CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, et.fromaddress, u.toaddress, et.contenttype - FROM updated u, notifications.emailtexts et - WHERE u._orderid = et._orderid; -END; -$BODY$; - --- getemailsummary.sql: -CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2( - _alternateorderid uuid, - _creatorname text) - RETURNS TABLE( - sendersreference text, - alternateid uuid, - recipientorgno text, - recipientnin text, - toaddress text, - result emailnotificationresulttype, - resulttime timestamptz) - LANGUAGE 'plpgsql' -AS $BODY$ - - BEGIN - RETURN QUERY - SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime - FROM notifications.emailnotifications n - LEFT JOIN notifications.orders o ON n._orderid = o._id - WHERE o.alternateid = _alternateorderid - and o.creatorname = _creatorname; - IF NOT FOUND THEN - RETURN QUERY - SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz - FROM notifications.orders o - WHERE o.alternateid = _alternateorderid - and o.creatorname = _creatorname; - END IF; - END; -$BODY$; - --- getmetrics.sql: -CREATE OR REPLACE FUNCTION notifications.getmetrics( - month_input int, - year_input int -) -RETURNS TABLE ( - org text, - placed_orders bigint, - sent_emails bigint, - succeeded_emails bigint, - sent_sms bigint, - succeeded_sms bigint -) -AS $$ -BEGIN - RETURN QUERY - SELECT - o.creatorname, - COUNT(DISTINCT o._id) AS placed_orders, - SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails, - SUM(CASE WHEN e.result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END) AS succeeded_emails, - SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms, - SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms - FROM notifications.orders o - LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid - LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid - WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input - AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input - GROUP BY o.creatorname; -END; -$$ LANGUAGE plpgsql; - - --- getorderincludestatus.sql: -CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v4( - _alternateid uuid, - _creatorname text -) -RETURNS TABLE( - alternateid uuid, - creatorname text, - sendersreference text, - created timestamp with time zone, - requestedsendtime timestamp with time zone, - processed timestamp with time zone, - processedstatus orderprocessingstate, - notificationchannel text, - ignorereservation boolean, - resourceid text, - conditionendpoint text, - generatedemailcount bigint, - succeededemailcount bigint, - generatedsmscount bigint, - succeededsmscount bigint -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE - _target_orderid INTEGER; - _succeededEmailCount BIGINT; - _generatedEmailCount BIGINT; - _succeededSmsCount BIGINT; - _generatedSmsCount BIGINT; -BEGIN - SELECT _id INTO _target_orderid - FROM notifications.orders - WHERE orders.alternateid = _alternateid - AND orders.creatorname = _creatorname; - - SELECT - SUM(CASE WHEN result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END), - COUNT(1) AS generatedEmailCount - INTO _succeededEmailCount, _generatedEmailCount - FROM notifications.emailnotifications - WHERE _orderid = _target_orderid; - - SELECT - SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END), - COUNT(1) AS generatedSmsCount - INTO _succeededSmsCount, _generatedSmsCount - FROM notifications.smsnotifications - WHERE _orderid = _target_orderid; - - RETURN QUERY - SELECT - orders.alternateid, - orders.creatorname, - orders.sendersreference, - orders.created, - orders.requestedsendtime, - orders.processed, - orders.processedstatus, - orders.notificationorder->>'NotificationChannel', - CASE - WHEN orders.notificationorder->>'IgnoreReservation' IS NULL THEN NULL - ELSE (orders.notificationorder->>'IgnoreReservation')::BOOLEAN - END AS IgnoreReservation, - orders.notificationorder->>'ResourceId', - orders.notificationorder->>'ConditionEndpoint', - _generatedEmailCount, - _succeededEmailCount, - _generatedSmsCount, - _succeededSmsCount - FROM - notifications.orders AS orders - WHERE - orders.alternateid = _alternateid; -END; -$BODY$; - - --- getorderspastsendtimeupdatestatus.sql: -CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus() - RETURNS TABLE(notificationorders jsonb) - LANGUAGE 'plpgsql' -AS $BODY$ -BEGIN -RETURN QUERY - UPDATE notifications.orders - SET processedstatus = 'Processing' - WHERE _id IN (select _id - from notifications.orders - where processedstatus = 'Registered' - and requestedsendtime <= now() + INTERVAL '1 minute' - limit 50) - RETURNING notificationorder AS notificationorders; -END; -$BODY$; - --- getsmsrecipients.sql: -CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid) -RETURNS TABLE( - recipientorgno text, - recipientnin text, - mobilenumber text -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); -BEGIN -RETURN query - SELECT s.recipientorgno, s.recipientnin, s.mobilenumber - FROM notifications.smsnotifications s - WHERE s._orderid = __orderid; -END; -$BODY$; - --- getsmsstatusnewupdatestatus.sql: -CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text, recipientorgno text, recipientnin text) - LANGUAGE 'plpgsql' -AS $BODY$ -BEGIN - RETURN query - WITH updated AS ( - UPDATE notifications.smsnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber, notifications.smsnotifications.recipientorgno, notifications.smsnotifications.recipientnin) - SELECT u.alternateid, st.sendernumber, u.mobilenumber, CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body, u.recipientorgno, u.recipientnin - FROM updated u, notifications.smstexts st - WHERE u._orderid = st._orderid; -END; -$BODY$; - --- getsmssummary.sql: -CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2( - _alternateorderid uuid, - _creatorname text) - RETURNS TABLE( - sendersreference text, - alternateid uuid, - recipientorgno text, - recipientnin text, - mobilenumber text, - result smsnotificationresulttype, - resulttime timestamptz) - LANGUAGE 'plpgsql' -AS $BODY$ - - BEGIN - RETURN QUERY - SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime - FROM notifications.smsnotifications n - LEFT JOIN notifications.orders o ON n._orderid = o._id - WHERE o.alternateid = _alternateorderid - and o.creatorname = _creatorname; - IF NOT FOUND THEN - RETURN QUERY - SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz - FROM notifications.orders o - WHERE o.alternateid = _alternateorderid - and o.creatorname = _creatorname; - END IF; - END; -$BODY$; - --- insertemailnotification.sql: -CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_toaddress TEXT, -_customizedbody TEXT, -_customizedsubject TEXT, -_result TEXT, -_resulttime timestamptz, -_expirytime timestamptz) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); -BEGIN - -INSERT INTO notifications.emailnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -toaddress, -customizedBody, -customizedSubject, -result, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_toaddress, -_customizedbody, -_customizedsubject, -_result::emailnotificationresulttype, -_resulttime, -_expirytime); -END; -$BODY$; - --- insertemailtext.sql: -CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT) -LANGUAGE 'plpgsql' -AS $BODY$ -BEGIN -INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype) - VALUES (__orderid, _fromaddress, _subject, _body, _contenttype); -END; -$BODY$; - - --- insertorder.sql: -CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB) -RETURNS BIGINT - LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE -_orderid BIGINT; -BEGIN - INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder) - VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder) - RETURNING _id INTO _orderid; - - RETURN _orderid; -END; -$BODY$; - --- insertsmsnotification.sql: -CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_mobilenumber TEXT, -_customizedbody TEXT, -_result text, -_smscount integer, -_resulttime timestamptz, -_expirytime timestamptz -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); -BEGIN - -INSERT INTO notifications.smsnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -mobilenumber, -customizedbody, -result, -smscount, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_mobilenumber, -_customizedbody, -_result::smsnotificationresulttype, -_smscount, -_resulttime, -_expirytime); -END; -$BODY$; - --- updateemailstatus.sql: -CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text) -LANGUAGE 'plpgsql' -AS $BODY$ -BEGIN - UPDATE notifications.emailnotifications - SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid - WHERE alternateid = _alternateid; -END; -$BODY$; - diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql new file mode 100644 index 00000000..ff6c7f05 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql @@ -0,0 +1,118 @@ +DROP PROCEDURE IF EXISTS notifications.insertemailnotification( + IN _orderid uuid, + IN _alternateid uuid, + IN _recipientorgno TEXT, + IN _recipientnin TEXT, + IN _toaddress TEXT, + IN _result TEXT, + IN _resulttime timestamptz, + IN _expirytime timestamptz +); + +DROP PROCEDURE IF EXISTS notifications.insertsmsnotification( + IN _orderid uuid, + IN _alternateid uuid, + IN _recipientorgno TEXT, + IN _recipientnin TEXT, + IN _mobilenumber TEXT, + IN _result TEXT, + IN _smscount integer, + IN _resulttime timestamptz, + IN _expirytime timestamptz +); + +CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _toaddress TEXT, + _customizedbody TEXT, + _customizedsubject TEXT, + _result TEXT, + _resulttime timestamptz, + _expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + __orderid BIGINT; +BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; + + INSERT INTO notifications.emailnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + toaddress, + customizedbody, + customizedsubject, + result, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _toaddress, + _customizedbody, + _customizedsubject, + _result::emailnotificationresulttype, + _resulttime, + _expirytime + ); +END; +$BODY$; + +CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _mobilenumber TEXT, + _customizedbody TEXT, + _result TEXT, + _smscount integer, + _resulttime timestamptz, + _expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + __orderid BIGINT; +BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; + + INSERT INTO notifications.smsnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + mobilenumber, + customizedbody, + result, + smscount, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _mobilenumber, + _customizedbody, + _result::smsnotificationresulttype, + _smscount, + _resulttime, + _expirytime + ); +END; +$BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql new file mode 100644 index 00000000..f5e067df --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql @@ -0,0 +1,79 @@ +DROP FUNCTION IF EXISTS notifications.getsms_statusnew_updatestatus; +DROP FUNCTION IF EXISTS notifications.getemails_statusnew_updatestatus; + +CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + latest_email_timeout TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT emaillimittimeout + INTO latest_email_timeout + FROM notifications.resourcelimitlog + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + + -- Check if the latest email timeout is set and valid + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY + SELECT NULL::uuid AS alternateid, + NULL::text AS subject, + NULL::text AS body, + NULL::text AS fromaddress, + NULL::text AS toaddress, + NULL::text AS contenttype + WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog + SET emaillimittimeout = NULL + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN QUERY + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, + _orderid, + notifications.emailnotifications.toaddress, + notifications.emailnotifications.customizedsubject, + notifications.emailnotifications.customizedbody + ) + SELECT u.alternateid, + CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, + et.fromaddress, + u.toaddress, + et.contenttype + FROM updated u + JOIN notifications.emailtexts et ON u._orderid = et._orderid; +END; +$BODY$; + +CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + RETURN QUERY + WITH updated AS ( + UPDATE notifications.smsnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.smsnotifications.alternateid, + _orderid, + notifications.smsnotifications.mobilenumber, + notifications.smsnotifications.customizedbody + ) + SELECT u.alternateid, + st.sendernumber, + u.mobilenumber, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body + FROM updated u + JOIN notifications.smstexts st ON u._orderid = st._orderid; +END; +$BODY$; diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql new file mode 100644 index 00000000..e6b99873 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql @@ -0,0 +1 @@ +-- This script is auto generated from the tool DbTools. Do not edit manually. \ No newline at end of file From 0e9451528d6e35e58940d41286f64416be606661 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:49:33 +0100 Subject: [PATCH 42/75] Remove two unnecessary values. --- .../Repository/SmsNotificationRepository.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index 3af37d47..e6c8510c 100644 --- a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs @@ -104,9 +104,7 @@ public async Task> GetNewNotifications() reader.GetValue("alternateid"), reader.GetValue("sendernumber"), reader.GetValue("mobilenumber"), - reader.GetValue("body"), - reader.GetValue("recipientnin"), - reader.GetValue("recipientorgno")); + reader.GetValue("body"); searchResult.Add(sms); } From 9320d267372cd624b9e2b477651404e038f8571b Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:51:25 +0100 Subject: [PATCH 43/75] Add a missing parenthesis --- .../Repository/SmsNotificationRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index e6c8510c..abd0e4bd 100644 --- a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs @@ -104,7 +104,7 @@ public async Task> GetNewNotifications() reader.GetValue("alternateid"), reader.GetValue("sendernumber"), reader.GetValue("mobilenumber"), - reader.GetValue("body"); + reader.GetValue("body")); searchResult.Add(sms); } From caeae94abf316161bc48c2651e68a57f97c055f4 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:52:58 +0100 Subject: [PATCH 44/75] Remove two unnecessary properties. --- src/Altinn.Notifications.Core/Models/Sms.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Sms.cs b/src/Altinn.Notifications.Core/Models/Sms.cs index b5adc2bc..c4309149 100644 --- a/src/Altinn.Notifications.Core/Models/Sms.cs +++ b/src/Altinn.Notifications.Core/Models/Sms.cs @@ -30,16 +30,6 @@ public class Sms /// public string Message { get; set; } - /// - /// Gets or sets the national identity number of the recipient. - /// - public string NationalIdentityNumber { get; set; } - - /// - /// Gets or sets the organization number of the recipient. - /// - public string OrganizationNumber { get; set; } - /// /// Initializes a new instance of the class with the specified parameters. /// @@ -47,16 +37,12 @@ public class Sms /// The sender of the SMS message. /// The recipient of the SMS message. /// The contents of the SMS message. - /// The national identity number of the recipient. - /// The organization number of the recipient. - public Sms(Guid notificationId, string sender, string recipient, string message, string nationalIdentityNumber, string organizationNumber) + public Sms(Guid notificationId, string sender, string recipient, string message) { NotificationId = notificationId; Recipient = recipient; Sender = sender; Message = message; - NationalIdentityNumber = nationalIdentityNumber; - OrganizationNumber = organizationNumber; } /// From 1b58b0da7b094387f0ab56e7c095957efd809b74 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 08:54:57 +0100 Subject: [PATCH 45/75] Remove two unnecessary values. --- .../TestingServices/SmsNotificationServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index e3d72b0f..8d0f658d 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -23,7 +23,7 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class SmsNotificationServiceTests { private const string _smsQueueTopicName = "test.sms.queue"; - private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message", "10825795702 ", "310679941"); + private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message"); [Fact] public async Task CreateNotifications_NewSmsNotification_RepositoryCalledOnce() From 1b0385d671a39198d4cfdb13086a887719fe10d1 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 10:40:55 +0100 Subject: [PATCH 46/75] Enhance the logic to ensure all existing unit tests execute successfully. --- .../Services/EmailOrderProcessingService.cs | 14 +++++++++++-- .../TestingModels/NotificationOrderTests.cs | 20 +++++++++---------- .../EmailNotificationServiceTests.cs | 5 ++++- .../SmsNotificationServiceTests.cs | 5 ++++- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index 54288bdd..dab28984 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -45,7 +45,12 @@ public async Task ProcessOrder(NotificationOrder order) /// public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) { - var emailTemplate = order.Templates[0] as EmailTemplate; + EmailTemplate? emailTemplate = null; + if (order.Templates.Count > 0) + { + emailTemplate = order.Templates[0] as EmailTemplate; + } + foreach (Recipient recipient in recipients) { await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false, emailTemplate?.Body, emailTemplate?.Subject); @@ -66,7 +71,12 @@ public async Task ProcessOrderRetry(NotificationOrder order) /// public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { - var emailTemplate = order.Templates[0] as EmailTemplate; + EmailTemplate? emailTemplate = null; + if (order.Templates.Count > 0) + { + emailTemplate = order.Templates[0] as EmailTemplate; + } + List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); foreach (Recipient recipient in recipients) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs index 8cbe32c3..694e6b56 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/NotificationOrderTests.cs @@ -80,11 +80,11 @@ public NotificationOrderTests() new JsonObject() { { "$", "email" }, - { "type", "Email" }, + { "body", "email-body" }, + { "contentType", "Html" }, { "fromAddress", "sender@domain.com" }, { "subject", "email-subject" }, - { "body", "email-body" }, - { "contentType", "Html" } + { "type", "Email" } } } }, @@ -93,12 +93,6 @@ public NotificationOrderTests() { new JsonObject() { - { - "nationalIdentityNumber", "nationalidentitynumber" - }, - { - "isReserved", false - }, { "addressInfo", new JsonArray() { @@ -109,10 +103,16 @@ public NotificationOrderTests() { "emailAddress", "recipient1@domain.com" } } } + }, + { + "isReserved", false + }, + { + "nationalIdentityNumber", "nationalidentitynumber" } } } - } + } }.ToJsonString(); } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index 1dfc7996..d1b79c3e 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -8,6 +8,7 @@ using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.Core.Services; using Altinn.Notifications.Core.Services.Interfaces; @@ -313,7 +314,9 @@ private static EmailNotificationService GetTestService(IEmailNotificationReposit if (keywordsService == null) { - keywordsService = new Mock().Object; + var keywordsServiceMock = new Mock(); + keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((EmailRecipient recipient) => recipient); + keywordsService = keywordsServiceMock.Object; } return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName }), keywordsService); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index 8d0f658d..de2f5a4e 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -8,6 +8,7 @@ using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.Core.Services; using Altinn.Notifications.Core.Services.Interfaces; @@ -306,7 +307,9 @@ private static SmsNotificationService GetTestService(ISmsNotificationRepository? if (keywordsService == null) { - keywordsService = new Mock().Object; + var keywordsServiceMock = new Mock(); + keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((SmsRecipient recipient) => recipient); + keywordsService = keywordsServiceMock.Object; } return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName }), keywordsService); From adfb256e87adda5336f65e862cf3182ac000a829 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 10:55:49 +0100 Subject: [PATCH 47/75] Save and retrieve the customized subject and body to and from the database --- .../getemailsstatusnewupdatestatus.sql | 64 +++++++++++------ .../getsmsstatusnewupdatestatus.sql | 20 ++++-- .../insertemailnotification.sql | 66 ++++++++++-------- .../insertsmsnotification.sql | 69 ++++++++++--------- 4 files changed, 134 insertions(+), 85 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql index 93856b6e..fcd56eb6 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailsstatusnewupdatestatus.sql @@ -5,24 +5,48 @@ AS $BODY$ DECLARE latest_email_timeout TIMESTAMP WITH TIME ZONE; BEGIN - SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - IF latest_email_timeout IS NOT NULL THEN - IF latest_email_timeout >= NOW() THEN - RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; - RETURN; - ELSE - UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - END IF; - END IF; - - RETURN query - WITH updated AS ( - UPDATE notifications.emailnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress) - SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype - FROM updated u, notifications.emailtexts et - WHERE u._orderid = et._orderid; + SELECT emaillimittimeout + INTO latest_email_timeout + FROM notifications.resourcelimitlog + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + + -- Check if the latest email timeout is set and valid + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY + SELECT NULL::uuid AS alternateid, + NULL::text AS subject, + NULL::text AS body, + NULL::text AS fromaddress, + NULL::text AS toaddress, + NULL::text AS contenttype + WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog + SET emaillimittimeout = NULL + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN QUERY + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, + _orderid, + notifications.emailnotifications.toaddress, + notifications.emailnotifications.customizedsubject, + notifications.emailnotifications.customizedbody + ) + SELECT u.alternateid, + CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, + et.fromaddress, + u.toaddress, + et.contenttype + FROM updated u + JOIN notifications.emailtexts et ON u._orderid = et._orderid; END; -$BODY$; \ No newline at end of file +$BODY$; diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql index 91dafe24..cc6709af 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getsmsstatusnewupdatestatus.sql @@ -3,15 +3,21 @@ CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() LANGUAGE 'plpgsql' AS $BODY$ BEGIN - - RETURN query + RETURN QUERY WITH updated AS ( UPDATE notifications.smsnotifications SET result = 'Sending', resulttime = now() WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber) - SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body - FROM updated u, notifications.smstexts st - WHERE u._orderid = st._orderid; + RETURNING notifications.smsnotifications.alternateid, + _orderid, + notifications.smsnotifications.mobilenumber, + notifications.smsnotifications.customizedbody + ) + SELECT u.alternateid, + st.sendernumber, + u.mobilenumber, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body + FROM updated u + JOIN notifications.smstexts st ON u._orderid = st._orderid; END; -$BODY$; \ No newline at end of file +$BODY$; diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql index 3e377d3a..775ff06c 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertemailnotification.sql @@ -1,35 +1,47 @@ CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_toaddress TEXT, -_result text, -_resulttime timestamptz, -_expirytime timestamptz) + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _toaddress TEXT, + _customizedbody TEXT, + _customizedsubject TEXT, + _result TEXT, + _resulttime timestamptz, + _expirytime timestamptz +) LANGUAGE 'plpgsql' AS $BODY$ DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); + __orderid BIGINT; BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; -INSERT INTO notifications.emailnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -toaddress, result, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_toaddress, -_result::emailnotificationresulttype, -_resulttime, -_expirytime); + INSERT INTO notifications.emailnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + toaddress, + customizedbody, + customizedsubject, + result, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _toaddress, + _customizedbody, + _customizedsubject, + _result::emailnotificationresulttype, + _resulttime, + _expirytime + ); END; $BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql index 5f60838c..f41ebdb3 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/insertsmsnotification.sql @@ -1,40 +1,47 @@ CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( -_orderid uuid, -_alternateid uuid, -_recipientorgno TEXT, -_recipientnin TEXT, -_mobilenumber TEXT, -_result text, -_smscount integer, -_resulttime timestamptz, -_expirytime timestamptz + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _mobilenumber TEXT, + _customizedbody TEXT, + _result TEXT, + _smscount integer, + _resulttime timestamptz, + _expirytime timestamptz ) LANGUAGE 'plpgsql' AS $BODY$ DECLARE -__orderid BIGINT := (SELECT _id from notifications.orders - where alternateid = _orderid); + __orderid BIGINT; BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; -INSERT INTO notifications.smsnotifications( -_orderid, -alternateid, -recipientorgno, -recipientnin, -mobilenumber, -result, -smscount, -resulttime, -expirytime) -VALUES ( -__orderid, -_alternateid, -_recipientorgno, -_recipientnin, -_mobilenumber, -_result::smsnotificationresulttype, -_smscount, -_resulttime, -_expirytime); + INSERT INTO notifications.smsnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + mobilenumber, + customizedbody, + result, + smscount, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _mobilenumber, + _customizedbody, + _result::smsnotificationresulttype, + _smscount, + _resulttime, + _expirytime + ); END; $BODY$; \ No newline at end of file From b5200a8765bba81fcf9bcb0eaaef1c0c4b0880e1 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 14:20:00 +0100 Subject: [PATCH 48/75] Improve the XML documentation --- .../Models/Recipients/EmailRecipient.cs | 6 +++--- .../Models/Recipients/SmsRecipient.cs | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs index a5ce0594..17e95692 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs @@ -1,17 +1,17 @@ namespace Altinn.Notifications.Core.Models.Recipients; /// -/// Class representing an email recipient. +/// Represents an email recipient with various properties for customization and identification. /// public class EmailRecipient { /// - /// Gets or sets the customized body of the email. + /// Gets or sets the customized body of the email after replacing the keywords with actual values. /// public string? CustomizedBody { get; set; } = null; /// - /// Gets or sets the customized subject of the email. + /// Gets or sets the customized subject of the email after replacing the keywords with actual values. /// public string? CustomizedSubject { get; set; } = null; diff --git a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs index d33433da..73510553 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs @@ -1,32 +1,32 @@ namespace Altinn.Notifications.Core.Models.Recipients; /// -/// Class representing an SMS recipient +/// Represents an SMS recipient with various properties for customization and identification. /// public class SmsRecipient { /// - /// Gets or sets the recipient's organization number + /// Gets or sets the customized body of the SMS after replacing the keywords with actual values. /// - public string? OrganizationNumber { get; set; } = null; + public string? CustomizedBody { get; set; } = null; /// - /// Gets or sets the recipient's national identity number + /// Gets or sets a value indicating whether the recipient is reserved from digital communication. /// - public string? NationalIdentityNumber { get; set; } = null; + public bool? IsReserved { get; set; } /// - /// Gets or sets the mobile number + /// Gets or sets the recipient's mobile number. /// public string MobileNumber { get; set; } = string.Empty; /// - /// Gets or sets the customized body of the SMS. + /// Gets or sets the recipient's national identity number. /// - public string? CustomizedBody { get; set; } = null; + public string? NationalIdentityNumber { get; set; } = null; /// - /// Gets or sets a value indicating whether the recipient is reserved from digital communication + /// Gets or sets the recipient's organization number. /// - public bool? IsReserved { get; set; } + public string? OrganizationNumber { get; set; } = null; } From 7ea16a03d46cfd2c4cdc7b26b4de1eeddf97b351 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 16:46:47 +0100 Subject: [PATCH 49/75] Update the keywords service to handle data batches --- .../Services/Interfaces/IKeywordsService.cs | 20 +- .../Services/KeywordsService.cs | 181 ++++++++---------- 2 files changed, 85 insertions(+), 116 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs index 20b21855..84a03c85 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -10,29 +10,29 @@ public interface IKeywordsService /// /// Checks whether the specified string contains the placeholder keyword $recipientName$. /// - /// The string to check. + /// The string to check for the placeholder keyword. /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. bool ContainsRecipientNamePlaceholder(string? value); /// /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. /// - /// The string to check. + /// The string to check for the placeholder keyword. /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. bool ContainsRecipientNumberPlaceholder(string? value); /// - /// Replaces placeholder keywords in an with actual values. + /// Replaces placeholder keywords in a collection of with actual values. /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. - Task ReplaceKeywordsAsync(SmsRecipient smsRecipient); + /// The collection of to process. + /// A task that represents the asynchronous operation. The task result contains the collection of with the placeholder keywords replaced by actual values. + Task> ReplaceKeywordsAsync(IEnumerable smsRecipients); /// - /// Replaces placeholder keywords in an with actual values. + /// Replaces placeholder keywords in a collection of with actual values. /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. - Task ReplaceKeywordsAsync(EmailRecipient emailRecipient); + /// The collection of to process. + /// A task that represents the asynchronous operation. The task result contains the collection of with the placeholder keywords replaced by actual values. + Task> ReplaceKeywordsAsync(IEnumerable emailRecipients); } } diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 88683f27..9835e58f 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -1,4 +1,5 @@ using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Models.Parties; using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Services.Interfaces; @@ -25,153 +26,121 @@ public KeywordsService(IRegisterClient registerClient) } /// - /// - /// Checks whether the specified string contains the placeholder keyword $recipientName$. - /// - /// The string to check. - /// true if the specified string contains the placeholder keyword $recipientName$; otherwise, false. public bool ContainsRecipientNamePlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); } /// - /// - /// Checks whether the specified string contains the placeholder keyword $recipientNumber$. - /// - /// The string to check. - /// true if the specified string contains the placeholder keyword $recipientNumber$; otherwise, false. public bool ContainsRecipientNumberPlaceholder(string? value) { return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); } /// - /// - /// Replaces placeholder keywords in an with actual values. - /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. - public async Task ReplaceKeywordsAsync(SmsRecipient smsRecipient) + public async Task> ReplaceKeywordsAsync(IEnumerable smsRecipients) { - ArgumentNullException.ThrowIfNull(smsRecipient); + ArgumentNullException.ThrowIfNull(smsRecipients); + + var organizationNumbers = smsRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); + + var nationalIdentityNumbers = smsRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .ToList(); + + var personDetailsTask = nationalIdentityNumbers.Count > 0 + ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) + : Task.FromResult(new List()); + + var organizationDetailsTask = organizationNumbers.Count > 0 + ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) + : Task.FromResult(new List()); + + await Task.WhenAll(personDetailsTask, organizationDetailsTask); - smsRecipient = await ReplaceKeywordsAsync(smsRecipient, r => r.CustomizedBody, (r, v) => r.CustomizedBody = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); + var personDetails = personDetailsTask.Result; + var organizationDetails = organizationDetailsTask.Result; - return smsRecipient; + foreach (var smsRecipient in smsRecipients) + { + smsRecipient.CustomizedBody = ReplaceRecipientPlaceholders(smsRecipient.CustomizedBody, smsRecipient.OrganizationNumber, smsRecipient.NationalIdentityNumber, organizationDetails, personDetails); + } + + return smsRecipients; } /// - /// - /// Replaces placeholder keywords in an with actual values. - /// - /// The to process. - /// A task that represents the asynchronous operation. The task result contains the with the placeholder keywords replaced by actual values. - public async Task ReplaceKeywordsAsync(EmailRecipient emailRecipient) + public async Task> ReplaceKeywordsAsync(IEnumerable emailRecipients) { - ArgumentNullException.ThrowIfNull(emailRecipient); + ArgumentNullException.ThrowIfNull(emailRecipients); - emailRecipient = await ReplaceKeywordsAsync(emailRecipient, r => r.CustomizedBody, (r, v) => r.CustomizedBody = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); - emailRecipient = await ReplaceKeywordsAsync(emailRecipient, r => r.CustomizedSubject, (r, v) => r.CustomizedSubject = v, r => r.NationalIdentityNumber, r => r.OrganizationNumber); + var organizationNumbers = emailRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); - return emailRecipient; - } + var nationalIdentityNumbers = emailRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .ToList(); - /// - /// Replaces placeholder keywords with actual values. - /// - /// The type of the recipient. - /// The recipient to process. - /// A function to get the body of the recipient. - /// A function to set the body of the recipient. - /// A function to get the national identity number of the recipient. - /// A function to get the organization number of the recipient. - /// A task that represents the asynchronous operation. The task result contains the processed recipient. - private async Task ReplaceKeywordsAsync( - T recipient, - Func getBody, - Action setBody, - Func nationalIdentityNumberGetter, - Func organizationNumberGetter) - { - if (ContainsRecipientNamePlaceholder(getBody(recipient))) - { - await ReplaceRecipientNamePlaceholderAsync(recipient, getBody, setBody, nationalIdentityNumberGetter, organizationNumberGetter); - } + var personDetailsTask = nationalIdentityNumbers.Count > 0 + ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) + : Task.FromResult(new List()); + + var organizationDetailsTask = organizationNumbers.Count > 0 + ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) + : Task.FromResult(new List()); + + await Task.WhenAll(personDetailsTask, organizationDetailsTask); - if (ContainsRecipientNumberPlaceholder(getBody(recipient))) + var personDetails = personDetailsTask.Result; + var organizationDetails = organizationDetailsTask.Result; + + foreach (var emailRecipient in emailRecipients) { - ReplaceRecipientNumberPlaceholder(recipient, getBody, setBody, nationalIdentityNumberGetter, organizationNumberGetter); + emailRecipient.CustomizedBody = ReplaceRecipientPlaceholders(emailRecipient.CustomizedBody, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); + + emailRecipient.CustomizedSubject = ReplaceRecipientPlaceholders(emailRecipient.CustomizedSubject, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); } - return recipient; + return emailRecipients; } /// - /// Replaces the recipient name placeholder with the actual recipient name. + /// Replaces the recipient placeholders in the specified text with actual values. /// - /// The type of the recipient. - /// The recipient to process. - /// A function to get the body of the recipient. - /// A function to set the body of the recipient. - /// A function to get the national identity number of the recipient. - /// A function to get the organization number of the recipient. - /// A task that represents the asynchronous operation. - private async Task ReplaceRecipientNamePlaceholderAsync( - T recipient, - Func getBody, - Action setBody, - Func nationalIdentityNumberGetter, - Func organizationNumberGetter) + /// The text to process. + /// The organization number of the recipient. + /// The national identity number of the recipient. + /// The list of organization details. + /// The list of person details. + /// The text with the placeholders replaced by actual values. + private static string? ReplaceRecipientPlaceholders(string? text, string? organizationNumber, string? nationalIdentityNumber, List organizationDetails, List personDetails) { - var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) - { - var partyDetails = await _registerClient.GetPartyDetailsForPersons(new List { nationalIdentityNumber }); - if (partyDetails != null && partyDetails.Count > 0) - { - setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); - } - } - - var organizationNumber = organizationNumberGetter(recipient); if (!string.IsNullOrWhiteSpace(organizationNumber)) { - var partyDetails = await _registerClient.GetPartyDetailsForOrganizations(new List { organizationNumber }); - if (partyDetails != null && partyDetails.Count > 0) + var partyDetail = organizationDetails.Find(p => p.OrganizationNumber == organizationNumber); + if (partyDetail != null) { - setBody(recipient, getBody(recipient)?.Replace(_recipientNamePlaceholder, partyDetails[0]?.Name ?? string.Empty)); + text = text?.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); } } - } - /// - /// Replaces the recipient number placeholder with the actual recipient number. - /// - /// The type of the recipient. - /// The recipient to process. - /// A function to get the body of the recipient. - /// A function to set the body of the recipient. - /// A function to get the national identity number of the recipient. - /// A function to get the organization number of the recipient. - private static void ReplaceRecipientNumberPlaceholder( - T recipient, - Func getBody, - Action setBody, - Func nationalIdentityNumberGetter, - Func organizationNumberGetter) - { - var nationalIdentityNumber = nationalIdentityNumberGetter(recipient); if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) { - setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, nationalIdentityNumber)); + var partyDetail = personDetails.Find(p => p.NationalIdentityNumber == nationalIdentityNumber); + if (partyDetail != null) + { + text = text?.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); + } } - var organizationNumber = organizationNumberGetter(recipient); - if (!string.IsNullOrWhiteSpace(organizationNumber)) - { - setBody(recipient, getBody(recipient)?.Replace(_recipientNumberPlaceholder, organizationNumber)); - } + return text; } } } From b97af329cfa29ab79f6b5222dbed0eb660f45750 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 18:28:51 +0100 Subject: [PATCH 50/75] Remove the logic used to drop and create both functions and procedures --- .../Migration/v0.36/02-alter-procedures.sql | 118 ------------------ ...es.sql => 02-functions-and-procedures.sql} | 0 .../Migration/v0.36/03-alter-functions.sql | 79 ------------ 3 files changed, 197 deletions(-) delete mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql rename src/Altinn.Notifications.Persistence/Migration/v0.36/{04-functions-and-procedures.sql => 02-functions-and-procedures.sql} (100%) delete mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql deleted file mode 100644 index ff6c7f05..00000000 --- a/src/Altinn.Notifications.Persistence/Migration/v0.36/02-alter-procedures.sql +++ /dev/null @@ -1,118 +0,0 @@ -DROP PROCEDURE IF EXISTS notifications.insertemailnotification( - IN _orderid uuid, - IN _alternateid uuid, - IN _recipientorgno TEXT, - IN _recipientnin TEXT, - IN _toaddress TEXT, - IN _result TEXT, - IN _resulttime timestamptz, - IN _expirytime timestamptz -); - -DROP PROCEDURE IF EXISTS notifications.insertsmsnotification( - IN _orderid uuid, - IN _alternateid uuid, - IN _recipientorgno TEXT, - IN _recipientnin TEXT, - IN _mobilenumber TEXT, - IN _result TEXT, - IN _smscount integer, - IN _resulttime timestamptz, - IN _expirytime timestamptz -); - -CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( - _orderid uuid, - _alternateid uuid, - _recipientorgno TEXT, - _recipientnin TEXT, - _toaddress TEXT, - _customizedbody TEXT, - _customizedsubject TEXT, - _result TEXT, - _resulttime timestamptz, - _expirytime timestamptz -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE - __orderid BIGINT; -BEGIN - SELECT _id INTO __orderid - FROM notifications.orders - WHERE alternateid = _orderid; - - INSERT INTO notifications.emailnotifications( - _orderid, - alternateid, - recipientorgno, - recipientnin, - toaddress, - customizedbody, - customizedsubject, - result, - resulttime, - expirytime - ) - VALUES ( - __orderid, - _alternateid, - _recipientorgno, - _recipientnin, - _toaddress, - _customizedbody, - _customizedsubject, - _result::emailnotificationresulttype, - _resulttime, - _expirytime - ); -END; -$BODY$; - -CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( - _orderid uuid, - _alternateid uuid, - _recipientorgno TEXT, - _recipientnin TEXT, - _mobilenumber TEXT, - _customizedbody TEXT, - _result TEXT, - _smscount integer, - _resulttime timestamptz, - _expirytime timestamptz -) -LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE - __orderid BIGINT; -BEGIN - SELECT _id INTO __orderid - FROM notifications.orders - WHERE alternateid = _orderid; - - INSERT INTO notifications.smsnotifications( - _orderid, - alternateid, - recipientorgno, - recipientnin, - mobilenumber, - customizedbody, - result, - smscount, - resulttime, - expirytime - ) - VALUES ( - __orderid, - _alternateid, - _recipientorgno, - _recipientnin, - _mobilenumber, - _customizedbody, - _result::smsnotificationresulttype, - _smscount, - _resulttime, - _expirytime - ); -END; -$BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql similarity index 100% rename from src/Altinn.Notifications.Persistence/Migration/v0.36/04-functions-and-procedures.sql rename to src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql deleted file mode 100644 index f5e067df..00000000 --- a/src/Altinn.Notifications.Persistence/Migration/v0.36/03-alter-functions.sql +++ /dev/null @@ -1,79 +0,0 @@ -DROP FUNCTION IF EXISTS notifications.getsms_statusnew_updatestatus; -DROP FUNCTION IF EXISTS notifications.getemails_statusnew_updatestatus; - -CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) - LANGUAGE 'plpgsql' -AS $BODY$ -DECLARE - latest_email_timeout TIMESTAMP WITH TIME ZONE; -BEGIN - SELECT emaillimittimeout - INTO latest_email_timeout - FROM notifications.resourcelimitlog - WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - - -- Check if the latest email timeout is set and valid - IF latest_email_timeout IS NOT NULL THEN - IF latest_email_timeout >= NOW() THEN - RETURN QUERY - SELECT NULL::uuid AS alternateid, - NULL::text AS subject, - NULL::text AS body, - NULL::text AS fromaddress, - NULL::text AS toaddress, - NULL::text AS contenttype - WHERE FALSE; - RETURN; - ELSE - UPDATE notifications.resourcelimitlog - SET emaillimittimeout = NULL - WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); - END IF; - END IF; - - RETURN QUERY - WITH updated AS ( - UPDATE notifications.emailnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.emailnotifications.alternateid, - _orderid, - notifications.emailnotifications.toaddress, - notifications.emailnotifications.customizedsubject, - notifications.emailnotifications.customizedbody - ) - SELECT u.alternateid, - CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, - CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, - et.fromaddress, - u.toaddress, - et.contenttype - FROM updated u - JOIN notifications.emailtexts et ON u._orderid = et._orderid; -END; -$BODY$; - -CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() - RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) - LANGUAGE 'plpgsql' -AS $BODY$ -BEGIN - RETURN QUERY - WITH updated AS ( - UPDATE notifications.smsnotifications - SET result = 'Sending', resulttime = now() - WHERE result = 'New' - RETURNING notifications.smsnotifications.alternateid, - _orderid, - notifications.smsnotifications.mobilenumber, - notifications.smsnotifications.customizedbody - ) - SELECT u.alternateid, - st.sendernumber, - u.mobilenumber, - CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body - FROM updated u - JOIN notifications.smstexts st ON u._orderid = st._orderid; -END; -$BODY$; From 468899f2f6d979d2b926521cd190ffdf46a55f51 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Fri, 29 Nov 2024 20:56:05 +0100 Subject: [PATCH 51/75] Update the Email sending and retry sending logic --- .../Services/EmailNotificationService.cs | 51 +++--- .../Services/EmailOrderProcessingService.cs | 152 +++++++++++++----- .../Interfaces/IEmailNotificationService.cs | 26 ++- .../IEmailOrderProcessingService.cs | 30 ++-- .../Services/KeywordsService.cs | 129 ++++++++------- .../Services/SmsNotificationService.cs | 2 +- .../getemailrecipients.sql | 6 +- .../Repository/EmailNotificationRepository.cs | 6 +- .../EmailNotificationServiceTests.cs | 6 +- .../SmsNotificationServiceTests.cs | 6 +- 10 files changed, 264 insertions(+), 150 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index 25794f1e..3398f944 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -13,63 +13,49 @@ namespace Altinn.Notifications.Core.Services; /// -/// Implementation of +/// Implementation of . /// public class EmailNotificationService : IEmailNotificationService { - private readonly IGuidService _guid; private readonly IDateTimeService _dateTime; + private readonly IGuidService _guid; + private readonly string _emailQueueTopicName; private readonly IEmailNotificationRepository _repository; private readonly IKafkaProducer _producer; - private readonly string _emailQueueTopicName; - private readonly IKeywordsService _keywordsService; /// /// Initializes a new instance of the class. /// + /// The GUID service. + /// The date time service. + /// The email notification repository. + /// The Kafka producer. + /// The Kafka settings. public EmailNotificationService( IGuidService guid, IDateTimeService dateTime, IEmailNotificationRepository repository, IKafkaProducer producer, - IOptions kafkaSettings, - IKeywordsService keywordsService) + IOptions kafkaSettings) { _guid = guid; _dateTime = dateTime; - _repository = repository; _producer = producer; + _repository = repository; _emailQueueTopicName = kafkaSettings.Value.EmailQueueTopicName; - _keywordsService = keywordsService; } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false, string? emailBody = null, string? emailSubject = null) + public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, List emailAddresses, EmailRecipient emailRecipient, bool ignoreReservation = false) { - List emailAddresses = recipient.AddressInfo - .Where(a => a.AddressType == AddressType.Email) - .Select(a => (a as EmailAddressPoint)!) - .Where(a => a != null && !string.IsNullOrEmpty(a.EmailAddress)) - .ToList(); - - EmailRecipient emailRecipient = new() - { - IsReserved = recipient.IsReserved, - OrganizationNumber = recipient.OrganizationNumber, - NationalIdentityNumber = recipient.NationalIdentityNumber, - CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailBody) || _keywordsService.ContainsRecipientNamePlaceholder(emailBody)) ? emailBody : null, - CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailSubject) || _keywordsService.ContainsRecipientNamePlaceholder(emailSubject)) ? emailSubject : null, - }; - - emailRecipient = await _keywordsService.ReplaceKeywordsAsync(emailRecipient); - - if (recipient.IsReserved.HasValue && recipient.IsReserved.Value && !ignoreReservation) + if (emailRecipient.IsReserved.HasValue && emailRecipient.IsReserved.Value && !ignoreReservation) { emailRecipient.ToAddress = string.Empty; // not persisting email address for reserved recipients await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.Failed_RecipientReserved); return; } - else if (emailAddresses.Count == 0) + + if (emailAddresses.Count == 0) { await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.Failed_RecipientNotIdentified); return; @@ -110,6 +96,14 @@ public async Task UpdateSendStatus(EmailSendOperationResult sendOperationResult) await _repository.UpdateSendStatus(sendOperationResult.NotificationId, (EmailNotificationResultType)sendOperationResult.SendResult!, sendOperationResult.OperationId); } + /// + /// Creates a notification for a recipient. + /// + /// The order identifier. + /// The requested send time. + /// The email recipient. + /// The result of the notification. + /// A task that represents the asynchronous operation. private async Task CreateNotificationForRecipient(Guid orderId, DateTime requestedSendTime, EmailRecipient recipient, EmailNotificationResultType result) { var emailNotification = new EmailNotification() @@ -129,7 +123,6 @@ private async Task CreateNotificationForRecipient(Guid orderId, DateTime request } else { - // lets see how much time it takes to get a status for communication services expiry = requestedSendTime.AddHours(1); } diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index dab28984..d4a90471 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -10,86 +10,164 @@ namespace Altinn.Notifications.Core.Services; /// -/// Implementation of the +/// Implementation of the . /// public class EmailOrderProcessingService : IEmailOrderProcessingService { + private readonly IContactPointService _contactPointService; private readonly IEmailNotificationRepository _emailNotificationRepository; private readonly IEmailNotificationService _emailService; - private readonly IContactPointService _contactPointService; + private readonly IKeywordsService _keywordsService; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The email notification repository. + /// The email notification service. + /// The contact point service. + /// The keywords service. public EmailOrderProcessingService( IEmailNotificationRepository emailNotificationRepository, IEmailNotificationService emailService, - IContactPointService contactPointService) + IContactPointService contactPointService, + IKeywordsService keywordsService) { _emailNotificationRepository = emailNotificationRepository; _emailService = emailService; _contactPointService = contactPointService; + _keywordsService = keywordsService; } /// public async Task ProcessOrder(NotificationOrder order) { - var recipients = order.Recipients; - var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList(); + ArgumentNullException.ThrowIfNull(order); - await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); + var recipients = await UpdateRecipientsWithContactPointsAsync(order); await ProcessOrderWithoutAddressLookup(order, recipients); } /// - public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) + public async Task ProcessOrderRetry(NotificationOrder order) + { + ArgumentNullException.ThrowIfNull(order); + + var recipients = await UpdateRecipientsWithContactPointsAsync(order); + + await ProcessOrderRetryWithoutAddressLookup(order, recipients); + } + + /// + public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { - EmailTemplate? emailTemplate = null; - if (order.Templates.Count > 0) + var emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); + + foreach (var recipient in recipients) { - emailTemplate = order.Templates[0] as EmailTemplate; + var addressPoint = recipient.AddressInfo.OfType().FirstOrDefault(); + + var emailRecipient = emailRecipients.Find(er => + er.ToAddress == addressPoint?.EmailAddress && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); + + if (emailRecipient == null || addressPoint == null) + { + continue; + } + + await _emailService.CreateNotification( + order.Id, + order.RequestedSendTime, + [addressPoint], + emailRecipient, + order.IgnoreReservation ?? false); } + } + + /// + public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) + { + var emailRecipients = await GetEmailRecipientsAsync(order, recipients); - foreach (Recipient recipient in recipients) + foreach (var recipient in recipients) { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false, emailTemplate?.Body, emailTemplate?.Subject); + var emailRecipient = FindEmailRecipient(emailRecipients, recipient); + if (emailRecipient == null) + { + continue; + } + + var emailAddresses = recipient.AddressInfo + .OfType() + .Where(a => !string.IsNullOrWhiteSpace(a.EmailAddress)) + .ToList(); + + await _emailService.CreateNotification( + order.Id, + order.RequestedSendTime, + emailAddresses, + emailRecipient, + order.IgnoreReservation ?? false); } } - /// - public async Task ProcessOrderRetry(NotificationOrder order) + /// + /// Retrieves email recipients with replaced keywords. + /// + /// The notification order. + /// The list of recipients. + /// A task that represents the asynchronous operation. The task result contains the list of email recipients. + private async Task> GetEmailRecipientsAsync(NotificationOrder order, IEnumerable recipients) { - var recipients = order.Recipients; - var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList(); + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(order.Templates); - await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); + var emailTemplate = order.Templates.OfType().FirstOrDefault(); + ArgumentNullException.ThrowIfNull(emailTemplate); - await ProcessOrderRetryWithoutAddressLookup(order, recipients); + var emailRecipients = recipients.Select(recipient => new EmailRecipient + { + IsReserved = recipient.IsReserved, + OrganizationNumber = recipient.OrganizationNumber, + NationalIdentityNumber = recipient.NationalIdentityNumber, + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate.Body) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate.Body)) ? emailTemplate.Body : null, + CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate.Subject) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate.Subject)) ? emailTemplate.Subject : null, + }).ToList(); + + return await _keywordsService.ReplaceKeywordsAsync(emailRecipients); } - /// - public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) + /// + /// Finds the email recipient matching the given recipient. + /// + /// The list of email recipients. + /// The recipient to match. + /// The matching email recipient, or null if no match is found. + private static EmailRecipient? FindEmailRecipient(IEnumerable emailRecipients, Recipient recipient) { - EmailTemplate? emailTemplate = null; - if (order.Templates.Count > 0) - { - emailTemplate = order.Templates[0] as EmailTemplate; - } + return emailRecipients.FirstOrDefault(er => + (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || + (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); + } - List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); + /// + /// Updates the recipients with contact points. + /// + /// The notification order. + /// A task that represents the asynchronous operation. The task result contains the updated list of recipients. + private async Task> UpdateRecipientsWithContactPointsAsync(NotificationOrder order) + { + var recipientsWithoutEmail = order.Recipients + .Where(r => !r.AddressInfo.Exists(a => a.AddressType == AddressType.Email)) + .ToList(); - foreach (Recipient recipient in recipients) + if (recipientsWithoutEmail.Count != 0) { - EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint; - - if (!emailRecipients.Exists(er => - er.NationalIdentityNumber == recipient.NationalIdentityNumber - && er.OrganizationNumber == recipient.OrganizationNumber - && er.ToAddress == addressPoint?.EmailAddress)) - { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false, emailTemplate?.Body, emailTemplate?.Subject); - } + await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); } + + return order.Recipients; } } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs index d60efaef..433ac9bd 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs @@ -1,25 +1,35 @@ -using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; namespace Altinn.Notifications.Core.Services.Interfaces; /// -/// Interface for email notification service +/// Interface for the email notification service. /// public interface IEmailNotificationService { /// - /// Creates a new email notification based on the provided orderId and recipient + /// Creates a new email notification. /// - public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false, string? emailBody = null, string? emailSubject = null); + /// The unique identifier for the order associated with the notification. + /// The time at which the notification is requested to be sent. + /// The list of email addresses to send the notification to. + /// The details of the email recipient. + /// Indicates whether to ignore the reservation status of the recipient. + /// A task that represents the asynchronous operation. + Task CreateNotification(Guid orderId, DateTime requestedSendTime, List emailAddresses, EmailRecipient emailRecipient, bool ignoreReservation = false); /// - /// Starts the process of sending all ready email notifications + /// Initiates the process of sending all ready email notifications. /// - public Task SendNotifications(); + /// A task that represents the asynchronous operation. + Task SendNotifications(); /// - /// Update send status for a notification + /// Updates the send status of a notification. /// - public Task UpdateSendStatus(EmailSendOperationResult sendOperationResult); + /// The result of the send operation. + /// A task that represents the asynchronous operation. + Task UpdateSendStatus(EmailSendOperationResult sendOperationResult); } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs index b073ef60..18404241 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs @@ -4,29 +4,37 @@ namespace Altinn.Notifications.Core.Services.Interfaces; /// -/// Interface for the order processing service speficic to email orders +/// Interface for the order processing service specific to email orders. /// public interface IEmailOrderProcessingService { /// - /// Processes a notification order + /// Processes a notification order. /// - public Task ProcessOrder(NotificationOrder order); + /// The notification order to process. + /// A task that represents the asynchronous operation. + Task ProcessOrder(NotificationOrder order); /// - /// Processes a notification order for the provided list of recipients - /// without looking up additional recipient data + /// Processes a notification order for the provided list of recipients without looking up additional recipient data. /// - public Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients); + /// The notification order to process. + /// The list of recipients to process. + /// A task that represents the asynchronous operation. + Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients); /// - /// Retry processing of a notification order + /// Retries processing of a notification order. /// - public Task ProcessOrderRetry(NotificationOrder order); + /// The notification order to retry processing. + /// A task that represents the asynchronous operation. + Task ProcessOrderRetry(NotificationOrder order); /// - /// Retryprocessing of a notification order for the provided list of recipients - /// without looking up additional recipient data + /// Retries processing of a notification order for the provided list of recipients without looking up additional recipient data. /// - public Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients); + /// The notification order to retry processing. + /// The list of recipients to process. + /// A task that represents the asynchronous operation. + Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients); } diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 9835e58f..1ac123f7 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -19,23 +19,18 @@ public class KeywordsService : IKeywordsService /// Initializes a new instance of the class. /// /// The register client to interact with the register service. - /// Thrown if is null. public KeywordsService(IRegisterClient registerClient) { _registerClient = registerClient ?? throw new ArgumentNullException(nameof(registerClient)); } /// - public bool ContainsRecipientNamePlaceholder(string? value) - { - return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); - } + public bool ContainsRecipientNamePlaceholder(string? value) => + !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); /// - public bool ContainsRecipientNumberPlaceholder(string? value) - { - return !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); - } + public bool ContainsRecipientNumberPlaceholder(string? value) => + !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); /// public async Task> ReplaceKeywordsAsync(IEnumerable smsRecipients) @@ -52,22 +47,12 @@ public async Task> ReplaceKeywordsAsync(IEnumerable r.NationalIdentityNumber!) .ToList(); - var personDetailsTask = nationalIdentityNumbers.Count > 0 - ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) - : Task.FromResult(new List()); - - var organizationDetailsTask = organizationNumbers.Count > 0 - ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) - : Task.FromResult(new List()); - - await Task.WhenAll(personDetailsTask, organizationDetailsTask); - - var personDetails = personDetailsTask.Result; - var organizationDetails = organizationDetailsTask.Result; + var (personDetails, organizationDetails) = await FetchPartyDetailsAsync(organizationNumbers, nationalIdentityNumbers); foreach (var smsRecipient in smsRecipients) { - smsRecipient.CustomizedBody = ReplaceRecipientPlaceholders(smsRecipient.CustomizedBody, smsRecipient.OrganizationNumber, smsRecipient.NationalIdentityNumber, organizationDetails, personDetails); + smsRecipient.CustomizedBody = + ReplacePlaceholders(smsRecipient.CustomizedBody, smsRecipient.OrganizationNumber, smsRecipient.NationalIdentityNumber, organizationDetails, personDetails); } return smsRecipients; @@ -88,56 +73,92 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< .Select(r => r.NationalIdentityNumber!) .ToList(); - var personDetailsTask = nationalIdentityNumbers.Count > 0 - ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) - : Task.FromResult(new List()); - - var organizationDetailsTask = organizationNumbers.Count > 0 - ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) - : Task.FromResult(new List()); - - await Task.WhenAll(personDetailsTask, organizationDetailsTask); - - var personDetails = personDetailsTask.Result; - var organizationDetails = organizationDetailsTask.Result; + var (personDetails, organizationDetails) = await FetchPartyDetailsAsync(organizationNumbers, nationalIdentityNumbers); foreach (var emailRecipient in emailRecipients) { - emailRecipient.CustomizedBody = ReplaceRecipientPlaceholders(emailRecipient.CustomizedBody, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); + emailRecipient.CustomizedBody = + ReplacePlaceholders(emailRecipient.CustomizedBody, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); - emailRecipient.CustomizedSubject = ReplaceRecipientPlaceholders(emailRecipient.CustomizedSubject, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); + emailRecipient.CustomizedSubject = + ReplacePlaceholders(emailRecipient.CustomizedSubject, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); } return emailRecipients; } /// - /// Replaces the recipient placeholders in the specified text with actual values. + /// Fetches party details for the given national identity and organization numbers. /// - /// The text to process. - /// The organization number of the recipient. - /// The national identity number of the recipient. + /// The list of organization numbers. + /// The list of national identity numbers. + /// A tuple containing lists of person and organization details. + private async Task<(List PersonDetails, List OrganizationDetails)> FetchPartyDetailsAsync( + List organizationNumbers, + List nationalIdentityNumbers) + { + var organizationDetailsTask = organizationNumbers.Count != 0 + ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) + : Task.FromResult(new List()); + + var personDetailsTask = nationalIdentityNumbers.Count != 0 + ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) + : Task.FromResult(new List()); + + await Task.WhenAll(personDetailsTask, organizationDetailsTask); + + return (personDetailsTask.Result, organizationDetailsTask.Result); + } + + /// + /// Replaces placeholders in the given text with actual values from the provided details. + /// + /// The text containing placeholders. + /// The organization number. + /// The national identity number. /// The list of organization details. /// The list of person details. - /// The text with the placeholders replaced by actual values. - private static string? ReplaceRecipientPlaceholders(string? text, string? organizationNumber, string? nationalIdentityNumber, List organizationDetails, List personDetails) + /// The text with placeholders replaced by actual values. + private static string? ReplacePlaceholders( + string? text, + string? organizationNumber, + string? nationalIdentityNumber, + IEnumerable organizationDetails, + IEnumerable personDetails) { - if (!string.IsNullOrWhiteSpace(organizationNumber)) + text = ReplaceWithDetails(text, organizationNumber, organizationDetails, p => p.OrganizationNumber); + + text = ReplaceWithDetails(text, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber); + + return text; + } + + /// + /// Replaces placeholders in the given text with actual values from the provided details. + /// + /// The text containing placeholders. + /// The key to match in the details. + /// The list of details. + /// The function to select the key from the details. + /// The text with placeholders replaced by actual values. + private static string? ReplaceWithDetails( + string? text, + string? key, + IEnumerable details, + Func keySelector) + { + if (string.IsNullOrWhiteSpace(key)) { - var partyDetail = organizationDetails.Find(p => p.OrganizationNumber == organizationNumber); - if (partyDetail != null) - { - text = text?.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); - } + return text; } - if (!string.IsNullOrWhiteSpace(nationalIdentityNumber)) + var detail = details.FirstOrDefault(e => keySelector(e) == key); + + if (detail != null) { - var partyDetail = personDetails.Find(p => p.NationalIdentityNumber == nationalIdentityNumber); - if (partyDetail != null) - { - text = text?.Replace(_recipientNamePlaceholder, partyDetail.Name ?? string.Empty); - } + text = text?.Replace(_recipientNamePlaceholder, detail.Name ?? string.Empty); + + text = text?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); } return text; diff --git a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs index 2af18ce3..9be8f62b 100644 --- a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -60,7 +60,7 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(smsBody) || _keywordsService.ContainsRecipientNamePlaceholder(smsBody)) ? smsBody : null, }; - smsRecipient = await _keywordsService.ReplaceKeywordsAsync(smsRecipient); + //smsRecipient = await _keywordsService.ReplaceKeywordsAsync(smsRecipient); if (recipient.IsReserved.HasValue && recipient.IsReserved.Value && !ignoreReservation) { diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql index cff77f0a..75bbac48 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql @@ -2,7 +2,9 @@ CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid RETURNS TABLE( recipientorgno text, recipientnin text, - toaddress text + toaddress text, + customizedbody text, + customizedsubject text ) LANGUAGE 'plpgsql' AS $BODY$ @@ -11,7 +13,7 @@ __orderid BIGINT := (SELECT _id from notifications.orders where alternateid = _alternateid); BEGIN RETURN query - SELECT e.recipientorgno, e.recipientnin, e.toaddress + SELECT e.recipientorgno, e.recipientnin, e.toaddress, e.customizedbody, e.customizedsubject FROM notifications.emailnotifications e WHERE e._orderid = __orderid; END; diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index ace06084..99d9133c 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -107,7 +107,7 @@ public async Task UpdateSendStatus(Guid? notificationId, EmailNotificationResult /// public async Task> GetRecipients(Guid orderId) { - List searchResult = new(); + List searchResult = []; await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_getEmailRecipients); using TelemetryTracker tracker = new(_telemetryClient, pgcom); @@ -118,9 +118,11 @@ public async Task> GetRecipients(Guid orderId) { searchResult.Add(new EmailRecipient() { + ToAddress = reader.GetValue("toaddress"), + CustomizedBody = reader.GetValue("customizedbody"), OrganizationNumber = reader.GetValue("recipientorgno"), + CustomizedSubject = reader.GetValue("customizedsubject"), NationalIdentityNumber = reader.GetValue("recipientnin"), - ToAddress = reader.GetValue("toaddress") }); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index d1b79c3e..ef165a37 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -314,9 +314,9 @@ private static EmailNotificationService GetTestService(IEmailNotificationReposit if (keywordsService == null) { - var keywordsServiceMock = new Mock(); - keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((EmailRecipient recipient) => recipient); - keywordsService = keywordsServiceMock.Object; + //var keywordsServiceMock = new Mock(); + //keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((EmailRecipient recipient) => recipient); + //keywordsService = keywordsServiceMock.Object; } return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName }), keywordsService); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index de2f5a4e..f93a4d6b 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -307,9 +307,9 @@ private static SmsNotificationService GetTestService(ISmsNotificationRepository? if (keywordsService == null) { - var keywordsServiceMock = new Mock(); - keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((SmsRecipient recipient) => recipient); - keywordsService = keywordsServiceMock.Object; + //var keywordsServiceMock = new Mock(); + //keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((SmsRecipient recipient) => recipient); + //keywordsService = keywordsServiceMock.Object; } return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName }), keywordsService); From 0e391e4de65c8cb211873fa325ff3484518197fe Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sat, 30 Nov 2024 17:04:01 +0100 Subject: [PATCH 52/75] Code refactoring --- .../Models/Parties/PartyDetailsLookupBatch.cs | 2 +- .../Models/Parties/PartyDetailsLookupRequest.cs | 2 +- src/Altinn.Notifications.Core/Services/KeywordsService.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs index e8221066..975bef87 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -24,7 +24,7 @@ public PartyDetailsLookupBatch(List? organizationNumbers = null, List new PartyDetailsLookupRequest(organizationNumber: orgNumber))); + PartyDetailsLookupRequestList.AddRange(organizationNumbers.Select(orgNu => new PartyDetailsLookupRequest(organizationNumber: orgNu))); } if (socialSecurityNumbers != null) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs index 8f04dde5..11d9709f 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs @@ -18,7 +18,7 @@ public PartyDetailsLookupRequest(string? organizationNumber = null, string? soci { if (!string.IsNullOrEmpty(organizationNumber) && !string.IsNullOrEmpty(socialSecurityNumber)) { - throw new ArgumentException("Only one of OrganizationNumber or SocialSecurityNumber can be set."); + throw new ArgumentException("You can specify either an OrganizationNumber or a SocialSecurityNumber, but not both."); } OrganizationNumber = organizationNumber; diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 1ac123f7..7b903730 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -88,7 +88,7 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< } /// - /// Fetches party details for the given national identity and organization numbers. + /// Fetches party details for the given organization and national identity numbers. /// /// The list of organization numbers. /// The list of national identity numbers. From 7606ba7bbc7a3707d6e0cb3a2b4a5fa725df4421 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sat, 30 Nov 2024 17:45:17 +0100 Subject: [PATCH 53/75] Fix the parameter of some unit tests --- .../EmailNotificationServiceTests.cs | 38 ++++++++--------- .../EmailOrderProcessingServiceTests.cs | 41 ++++++++++++------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index ef165a37..ddcc006c 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -83,6 +83,8 @@ public async Task CreateNotification_ToAddressDefined_ResultNew() DateTime requestedSendTime = DateTime.UtcNow; DateTime dateTimeOutput = DateTime.UtcNow; DateTime expectedExpiry = requestedSendTime.AddHours(1); + var emailRecipient = new EmailRecipient() { OrganizationNumber = "skd-orgno" }; + var emailAddressPoints = new List() { new("skd@norge.no") }; EmailNotification expected = new() { @@ -103,7 +105,7 @@ public async Task CreateNotification_ToAddressDefined_ResultNew() var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List() { new EmailAddressPoint("skd@norge.no") }, organizationNumber: "skd-orgno")); + await service.CreateNotification(orderId, requestedSendTime, emailAddressPoints, emailRecipient); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry)), Times.Once); @@ -118,6 +120,8 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsFalse DateTime requestedSendTime = DateTime.UtcNow; DateTime dateTimeOutput = DateTime.UtcNow; DateTime expectedExpiry = requestedSendTime.AddHours(1); + var emailRecipient = new EmailRecipient() { IsReserved = true }; + var emailAddressPoints = new List() { new("skd@norge.no") }; EmailNotification expected = new() { @@ -137,7 +141,7 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsFalse var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient() { IsReserved = true }); + await service.CreateNotification(orderId, requestedSendTime, emailAddressPoints, emailRecipient); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry)), Times.Once); @@ -152,6 +156,8 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsTrue_ DateTime requestedSendTime = DateTime.UtcNow; DateTime dateTimeOutput = DateTime.UtcNow; DateTime expectedExpiry = requestedSendTime.AddHours(1); + var emailRecipient = new EmailRecipient() { IsReserved = true }; + var emailAddressPoints = new List() { new("email@domain.com") }; EmailNotification expected = new() { @@ -172,7 +178,7 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsTrue_ var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient() { IsReserved = true, AddressInfo = [new EmailAddressPoint("email@domain.com")] }, true); + await service.CreateNotification(orderId, requestedSendTime, emailAddressPoints, emailRecipient, true); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry)), Times.Once); @@ -187,6 +193,8 @@ public async Task CreateNotification_ToAddressMissing_LookupFails_ResultFailedRe DateTime requestedSendTime = DateTime.UtcNow; DateTime dateTimeOutput = DateTime.UtcNow; DateTime expectedExpiry = dateTimeOutput; + var emailAddressPoints = new List() { new() }; + var emailRecipient = new EmailRecipient() { OrganizationNumber = "skd-orgno" }; EmailNotification expected = new() { @@ -206,7 +214,7 @@ public async Task CreateNotification_ToAddressMissing_LookupFails_ResultFailedRe var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List(), organizationNumber: "skd-orgno")); + await service.CreateNotification(orderId, requestedSendTime, emailAddressPoints, emailRecipient); // Assert repoMock.Verify(); @@ -215,12 +223,9 @@ public async Task CreateNotification_ToAddressMissing_LookupFails_ResultFailedRe [Fact] public async Task CreateNotification_RecipientHasTwoEmailAddresses_RepositoryCalledOnceForEachAddress() { - // Arrange - Recipient recipient = new() - { - OrganizationNumber = "org", - AddressInfo = [new EmailAddressPoint("user_1@domain.com"), new EmailAddressPoint("user_2@domain.com")] - }; + // Arrange + var emailRecipient = new EmailRecipient() { OrganizationNumber = "org" }; + var emailAddressPoints = new List() { new("user_1@domain.com"), new("user_2@domain.com") }; var repoMock = new Mock(); repoMock.Setup(r => r.AddNotification(It.Is(s => s.Recipient.OrganizationNumber == "org"), It.IsAny())); @@ -228,7 +233,7 @@ public async Task CreateNotification_RecipientHasTwoEmailAddresses_RepositoryCal var service = GetTestService(repo: repoMock.Object); // Act - await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, recipient); + await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, emailAddressPoints, emailRecipient); // Assert repoMock.Verify(r => r.AddNotification(It.Is(s => s.Recipient.OrganizationNumber == "org"), It.IsAny()), Times.Exactly(2)); @@ -289,7 +294,7 @@ public async Task UpdateSendStatus_TransientErrorResult_ConvertedToNew() repoMock.Verify(); } - private static EmailNotificationService GetTestService(IEmailNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null, IKeywordsService? keywordsService = null) + private static EmailNotificationService GetTestService(IEmailNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) { var guidService = new Mock(); guidService @@ -312,13 +317,6 @@ private static EmailNotificationService GetTestService(IEmailNotificationReposit producer = producerMock.Object; } - if (keywordsService == null) - { - //var keywordsServiceMock = new Mock(); - //keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((EmailRecipient recipient) => recipient); - //keywordsService = keywordsServiceMock.Object; - } - - return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName }), keywordsService); + return new EmailNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { EmailQueueTopicName = _emailQueueTopicName })); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index 02ed6238..68a9f3f3 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -44,7 +44,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())); var service = GetTestService(emailService: serviceMock.Object); @@ -52,15 +52,15 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() await service.ProcessOrder(order); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] public async Task ProcessOrder_ExpectedInputToNotificationService() { // Arrange - DateTime requested = DateTime.UtcNow; Guid orderId = Guid.NewGuid(); + DateTime requested = DateTime.UtcNow; var order = new NotificationOrder() { @@ -73,10 +73,14 @@ public async Task ProcessOrder_ExpectedInputToNotificationService() } }; - Recipient expectedRecipient = new(new List() { new EmailAddressPoint("test@test.com") }, organizationNumber: "skd-orgno"); + List expectedEmailAddressPoints = [new("test@test.com")]; + EmailRecipient expectedEmailRecipient = new() + { + OrganizationNumber = "skd-orgno" + }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny(), It.IsAny(), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is>(r => AssertUtils.AreEquivalent(expectedEmailAddressPoints, r)), It.Is(e => AssertUtils.AreEquivalent(expectedEmailRecipient, e)), It.IsAny())); var service = GetTestService(emailService: serviceMock.Object); @@ -101,7 +105,7 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception()); var repoMock = new Mock(); @@ -113,7 +117,7 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC await Assert.ThrowsAsync(async () => await service.ProcessOrder(order)); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); } @@ -140,10 +144,9 @@ public async Task ProcessOrder_RecipientMissingEmail_ContactPointServiceCalled() s => s.CreateNotification( It.IsAny(), It.IsAny(), - It.Is(r => r.NationalIdentityNumber == "123456"), - It.IsAny(), - It.IsAny(), - It.IsAny())); + It.IsAny>(), + It.Is(r => r.NationalIdentityNumber == "123456"), + It.IsAny())); var contactPointServiceMock = new Mock(); contactPointServiceMock.Setup(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1), It.IsAny())); @@ -176,7 +179,7 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())); var emailRepoMock = new Mock(); emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() @@ -192,13 +195,14 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() // Assert emailRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } private static EmailOrderProcessingService GetTestService( IEmailNotificationRepository? emailRepo = null, IEmailNotificationService? emailService = null, - IContactPointService? contactPointService = null) + IContactPointService? contactPointService = null, + IKeywordsService? keywordsService = null) { if (emailRepo == null) { @@ -220,6 +224,13 @@ private static EmailOrderProcessingService GetTestService( contactPointService = contactPointServiceMock.Object; } - return new EmailOrderProcessingService(emailRepo, emailService, contactPointService); + if (keywordsService == null) + { + var keywordsServiceMock = new Mock(); + keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny>())).ReturnsAsync((IEnumerable recipient) => recipient); + keywordsService = keywordsServiceMock.Object; + } + + return new EmailOrderProcessingService(emailRepo, emailService, contactPointService, keywordsService); } } From d88cb97305ee57ab7adabddf8215333de1220d28 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sat, 30 Nov 2024 18:33:37 +0100 Subject: [PATCH 54/75] Adjust the code to run unit test successfully --- .../Services/EmailOrderProcessingService.cs | 10 ++-------- .../TestingServices/EmailNotificationServiceTests.cs | 2 +- .../TestingServices/SmsNotificationServiceTests.cs | 6 +++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index d4a90471..ac5be964 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -94,10 +94,6 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List foreach (var recipient in recipients) { var emailRecipient = FindEmailRecipient(emailRecipients, recipient); - if (emailRecipient == null) - { - continue; - } var emailAddresses = recipient.AddressInfo .OfType() @@ -125,15 +121,13 @@ private async Task> GetEmailRecipientsAsync(Notifica ArgumentNullException.ThrowIfNull(order.Templates); var emailTemplate = order.Templates.OfType().FirstOrDefault(); - ArgumentNullException.ThrowIfNull(emailTemplate); - var emailRecipients = recipients.Select(recipient => new EmailRecipient { IsReserved = recipient.IsReserved, OrganizationNumber = recipient.OrganizationNumber, NationalIdentityNumber = recipient.NationalIdentityNumber, - CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate.Body) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate.Body)) ? emailTemplate.Body : null, - CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate.Subject) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate.Subject)) ? emailTemplate.Subject : null, + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate?.Body) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate?.Body)) ? emailTemplate?.Body : null, + CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate?.Subject) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate?.Subject)) ? emailTemplate?.Subject : null, }).ToList(); return await _keywordsService.ReplaceKeywordsAsync(emailRecipients); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index ddcc006c..557d3108 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -193,7 +193,7 @@ public async Task CreateNotification_ToAddressMissing_LookupFails_ResultFailedRe DateTime requestedSendTime = DateTime.UtcNow; DateTime dateTimeOutput = DateTime.UtcNow; DateTime expectedExpiry = dateTimeOutput; - var emailAddressPoints = new List() { new() }; + var emailAddressPoints = new List(); var emailRecipient = new EmailRecipient() { OrganizationNumber = "skd-orgno" }; EmailNotification expected = new() diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index f93a4d6b..6c1884cf 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -307,9 +307,9 @@ private static SmsNotificationService GetTestService(ISmsNotificationRepository? if (keywordsService == null) { - //var keywordsServiceMock = new Mock(); - //keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny())).ReturnsAsync((SmsRecipient recipient) => recipient); - //keywordsService = keywordsServiceMock.Object; + var keywordsServiceMock = new Mock(); + keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny>())).ReturnsAsync((List recipient) => recipient); + keywordsService = keywordsServiceMock.Object; } return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName }), keywordsService); From 2c8461e69d8ffe3710d54867ec2b59cfe39c17d2 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sat, 30 Nov 2024 20:20:23 +0100 Subject: [PATCH 55/75] Handle batch of organization and national identity numbers at one --- .../Interfaces/ISmsNotificationService.cs | 5 +- .../Services/SmsNotificationService.cs | 25 +--- .../Services/SmsOrderProcessingService.cs | 138 ++++++++++++++---- 3 files changed, 116 insertions(+), 52 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs index af3cb922..940fea8d 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs @@ -1,5 +1,6 @@ -using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; namespace Altinn.Notifications.Core.Services.Interfaces; @@ -11,7 +12,7 @@ public interface ISmsNotificationService /// /// Creates a new SMS notification based on the provided orderId and recipient /// - public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false, string? body = null); + public Task CreateNotification(Guid orderId, DateTime requestedSendTime, List smsAddresses, SmsRecipient smsRecipient, int smsCount, bool ignoreReservation = false); /// /// Starts the process of sending all ready SMS notifications diff --git a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs index 9be8f62b..aa853702 100644 --- a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -22,7 +22,6 @@ public class SmsNotificationService : ISmsNotificationService private readonly ISmsNotificationRepository _repository; private readonly IKafkaProducer _producer; private readonly string _smsQueueTopicName; - private readonly IKeywordsService _keywordsService; /// /// Initializes a new instance of the class. @@ -32,37 +31,19 @@ public SmsNotificationService( IDateTimeService dateTime, ISmsNotificationRepository repository, IKafkaProducer producer, - IOptions kafkaSettings, - IKeywordsService keywordsService) + IOptions kafkaSettings) { _guid = guid; _dateTime = dateTime; _repository = repository; _producer = producer; _smsQueueTopicName = kafkaSettings.Value.SmsQueueTopicName; - _keywordsService = keywordsService; } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false, string? smsBody = null) + public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, List smsAddresses, SmsRecipient smsRecipient, int smsCount, bool ignoreReservation = false) { - List smsAddresses = recipient.AddressInfo - .Where(a => a.AddressType == AddressType.Sms) - .Select(a => (a as SmsAddressPoint)!) - .Where(a => a != null && !string.IsNullOrEmpty(a.MobileNumber)) - .ToList(); - - SmsRecipient smsRecipient = new() - { - IsReserved = recipient.IsReserved, - OrganizationNumber = recipient.OrganizationNumber, - NationalIdentityNumber = recipient.NationalIdentityNumber, - CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(smsBody) || _keywordsService.ContainsRecipientNamePlaceholder(smsBody)) ? smsBody : null, - }; - - //smsRecipient = await _keywordsService.ReplaceKeywordsAsync(smsRecipient); - - if (recipient.IsReserved.HasValue && recipient.IsReserved.Value && !ignoreReservation) + if (smsRecipient.IsReserved.HasValue && smsRecipient.IsReserved.Value && !ignoreReservation) { await CreateNotificationForRecipient(orderId, requestedSendTime, smsRecipient, SmsNotificationResultType.Failed_RecipientReserved); return; diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs index 37069fe0..9fa8d6fd 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -19,47 +19,39 @@ public class SmsOrderProcessingService : ISmsOrderProcessingService private readonly ISmsNotificationRepository _smsNotificationRepository; private readonly ISmsNotificationService _smsService; private readonly IContactPointService _contactPointService; + private readonly IKeywordsService _keywordsService; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public SmsOrderProcessingService(ISmsNotificationRepository smsNotificationRepository, ISmsNotificationService smsService, IContactPointService contactPointService) + public SmsOrderProcessingService( + ISmsNotificationRepository smsNotificationRepository, + ISmsNotificationService smsService, + IContactPointService contactPointService, + IKeywordsService keywordsService) { _smsNotificationRepository = smsNotificationRepository; _smsService = smsService; _contactPointService = contactPointService; + _keywordsService = keywordsService; } /// public async Task ProcessOrder(NotificationOrder order) { - var recipients = order.Recipients; - var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); - await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + ArgumentNullException.ThrowIfNull(order); - await ProcessOrderWithoutAddressLookup(order, recipients); - } - - /// - public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) - { - int smsCount = GetSmsCountForOrder(order); - - var smsTemplate = order.Templates[0] as SmsTemplate; + var recipients = await UpdateRecipientsWithContactPointsAsync(order); - foreach (Recipient recipient in recipients) - { - await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount, order.IgnoreReservation ?? false, smsTemplate?.Body); - } + await ProcessOrderWithoutAddressLookup(order, recipients); } /// public async Task ProcessOrderRetry(NotificationOrder order) { - var recipients = order.Recipients; - var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); + ArgumentNullException.ThrowIfNull(order); - await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + var recipients = await UpdateRecipientsWithContactPointsAsync(order); await ProcessOrderRetryWithoutAddressLookup(order, recipients); } @@ -70,20 +62,110 @@ public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, int smsCount = GetSmsCountForOrder(order); List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); - foreach (Recipient recipient in recipients) + foreach (var recipient in recipients) { - SmsAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Sms) as SmsAddressPoint; + var smsAddress = recipient.AddressInfo.OfType().FirstOrDefault(); + + var smsRecipient = smsRecipients.Find(er => + er.MobileNumber == smsAddress?.MobileNumber && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); - if (!smsRecipients.Exists(sr => - sr.NationalIdentityNumber == recipient.NationalIdentityNumber - && sr.OrganizationNumber == recipient.OrganizationNumber - && sr.MobileNumber == addressPoint?.MobileNumber)) + if (smsRecipient == null || smsAddress == null) { - await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount); + continue; } + + await _smsService.CreateNotification( + order.Id, + order.RequestedSendTime, + [smsAddress], + smsRecipient, + smsCount); + } + } + + /// + public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) + { + int smsCount = GetSmsCountForOrder(order); + + var emailRecipients = await GetSmsRecipientsAsync(order, recipients); + + foreach (var recipient in recipients) + { + var emailRecipient = FindEmailRecipient(emailRecipients, recipient); + + var emailAddresses = recipient.AddressInfo + .OfType() + .Where(a => !string.IsNullOrWhiteSpace(a.MobileNumber)) + .ToList(); + + await _smsService.CreateNotification( + order.Id, + order.RequestedSendTime, + emailAddresses, + emailRecipient, + smsCount, + order.IgnoreReservation ?? false); } } + /// + /// Retrieves email recipients with replaced keywords. + /// + /// The notification order. + /// The list of recipients. + /// A task that represents the asynchronous operation. The task result contains the list of email recipients. + private async Task> GetSmsRecipientsAsync(NotificationOrder order, IEnumerable recipients) + { + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(order.Templates); + + var smsTemplate = order.Templates.OfType().FirstOrDefault(); + var smsRecipients = recipients.Select(recipient => new SmsRecipient + { + IsReserved = recipient.IsReserved, + OrganizationNumber = recipient.OrganizationNumber, + NationalIdentityNumber = recipient.NationalIdentityNumber, + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(smsTemplate?.Body) || _keywordsService.ContainsRecipientNamePlaceholder(smsTemplate?.Body)) ? smsTemplate?.Body : null, + }).ToList(); + + return await _keywordsService.ReplaceKeywordsAsync(smsRecipients); + } + + /// + /// Finds the email recipient matching the given recipient. + /// + /// The list of email recipients. + /// The recipient to match. + /// The matching email recipient, or null if no match is found. + private static SmsRecipient? FindEmailRecipient(IEnumerable emailRecipients, Recipient recipient) + { + return emailRecipients.FirstOrDefault(er => + (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || + (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); + } + + /// + /// Updates the recipients with contact points. + /// + /// The notification order. + /// A task that represents the asynchronous operation. The task result contains the updated list of recipients. + private async Task> UpdateRecipientsWithContactPointsAsync(NotificationOrder order) + { + var recipientsWithoutMobileNumber = order.Recipients + .Where(r => !r.AddressInfo.Exists(a => a.AddressType == AddressType.Sms)) + .ToList(); + + if (recipientsWithoutMobileNumber.Count != 0) + { + await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + } + + return order.Recipients; + } + /// /// Calculates the number of messages based on the rules for concatenation of SMS messages in the SMS gateway. /// From 755cd8cb28e7b537f37f8356c59c8d384ef1631c Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sat, 30 Nov 2024 21:06:16 +0100 Subject: [PATCH 56/75] Update the test units to reflect latest changes --- .../SmsNotificationServiceTests.cs | 58 +++++-------------- .../SmsOrderProcessingServiceTests.cs | 36 ++++++++---- 2 files changed, 39 insertions(+), 55 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index 6c1884cf..e39757cf 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Altinn.Notifications.Core.Configuration; @@ -26,30 +27,6 @@ public class SmsNotificationServiceTests private const string _smsQueueTopicName = "test.sms.queue"; private readonly Sms _sms = new(Guid.NewGuid(), "Altinn Test", "Recipient", "Text message"); - [Fact] - public async Task CreateNotifications_NewSmsNotification_RepositoryCalledOnce() - { - // Arrange - var repoMock = new Mock(); - var guidService = new Mock(); - guidService - .Setup(g => g.NewGuid()) - .Returns(Guid.NewGuid()); - - var dateTimeService = new Mock(); - dateTimeService - .Setup(d => d.UtcNow()) - .Returns(DateTime.UtcNow); - - var service = GetTestService(repo: repoMock.Object); - - // Act - await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, new Recipient(new List() { new SmsAddressPoint("999999999") }, nationalIdentityNumber: "enduser-nin"), It.IsAny()); - - // Assert - repoMock.Verify(r => r.AddNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - [Fact] public async Task CreateNotification_RecipientNumberIsDefined_ResultNew() { @@ -78,7 +55,7 @@ public async Task CreateNotification_RecipientNumberIsDefined_ResultNew() var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List() { new SmsAddressPoint("+4799999999") }), 1); + await service.CreateNotification(orderId, requestedSendTime, [new("+4799999999")], new SmsRecipient(), 1); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry), It.IsAny()), Times.Once); @@ -112,7 +89,7 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsFalse var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient() { IsReserved = true }, 1); + await service.CreateNotification(orderId, requestedSendTime, [], new SmsRecipient { IsReserved = true }, 1); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry), It.IsAny()), Times.Once); @@ -147,14 +124,14 @@ public async Task CreateNotification_RecipientIsReserved_IgnoreReservationsTrue_ var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient() { IsReserved = true, AddressInfo = [new SmsAddressPoint("+4799999999")] }, 1, true); + await service.CreateNotification(orderId, requestedSendTime, [new("+4799999999")], new SmsRecipient { IsReserved = true }, 1, true); // Assert repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry), It.IsAny()), Times.Once); } [Fact] - public async Task CreateNotification_RecipientNumberMissing_LookupFails_ResultFailedRecipientNotDefined() + public async Task CreateNotification_RecipientNumberMissing_LookupFails_ResultFailedRecipientNotIdentified() { // Arrange Guid id = Guid.NewGuid(); @@ -177,10 +154,10 @@ public async Task CreateNotification_RecipientNumberMissing_LookupFails_ResultFa var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); // Act - await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List()), It.IsAny()); + await service.CreateNotification(orderId, requestedSendTime, [], new SmsRecipient(), 1); // Assert - repoMock.Verify(); + repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry), It.IsAny()), Times.Once); } [Fact] @@ -190,7 +167,7 @@ public async Task CreateNotification_RecipientHasTwoMobileNumbers_RepositoryCall Recipient recipient = new() { OrganizationNumber = "org", - AddressInfo = [new SmsAddressPoint("+4748123456"), new SmsAddressPoint("+4799123456")] + AddressInfo = new List { new SmsAddressPoint("+4748123456"), new SmsAddressPoint("+4799123456") } }; var repoMock = new Mock(); @@ -199,7 +176,7 @@ public async Task CreateNotification_RecipientHasTwoMobileNumbers_RepositoryCall var service = GetTestService(repo: repoMock.Object); // Act - await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, recipient, 1, true); + await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, recipient.AddressInfo.OfType().ToList(), new SmsRecipient { OrganizationNumber = "org" }, 1, true); // Assert repoMock.Verify(r => r.AddNotification(It.Is(s => s.Recipient.OrganizationNumber == "org"), It.IsAny(), It.IsAny()), Times.Exactly(2)); @@ -211,7 +188,7 @@ public async Task SendNotifications_ProducerCalledOnceForEachRetrievedSms() // Arrange var repoMock = new Mock(); repoMock.Setup(r => r.GetNewNotifications()) - .ReturnsAsync(new List() { _sms, _sms, _sms }); + .ReturnsAsync(new List { _sms, _sms, _sms }); var producerMock = new Mock(); producerMock.Setup(p => p.ProduceAsync(It.Is(s => s.Equals(_smsQueueTopicName)), It.IsAny())) @@ -233,7 +210,7 @@ public async Task SendNotifications_ProducerReturnsFalse_RepositoryCalledToUpdat // Arrange var repoMock = new Mock(); repoMock.Setup(r => r.GetNewNotifications()) - .ReturnsAsync(new List() { _sms }); + .ReturnsAsync(new List { _sms }); repoMock .Setup(r => r.UpdateSendStatus(It.IsAny(), It.Is(t => t == SmsNotificationResultType.New), It.IsAny())); @@ -254,7 +231,7 @@ public async Task SendNotifications_ProducerReturnsFalse_RepositoryCalledToUpdat } [Fact] - public async Task UpdateSendStatus_SendResultDefined_Succeded() + public async Task UpdateSendStatus_SendResultDefined_Succeeded() { // Arrange Guid notificationid = Guid.NewGuid(); @@ -282,7 +259,7 @@ public async Task UpdateSendStatus_SendResultDefined_Succeded() repoMock.Verify(); } - private static SmsNotificationService GetTestService(ISmsNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null, IKeywordsService? keywordsService = null) + private static SmsNotificationService GetTestService(ISmsNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) { var guidService = new Mock(); guidService @@ -305,13 +282,6 @@ private static SmsNotificationService GetTestService(ISmsNotificationRepository? producer = producerMock.Object; } - if (keywordsService == null) - { - var keywordsServiceMock = new Mock(); - keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny>())).ReturnsAsync((List recipient) => recipient); - keywordsService = keywordsServiceMock.Object; - } - - return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName }), keywordsService); + return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo, producer, Options.Create(new KafkaSettings { SmsQueueTopicName = _smsQueueTopicName })); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs index 81c28ef2..d98bde72 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs @@ -41,10 +41,16 @@ public async Task ProcessOrder_ExpectedInputToService() Templates = [new SmsTemplate("Altinn", "this is the body")] }; - Recipient expectedRecipient = new(new List() { new SmsAddressPoint("+4799999999") }, nationalIdentityNumber: "enduser-nin"); + var smsAddressPoints = new List { new("+4799999999"), }; var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)), It.IsAny(), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification( + It.IsAny(), + It.Is(d => d.Equals(requested)), + It.Is>(r => AssertUtils.AreEquivalent(smsAddressPoints, r)), + It.Is(r => r.NationalIdentityNumber == "enduser-nin"), + It.IsAny(), + It.IsAny())); var service = GetTestService(smsService: notificationServiceMock.Object); @@ -80,7 +86,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() }; var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())); var service = GetTestService(smsService: notificationServiceMock.Object); @@ -88,7 +94,7 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() await service.ProcessOrder(order); // Assert - notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] @@ -114,10 +120,10 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC s => s.CreateNotification( It.IsAny(), It.IsAny(), - It.Is(r => r.NationalIdentityNumber == "123456"), + It.IsAny>(), + It.Is(r => r.NationalIdentityNumber == "123456"), It.IsAny(), - It.IsAny(), - It.IsAny())); + It.IsAny())); var contactPointServiceMock = new Mock(); contactPointServiceMock.Setup(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1), It.IsAny())) @@ -157,7 +163,7 @@ public async Task ProcessOrderRetry_NotificationServiceCalledIfRecipientNotInDat }; var notificationServiceMock = new Mock(); - notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + notificationServiceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())); var smsRepoMock = new Mock(); smsRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync( @@ -173,7 +179,7 @@ public async Task ProcessOrderRetry_NotificationServiceCalledIfRecipientNotInDat // Assert smsRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); - notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Theory] @@ -196,7 +202,8 @@ public void CalculateNumberOfMessages_MessageWithSymbolsAreEncodedBeforeCalculat private static SmsOrderProcessingService GetTestService( ISmsNotificationRepository? smsRepo = null, ISmsNotificationService? smsService = null, - IContactPointService? contactPointService = null) + IContactPointService? contactPointService = null, + IKeywordsService? keywordsService = null) { if (smsRepo == null) { @@ -218,6 +225,13 @@ private static SmsOrderProcessingService GetTestService( contactPointService = contactPointServiceMock.Object; } - return new SmsOrderProcessingService(smsRepo, smsService, contactPointService); + if (keywordsService == null) + { + var keywordsServiceMock = new Mock(); + keywordsServiceMock.Setup(e => e.ReplaceKeywordsAsync(It.IsAny>())).ReturnsAsync((List recipient) => recipient); + keywordsService = keywordsServiceMock.Object; + } + + return new SmsOrderProcessingService(smsRepo, smsService, contactPointService, keywordsService); } } From 0d163ca90b376c08d3e6d19721961ab5e8234c37 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 09:33:45 +0100 Subject: [PATCH 57/75] Improve the readability --- .../Services/KeywordsService.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 7b903730..bdd8f461 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -113,55 +113,55 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< /// /// Replaces placeholders in the given text with actual values from the provided details. /// - /// The text containing placeholders. + /// The text containing placeholders. /// The organization number. /// The national identity number. /// The list of organization details. /// The list of person details. /// The text with placeholders replaced by actual values. private static string? ReplacePlaceholders( - string? text, + string? customizedText, string? organizationNumber, string? nationalIdentityNumber, IEnumerable organizationDetails, IEnumerable personDetails) { - text = ReplaceWithDetails(text, organizationNumber, organizationDetails, p => p.OrganizationNumber); + customizedText = ReplaceWithDetails(customizedText, organizationNumber, organizationDetails, p => p.OrganizationNumber); - text = ReplaceWithDetails(text, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber); + customizedText = ReplaceWithDetails(customizedText, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber); - return text; + return customizedText; } /// /// Replaces placeholders in the given text with actual values from the provided details. /// - /// The text containing placeholders. - /// The key to match in the details. + /// The text containing placeholders. + /// The key to match in the details. /// The list of details. /// The function to select the key from the details. /// The text with placeholders replaced by actual values. private static string? ReplaceWithDetails( - string? text, - string? key, + string? customizedText, + string? searchKey, IEnumerable details, Func keySelector) { - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrWhiteSpace(searchKey)) { - return text; + return customizedText; } - var detail = details.FirstOrDefault(e => keySelector(e) == key); + var detail = details.FirstOrDefault(e => keySelector(e) == searchKey); if (detail != null) { - text = text?.Replace(_recipientNamePlaceholder, detail.Name ?? string.Empty); + customizedText = customizedText?.Replace(_recipientNamePlaceholder, detail.Name ?? string.Empty); - text = text?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); + customizedText = customizedText?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); } - return text; + return customizedText; } } } From cba7bf9b86fa4f159910cf930cd3355badddd7b9 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 09:40:14 +0100 Subject: [PATCH 58/75] Update the SQL query used to retrieve recipients --- .../Migration/FunctionsAndProcedures/getemailrecipients.sql | 6 ++---- .../Repository/EmailNotificationRepository.cs | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql index 75bbac48..cff77f0a 100644 --- a/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql +++ b/src/Altinn.Notifications.Persistence/Migration/FunctionsAndProcedures/getemailrecipients.sql @@ -2,9 +2,7 @@ CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid RETURNS TABLE( recipientorgno text, recipientnin text, - toaddress text, - customizedbody text, - customizedsubject text + toaddress text ) LANGUAGE 'plpgsql' AS $BODY$ @@ -13,7 +11,7 @@ __orderid BIGINT := (SELECT _id from notifications.orders where alternateid = _alternateid); BEGIN RETURN query - SELECT e.recipientorgno, e.recipientnin, e.toaddress, e.customizedbody, e.customizedsubject + SELECT e.recipientorgno, e.recipientnin, e.toaddress FROM notifications.emailnotifications e WHERE e._orderid = __orderid; END; diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 99d9133c..afd602b1 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -119,9 +119,7 @@ public async Task> GetRecipients(Guid orderId) searchResult.Add(new EmailRecipient() { ToAddress = reader.GetValue("toaddress"), - CustomizedBody = reader.GetValue("customizedbody"), OrganizationNumber = reader.GetValue("recipientorgno"), - CustomizedSubject = reader.GetValue("customizedsubject"), NationalIdentityNumber = reader.GetValue("recipientnin"), }); } From 31064d713cb38f8f58da722d3efe2950dae4eaca Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 10:07:15 +0100 Subject: [PATCH 59/75] Implement simple test units to text keyword replacement logic --- .../TestingServices/KeywordsServiceTests.cs | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs new file mode 100644 index 00000000..67a8d500 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs @@ -0,0 +1,195 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Models.Parties; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; + +public class KeywordsServiceTests +{ + private readonly Mock _registerClientMock; + private readonly IKeywordsService _keywordsService; + + public KeywordsServiceTests() + { + _registerClientMock = new Mock(); + _keywordsService = new KeywordsService(_registerClientMock.Object); + } + + [Fact] + public void ContainsRecipientNamePlaceholder_ShouldReturnTrue_WhenPlaceholderExists() + { + // Arrange + var value = "Hello $recipientName$"; + + // Act + var result = _keywordsService.ContainsRecipientNamePlaceholder(value); + + // Assert + Assert.True(result); + } + + [Fact] + public void ContainsRecipientNamePlaceholder_ShouldReturnFalse_WhenPlaceholderDoesNotExist() + { + // Arrange + var value = "Hello World"; + + // Act + var result = _keywordsService.ContainsRecipientNamePlaceholder(value); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsRecipientNumberPlaceholder_ShouldReturnTrue_WhenPlaceholderExists() + { + // Arrange + var value = "Your number is $recipientNumber$"; + + // Act + var result = _keywordsService.ContainsRecipientNumberPlaceholder(value); + + // Assert + Assert.True(result); + } + + [Fact] + public void ContainsRecipientNumberPlaceholder_ShouldReturnFalse_WhenPlaceholderDoesNotExist() + { + // Arrange + var value = "Your number is 12345"; + + // Act + var result = _keywordsService.ContainsRecipientNumberPlaceholder(value); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task ReplaceKeywordsAsync_EmailRecipients_ShouldReplacePlaceholdersForPersons() + { + // Arrange + var emailRecipients = new List + { + new() + { + NationalIdentityNumber = "07837399275", + CustomizedBody = "Hello $recipientName$", + CustomizedSubject = "Subject $recipientNumber$", + } + }; + + var personDetails = new List + { + new() { NationalIdentityNumber = "07837399275", Name = "Person name" } + }; + + _registerClientMock.Setup(client => client.GetPartyDetailsForPersons(It.IsAny>())).ReturnsAsync(personDetails); + + // Act + var result = await _keywordsService.ReplaceKeywordsAsync(emailRecipients); + + // Assert + var recipient = result.First(); + Assert.Equal("Hello Person name", recipient.CustomizedBody); + Assert.Equal("Subject 07837399275", recipient.CustomizedSubject); + } + + [Fact] + public async Task ReplaceKeywordsAsync_EmailRecipients_ShouldReplacePlaceholdersForOrganizations() + { + // Arrange + var emailRecipients = new List + { + new() + { + OrganizationNumber = "313997901", + CustomizedBody = "Hello $recipientName$", + CustomizedSubject = "Subject $recipientNumber$", + } + }; + + var organizationDetails = new List + { + new() { OrganizationNumber = "313997901", Name = "Organization name" } + }; + + _registerClientMock.Setup(client => client.GetPartyDetailsForOrganizations(It.IsAny>())).ReturnsAsync(organizationDetails); + + // Act + var result = await _keywordsService.ReplaceKeywordsAsync(emailRecipients); + + // Assert + var recipient = result.First(); + Assert.Equal("Subject 313997901", recipient.CustomizedSubject); + Assert.Equal("Hello Organization name", recipient.CustomizedBody); + } + + [Fact] + public async Task ReplaceKeywordsAsync_SmsRecipient_ShouldReplacePlaceholdersForPersons() + { + // Arrange + var emailRecipients = new List + { + new() + { + NationalIdentityNumber = "07837399275", + CustomizedBody = "Hello $recipientName$ your national identity number is $recipientNumber$" + } + }; + + var personDetails = new List + { + new() { NationalIdentityNumber = "07837399275", Name = "Person name" } + }; + + _registerClientMock.Setup(client => client.GetPartyDetailsForPersons(It.IsAny>())).ReturnsAsync(personDetails); + + // Act + var result = await _keywordsService.ReplaceKeywordsAsync(emailRecipients); + + // Assert + var recipient = result.First(); + Assert.Equal("Hello Person name your national identity number is 07837399275", recipient.CustomizedBody); + } + + [Fact] + public async Task ReplaceKeywordsAsync_SmsRecipients_ShouldReplacePlaceholdersForOrganizations() + { + // Arrange + var emailRecipients = new List + { + new() + { + OrganizationNumber = "313418154", + CustomizedBody = "Hello $recipientName$ your organization number is $recipientNumber$" + } + }; + + var organizationDetails = new List + { + new() { OrganizationNumber = "313418154", Name = "Organization name" } + }; + + _registerClientMock.Setup(client => client.GetPartyDetailsForOrganizations(It.IsAny>())).ReturnsAsync(organizationDetails); + + // Act + var result = await _keywordsService.ReplaceKeywordsAsync(emailRecipients); + + // Assert + var recipient = result.First(); + Assert.Equal("Hello Organization name your organization number is 313418154", recipient.CustomizedBody); + } +} From 94a291c58c67cf4b0d0499b3608f9b6b0abdae97 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 10:54:06 +0100 Subject: [PATCH 60/75] Update unit tests and test data --- .../Services/EmailOrderProcessingService.cs | 17 +++++++++++++---- .../Utils/TestdataUtil.cs | 1 + .../SmsNotificationServiceTests.cs | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index ac5be964..489bd722 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -72,16 +72,24 @@ public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, er.OrganizationNumber == recipient.OrganizationNumber && er.NationalIdentityNumber == recipient.NationalIdentityNumber); - if (emailRecipient == null || addressPoint == null) + if (emailRecipient == null && addressPoint == null) { continue; } + if (emailRecipient == null && addressPoint != null) + { + emailRecipient = new() + { + ToAddress = addressPoint.EmailAddress + }; + } + await _emailService.CreateNotification( order.Id, order.RequestedSendTime, [addressPoint], - emailRecipient, + emailRecipient!, order.IgnoreReservation ?? false); } } @@ -93,13 +101,14 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List foreach (var recipient in recipients) { - var emailRecipient = FindEmailRecipient(emailRecipients, recipient); - var emailAddresses = recipient.AddressInfo .OfType() .Where(a => !string.IsNullOrWhiteSpace(a.EmailAddress)) .ToList(); + var emailRecipient = FindEmailRecipient(emailRecipients, recipient); + emailRecipient ??= new() { ToAddress = emailAddresses[0].EmailAddress }; + await _emailService.CreateNotification( order.Id, order.RequestedSendTime, diff --git a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs index 3bf535fb..3d0f0028 100644 --- a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs +++ b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs @@ -79,6 +79,7 @@ public static NotificationOrder NotificationOrder_EmailTemplate_OneRecipient() { new Recipient() { + OrganizationNumber = "314396189", AddressInfo = new() { new EmailAddressPoint() diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index e39757cf..243c3a46 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -157,7 +157,7 @@ public async Task CreateNotification_RecipientNumberMissing_LookupFails_ResultFa await service.CreateNotification(orderId, requestedSendTime, [], new SmsRecipient(), 1); // Assert - repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry), It.IsAny()), Times.Once); + repoMock.Verify(); } [Fact] From cabb8d97af9f803705384defa708c08033679627 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 12:08:34 +0100 Subject: [PATCH 61/75] Fix a test unit --- .../Services/EmailOrderProcessingService.cs | 10 +--------- .../PastDueOrdersRetryConsumerTests.cs | 8 ++++---- .../EmailOrderProcessingServiceTests.cs | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index 489bd722..389ee89b 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -72,19 +72,11 @@ public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, er.OrganizationNumber == recipient.OrganizationNumber && er.NationalIdentityNumber == recipient.NationalIdentityNumber); - if (emailRecipient == null && addressPoint == null) + if (emailRecipient == null) { continue; } - if (emailRecipient == null && addressPoint != null) - { - emailRecipient = new() - { - ToAddress = addressPoint.EmailAddress - }; - } - await _emailService.CreateNotification( order.Id, order.RequestedSendTime, diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs index 152b0d76..b817de99 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs @@ -23,10 +23,10 @@ public async Task RunTask_ConfirmExpectedSideEffects() { // Arrange Dictionary vars = new() - { - { "KafkaSettings__PastDueOrdersRetryTopicName", _retryTopicName }, - { "KafkaSettings__Admin__TopicList", $"[\"{_retryTopicName}\"]" } - }; + { + { "KafkaSettings__PastDueOrdersRetryTopicName", _retryTopicName }, + { "KafkaSettings__Admin__TopicList", $"[\"{_retryTopicName}\"]" } + }; using PastDueOrdersRetryConsumer consumerRetryService = (PastDueOrdersRetryConsumer)ServiceUtil .GetServices(new List() { typeof(IHostedService) }, vars) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index 68a9f3f3..bcde6902 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -98,10 +98,20 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC var order = new NotificationOrder() { NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { + Recipients = + [ new() - } + { + AddressInfo = + [ + new EmailAddressPoint() + { + AddressType = AddressType.Email, + EmailAddress = "recipient@domain.com" + } + ] + } + ] }; var serviceMock = new Mock(); From c9cfc20ecec69843c438de4fabaade3582e75595 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Sun, 1 Dec 2024 12:59:52 +0100 Subject: [PATCH 62/75] Update the order logic to handle orders sent to recipients without using national identity and organization numbers --- .../Services/EmailOrderProcessingService.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index 389ee89b..aed5cc96 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -61,27 +61,31 @@ public async Task ProcessOrderRetry(NotificationOrder order) /// public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { + var allEmailRecipients = await GetEmailRecipientsAsync(order, recipients); var emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); foreach (var recipient in recipients) { var addressPoint = recipient.AddressInfo.OfType().FirstOrDefault(); - var emailRecipient = emailRecipients.Find(er => - er.ToAddress == addressPoint?.EmailAddress && - er.OrganizationNumber == recipient.OrganizationNumber && - er.NationalIdentityNumber == recipient.NationalIdentityNumber); + var isEmailRecipientMissing = !emailRecipients.Exists( + er => er.ToAddress == addressPoint?.EmailAddress && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); - if (emailRecipient == null) + if (!isEmailRecipientMissing) { continue; } + var missingEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); + missingEmailRecipient ??= new EmailRecipient { IsReserved = recipient.IsReserved }; + await _emailService.CreateNotification( order.Id, order.RequestedSendTime, [addressPoint], - emailRecipient!, + missingEmailRecipient, order.IgnoreReservation ?? false); } } @@ -99,7 +103,7 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List .ToList(); var emailRecipient = FindEmailRecipient(emailRecipients, recipient); - emailRecipient ??= new() { ToAddress = emailAddresses[0].EmailAddress }; + emailRecipient ??= new EmailRecipient { IsReserved = recipient.IsReserved }; await _emailService.CreateNotification( order.Id, @@ -143,8 +147,9 @@ private async Task> GetEmailRecipientsAsync(Notifica private static EmailRecipient? FindEmailRecipient(IEnumerable emailRecipients, Recipient recipient) { return emailRecipients.FirstOrDefault(er => - (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || - (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); + (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || + (recipient.AddressInfo != null && recipient.AddressInfo.OfType().Any(e => e.EmailAddress == er.ToAddress)) || + (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); } /// From e40ade7ffdc4bbd10dbbf6dc27e6c2303c601ba5 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 2 Dec 2024 08:02:27 +0100 Subject: [PATCH 63/75] Added test units to test retrieving party details using organization and national identity numbers. --- .../Models/Parties/PartyDetailsLookupBatch.cs | 35 +++- .../Services/KeywordsService.cs | 8 +- .../Register/RegisterClientTests.cs | 154 ++++++++++++++++-- 3 files changed, 172 insertions(+), 25 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs index 975bef87..983e347a 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -12,7 +12,8 @@ public class PartyDetailsLookupBatch /// /// A list of organization numbers to look up. /// A list of social security numbers to look up. - /// Thrown when both and are null or empty. + /// Thrown when both and are provided simultaneously or when both are null or empty. + [JsonConstructor] public PartyDetailsLookupBatch(List? organizationNumbers = null, List? socialSecurityNumbers = null) { if ((organizationNumbers == null || organizationNumbers.Count == 0) && (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0)) @@ -20,22 +21,42 @@ public PartyDetailsLookupBatch(List? organizationNumbers = null, List 0 && socialSecurityNumbers != null && socialSecurityNumbers.Count > 0) + { + throw new ArgumentException("Both organizationNumbers and socialSecurityNumbers cannot be provided simultaneously. Please provide only one."); + } + + OrganizationNumbers = organizationNumbers ?? []; + SocialSecurityNumbers = socialSecurityNumbers ?? []; + PartyDetailsLookupRequestList = []; - if (organizationNumbers != null) + if (OrganizationNumbers.Count != 0) { - PartyDetailsLookupRequestList.AddRange(organizationNumbers.Select(orgNu => new PartyDetailsLookupRequest(organizationNumber: orgNu))); + PartyDetailsLookupRequestList.AddRange(OrganizationNumbers.Select(orgNum => new PartyDetailsLookupRequest(organizationNumber: orgNum))); } - if (socialSecurityNumbers != null) + if (SocialSecurityNumbers.Count != 0) { - PartyDetailsLookupRequestList.AddRange(socialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn))); + PartyDetailsLookupRequestList.AddRange(SocialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn))); } } /// - /// Gets or sets the list of lookup criteria for parties. + /// Gets the organization numbers to look up. + /// + [JsonPropertyName("organizationNumbers")] + public List OrganizationNumbers { get; } + + /// + /// Gets the social security numbers to look up. + /// + [JsonPropertyName("socialSecurityNumbers")] + public List SocialSecurityNumbers { get; } + + /// + /// Gets the list of lookup criteria for parties. /// [JsonPropertyName("parties")] - public List? PartyDetailsLookupRequestList { get; } + public List PartyDetailsLookupRequestList { get; private set; } } diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index bdd8f461..7eb65341 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -97,14 +97,14 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< List organizationNumbers, List nationalIdentityNumbers) { - var organizationDetailsTask = organizationNumbers.Count != 0 - ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) - : Task.FromResult(new List()); - var personDetailsTask = nationalIdentityNumbers.Count != 0 ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) : Task.FromResult(new List()); + var organizationDetailsTask = organizationNumbers.Count != 0 + ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) + : Task.FromResult(new List()); + await Task.WhenAll(personDetailsTask, organizationDetailsTask); return (personDetailsTask.Result, organizationDetailsTask.Result); diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs index 154f9f4b..0083d86f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs @@ -6,8 +6,8 @@ using System.Text.Json; using System.Threading.Tasks; -using Altinn.Notifications.Core; using Altinn.Notifications.Core.Models.ContactPoints; +using Altinn.Notifications.Core.Models.Parties; using Altinn.Notifications.Core.Shared; using Altinn.Notifications.Integrations.Configuration; using Altinn.Notifications.Integrations.Register; @@ -30,15 +30,20 @@ public class RegisterClientTests public RegisterClientTests() { var registerHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => - { - if (request!.RequestUri!.AbsolutePath.EndsWith("contactpoint/lookup")) - { - OrgContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), JsonSerializerOptionsProvider.Options); - return await GetResponse(lookup!); - } + { + if (request!.RequestUri!.AbsolutePath.EndsWith("contactpoint/lookup")) + { + OrgContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), _serializerOptions); + return await GetResponse(lookup!); + } + else if (request!.RequestUri!.AbsolutePath.EndsWith("nameslookup")) + { + PartyDetailsLookupBatch? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), _serializerOptions); + return await GetPartyDetailsResponse(lookup!); + } - return new HttpResponseMessage(HttpStatusCode.NotFound); - }); + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); PlatformSettings settings = new() { @@ -46,8 +51,8 @@ public RegisterClientTests() }; _registerClient = new RegisterClient( - new HttpClient(registerHttpMessageHandler), - Options.Create(settings)); + new HttpClient(registerHttpMessageHandler), + Options.Create(settings)); } [Fact] @@ -81,6 +86,68 @@ public async Task GetOrganizationContactPoints_FailureResponse_ExceptionIsThrown Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } + [Fact] + public async Task GetPartyDetailsForOrganizations_SuccessResponse_NoMatches() + { + // Act + List actual = await _registerClient.GetPartyDetailsForOrganizations(["empty-list"]); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public async Task GetPartyDetailsForOrganizations_SuccessResponse_TwoElementsInResponse() + { + // Act + List actual = await _registerClient.GetPartyDetailsForOrganizations(["populated-list"]); + + // Assert + Assert.Equal(2, actual.Count); + Assert.Contains("313600947", actual.Select(pd => pd.OrganizationNumber)); + } + + [Fact] + public async Task GetPartyDetailsForOrganizations_FailureResponse_ExceptionIsThrown() + { + // Act + var exception = await Assert.ThrowsAsync(async () => await _registerClient.GetPartyDetailsForOrganizations(["unavailable"])); + + Assert.StartsWith("503 - Service Unavailable", exception.Message); + Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); + } + + [Fact] + public async Task GetPartyDetailsForPersons_SuccessResponse_NoMatches() + { + // Act + List actual = await _registerClient.GetPartyDetailsForPersons(["empty-list"]); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public async Task GetPartyDetailsForPersons_SuccessResponse_TwoElementsInResponse() + { + // Act + List actual = await _registerClient.GetPartyDetailsForPersons(["populated-list"]); + + // Assert + Assert.Equal(2, actual.Count); + Assert.Contains("04917199103", actual.Select(pd => pd.NationalIdentityNumber)); + } + + [Fact] + public async Task GetPartyDetailsForPersons_FailureResponse_ExceptionIsThrown() + { + // Act + var exception = await Assert.ThrowsAsync(async () => await _registerClient.GetPartyDetailsForPersons(["unavailable"])); + + Assert.StartsWith("503 - Service Unavailable", exception.Message); + Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); + } + private Task GetResponse(OrgContactPointLookup lookup) { HttpStatusCode statusCode = HttpStatusCode.OK; @@ -89,18 +156,77 @@ private Task GetResponse(OrgContactPointLookup lookup) switch (lookup.OrganizationNumbers[0]) { case "empty-list": - contentData = new OrgContactPointsList() { ContactPointsList = new List() }; + contentData = new OrgContactPointsList() { ContactPointsList = [] }; break; case "populated-list": contentData = new OrgContactPointsList { ContactPointsList = [ - new OrganizationContactPoints() { OrganizationNumber = "910011154", EmailList = [] }, - new OrganizationContactPoints() { OrganizationNumber = "910011155", EmailList = [] } + new() { OrganizationNumber = "910011154", EmailList = [] }, + new() { OrganizationNumber = "910011155", EmailList = [] } + ] + }; + break; + case "unavailable": + statusCode = HttpStatusCode.ServiceUnavailable; + break; + } + + JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; + + return Task.FromResult( + new HttpResponseMessage() + { + StatusCode = statusCode, + Content = content + }); + } + + private Task GetPartyDetailsResponse(PartyDetailsLookupBatch lookup) + { + HttpStatusCode statusCode = HttpStatusCode.OK; + object? contentData = null; + + switch (lookup.PartyDetailsLookupRequestList?.FirstOrDefault()?.OrganizationNumber) + { + case "empty-list": + contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; + break; + + case "populated-list": + contentData = new PartyDetailsLookupResult + { + PartyDetailsList = + [ + new() { OrganizationNumber = "313600947", Name = "Test Organization 1" }, + new() { OrganizationNumber = "315058384", Name = "Test Organization 2" } + ] + }; + break; + + case "unavailable": + statusCode = HttpStatusCode.ServiceUnavailable; + break; + } + + switch (lookup.PartyDetailsLookupRequestList?.FirstOrDefault()?.SocialSecurityNumber) + { + case "empty-list": + contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; + break; + + case "populated-list": + contentData = new PartyDetailsLookupResult + { + PartyDetailsList = + [ + new() { NationalIdentityNumber = "07837399275", Name = "Test Person 1" }, + new() { NationalIdentityNumber = "04917199103", Name = "Test Person 2" } ] }; break; + case "unavailable": statusCode = HttpStatusCode.ServiceUnavailable; break; From 46f13b2d0aa2bd14fc23e8299de98a1155b25f2d Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 2 Dec 2024 08:44:27 +0100 Subject: [PATCH 64/75] Allow the user to retrieve party detail for both organization and national identity number in the same request. --- .../Models/Parties/PartyDetailsLookupBatch.cs | 7 +-- .../Services/EmailOrderProcessingService.cs | 52 +++++++++++-------- .../Interfaces/IEmailNotificationService.cs | 2 +- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs index 983e347a..7af07f89 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -12,7 +12,7 @@ public class PartyDetailsLookupBatch /// /// A list of organization numbers to look up. /// A list of social security numbers to look up. - /// Thrown when both and are provided simultaneously or when both are null or empty. + /// Thrown when both and are null or empty. [JsonConstructor] public PartyDetailsLookupBatch(List? organizationNumbers = null, List? socialSecurityNumbers = null) { @@ -21,11 +21,6 @@ public PartyDetailsLookupBatch(List? organizationNumbers = null, List 0 && socialSecurityNumbers != null && socialSecurityNumbers.Count > 0) - { - throw new ArgumentException("Both organizationNumbers and socialSecurityNumbers cannot be provided simultaneously. Please provide only one."); - } - OrganizationNumbers = organizationNumbers ?? []; SocialSecurityNumbers = socialSecurityNumbers ?? []; diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index aed5cc96..7bd67ea5 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -62,30 +62,29 @@ public async Task ProcessOrderRetry(NotificationOrder order) public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { var allEmailRecipients = await GetEmailRecipientsAsync(order, recipients); - var emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); + var registeredEmailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); foreach (var recipient in recipients) { var addressPoint = recipient.AddressInfo.OfType().FirstOrDefault(); - var isEmailRecipientMissing = !emailRecipients.Exists( - er => er.ToAddress == addressPoint?.EmailAddress && - er.OrganizationNumber == recipient.OrganizationNumber && - er.NationalIdentityNumber == recipient.NationalIdentityNumber); - - if (!isEmailRecipientMissing) + var isEmailRecipientRegistered = + registeredEmailRecipients.Exists(er => er.ToAddress == addressPoint?.EmailAddress && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); + if (isEmailRecipientRegistered) { continue; } - var missingEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); - missingEmailRecipient ??= new EmailRecipient { IsReserved = recipient.IsReserved }; + var matchedEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); + var emailRecipient = matchedEmailRecipient ?? new EmailRecipient { IsReserved = recipient.IsReserved }; await _emailService.CreateNotification( order.Id, order.RequestedSendTime, [addressPoint], - missingEmailRecipient, + emailRecipient, order.IgnoreReservation ?? false); } } @@ -93,7 +92,7 @@ await _emailService.CreateNotification( /// public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) { - var emailRecipients = await GetEmailRecipientsAsync(order, recipients); + var allEmailRecipients = await GetEmailRecipientsAsync(order, recipients); foreach (var recipient in recipients) { @@ -102,8 +101,8 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List .Where(a => !string.IsNullOrWhiteSpace(a.EmailAddress)) .ToList(); - var emailRecipient = FindEmailRecipient(emailRecipients, recipient); - emailRecipient ??= new EmailRecipient { IsReserved = recipient.IsReserved }; + var matchedEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); + var emailRecipient = matchedEmailRecipient ?? new EmailRecipient { IsReserved = recipient.IsReserved }; await _emailService.CreateNotification( order.Id, @@ -115,24 +114,36 @@ await _emailService.CreateNotification( } /// - /// Retrieves email recipients with replaced keywords. + /// Determines whether the specified template part requires customization by checking for placeholder keywords. /// - /// The notification order. - /// The list of recipients. - /// A task that represents the asynchronous operation. The task result contains the list of email recipients. + /// The part of the email template (subject or body) to evaluate. + /// true if the template part contains placeholders for recipient-specific customization; otherwise, false. + private bool RequiresCustomization(string? templatePart) + { + return _keywordsService.ContainsRecipientNumberPlaceholder(templatePart) || _keywordsService.ContainsRecipientNamePlaceholder(templatePart); + } + + /// + /// Retrieves a list of recipients for sending emails, replacing keywords in the subject and body with actual values. + /// + /// The notification order containing the email template and recipients. + /// The list of recipients to process. + /// A task that represents the asynchronous operation. The task result contains the list of email recipients with keywords replaced. + /// Thrown when the order or its templates are null. private async Task> GetEmailRecipientsAsync(NotificationOrder order, IEnumerable recipients) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(order.Templates); var emailTemplate = order.Templates.OfType().FirstOrDefault(); + var emailRecipients = recipients.Select(recipient => new EmailRecipient { IsReserved = recipient.IsReserved, OrganizationNumber = recipient.OrganizationNumber, NationalIdentityNumber = recipient.NationalIdentityNumber, - CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate?.Body) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate?.Body)) ? emailTemplate?.Body : null, - CustomizedSubject = (_keywordsService.ContainsRecipientNumberPlaceholder(emailTemplate?.Subject) || _keywordsService.ContainsRecipientNamePlaceholder(emailTemplate?.Subject)) ? emailTemplate?.Subject : null, + CustomizedBody = RequiresCustomization(emailTemplate?.Body) ? emailTemplate?.Body : null, + CustomizedSubject = RequiresCustomization(emailTemplate?.Subject) ? emailTemplate?.Subject : null, }).ToList(); return await _keywordsService.ReplaceKeywordsAsync(emailRecipients); @@ -148,7 +159,6 @@ private async Task> GetEmailRecipientsAsync(Notifica { return emailRecipients.FirstOrDefault(er => (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || - (recipient.AddressInfo != null && recipient.AddressInfo.OfType().Any(e => e.EmailAddress == er.ToAddress)) || (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); } @@ -163,7 +173,7 @@ private async Task> UpdateRecipientsWithContactPointsAsync(Notif .Where(r => !r.AddressInfo.Exists(a => a.AddressType == AddressType.Email)) .ToList(); - if (recipientsWithoutEmail.Count != 0) + if (recipientsWithoutEmail.Count > 0) { await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs index 433ac9bd..e7d07b3a 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs @@ -15,7 +15,7 @@ public interface IEmailNotificationService /// The unique identifier for the order associated with the notification. /// The time at which the notification is requested to be sent. /// The list of email addresses to send the notification to. - /// The details of the email recipient. + /// The email recipient to send the notification to. /// Indicates whether to ignore the reservation status of the recipient. /// A task that represents the asynchronous operation. Task CreateNotification(Guid orderId, DateTime requestedSendTime, List emailAddresses, EmailRecipient emailRecipient, bool ignoreReservation = false); From f36165799590b28958adbf204a0c83d39a35ddea Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 2 Dec 2024 10:52:00 +0100 Subject: [PATCH 65/75] Refactor some test units --- .../EmailOrderProcessingServiceTests.cs | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index bcde6902..300c13d2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -28,19 +28,19 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() { Id = Guid.NewGuid(), NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { + Recipients = + [ new() { - OrganizationNumber = "123456", - AddressInfo = [new EmailAddressPoint("email@test.com")] + OrganizationNumber = "123456", + AddressInfo = [new EmailAddressPoint("email@test.com")] }, new() { - OrganizationNumber = "654321", - AddressInfo = [new EmailAddressPoint("email@test.com")] + OrganizationNumber = "654321", + AddressInfo = [new EmailAddressPoint("email@test.com")] } - } + ] }; var serviceMock = new Mock(); @@ -59,28 +59,30 @@ public async Task ProcessOrder_NotificationServiceCalledOnceForEachRecipient() public async Task ProcessOrder_ExpectedInputToNotificationService() { // Arrange - Guid orderId = Guid.NewGuid(); DateTime requested = DateTime.UtcNow; + Guid orderId = Guid.NewGuid(); var order = new NotificationOrder() { Id = orderId, NotificationChannel = NotificationChannel.Email, RequestedSendTime = requested, - Recipients = new List() - { - new(new List() { new EmailAddressPoint("test@test.com") }, organizationNumber: "skd-orgno") - } + Recipients = + [ + new([new EmailAddressPoint("test@test.com")], organizationNumber: "skd-orgno") + ] }; List expectedEmailAddressPoints = [new("test@test.com")]; - EmailRecipient expectedEmailRecipient = new() - { - OrganizationNumber = "skd-orgno" - }; + EmailRecipient expectedEmailRecipient = new() { OrganizationNumber = "skd-orgno" }; var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is>(r => AssertUtils.AreEquivalent(expectedEmailAddressPoints, r)), It.Is(e => AssertUtils.AreEquivalent(expectedEmailRecipient, e)), It.IsAny())); + serviceMock.Setup(s => s.CreateNotification( + It.IsAny(), + It.Is(d => d.Equals(requested)), + It.Is>(r => AssertUtils.AreEquivalent(expectedEmailAddressPoints, r)), + It.Is(e => AssertUtils.AreEquivalent(expectedEmailRecipient, e)), + It.IsAny())); var service = GetTestService(emailService: serviceMock.Object); @@ -101,16 +103,6 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC Recipients = [ new() - { - AddressInfo = - [ - new EmailAddressPoint() - { - AddressType = AddressType.Email, - EmailAddress = "recipient@domain.com" - } - ] - } ] }; @@ -179,13 +171,13 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() { Id = Guid.NewGuid(), NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { + Recipients = + [ new(), - new(new List() { new EmailAddressPoint("test@test.com") }, nationalIdentityNumber: "enduser-nin"), - new(new List() { new EmailAddressPoint("test@test.com") }, organizationNumber : "skd-orgNo"), - new(new List() { new EmailAddressPoint("test@domain.com") }) - } + new([new EmailAddressPoint("test@test.com")], nationalIdentityNumber: "enduser-nin"), + new([new EmailAddressPoint("test@test.com")], organizationNumber : "skd-orgNo"), + new([new EmailAddressPoint("test@domain.com")]) + ] }; var serviceMock = new Mock(); @@ -209,10 +201,10 @@ public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() } private static EmailOrderProcessingService GetTestService( - IEmailNotificationRepository? emailRepo = null, - IEmailNotificationService? emailService = null, - IContactPointService? contactPointService = null, - IKeywordsService? keywordsService = null) + IEmailNotificationRepository? emailRepo = null, + IEmailNotificationService? emailService = null, + IContactPointService? contactPointService = null, + IKeywordsService? keywordsService = null) { if (emailRepo == null) { From 4c72d13aa8861924dd415b4f15658ffec5cdabc9 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 2 Dec 2024 11:43:12 +0100 Subject: [PATCH 66/75] Improve the SMS order processing logic --- .../Services/SmsOrderProcessingService.cs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs index 9fa8d6fd..5a785f78 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -60,21 +60,26 @@ public async Task ProcessOrderRetry(NotificationOrder order) public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { int smsCount = GetSmsCountForOrder(order); - List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); + + var allSmsRecipients = await GetSmsRecipientsAsync(order, recipients); + var registeredSmsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); foreach (var recipient in recipients) { var smsAddress = recipient.AddressInfo.OfType().FirstOrDefault(); - var smsRecipient = smsRecipients.Find(er => - er.MobileNumber == smsAddress?.MobileNumber && - er.OrganizationNumber == recipient.OrganizationNumber && - er.NationalIdentityNumber == recipient.NationalIdentityNumber); - - if (smsRecipient == null || smsAddress == null) + var isSmsRecipientRegistered = + registeredSmsRecipients.Exists(er => + er.MobileNumber == smsAddress?.MobileNumber && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); + if (isSmsRecipientRegistered) { continue; } + + var matchedSmsRecipient = FindSmsRecipient(allSmsRecipients, recipient); + var smsRecipient = matchedSmsRecipient ?? new SmsRecipient { IsReserved = recipient.IsReserved }; await _smsService.CreateNotification( order.Id, @@ -90,33 +95,35 @@ public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List { int smsCount = GetSmsCountForOrder(order); - var emailRecipients = await GetSmsRecipientsAsync(order, recipients); + var allSmsRecipients = await GetSmsRecipientsAsync(order, recipients); foreach (var recipient in recipients) { - var emailRecipient = FindEmailRecipient(emailRecipients, recipient); - var emailAddresses = recipient.AddressInfo .OfType() .Where(a => !string.IsNullOrWhiteSpace(a.MobileNumber)) .ToList(); + var matchedSmsRecipient = FindSmsRecipient(allSmsRecipients, recipient); + var smsRecipient = matchedSmsRecipient ?? new SmsRecipient { IsReserved = recipient.IsReserved }; + await _smsService.CreateNotification( order.Id, order.RequestedSendTime, emailAddresses, - emailRecipient, - smsCount, + smsRecipient, + smsCount, order.IgnoreReservation ?? false); } } /// - /// Retrieves email recipients with replaced keywords. + /// Retrieves a list of recipients for sending SMS, replacing keywords in the body with actual values. /// - /// The notification order. - /// The list of recipients. - /// A task that represents the asynchronous operation. The task result contains the list of email recipients. + /// The notification order containing the SMS template and recipients. + /// The list of recipients to process. + /// A task that represents the asynchronous operation. The task result contains the list of SMS recipients with keywords replaced. + /// Thrown when the order or its templates are null. private async Task> GetSmsRecipientsAsync(NotificationOrder order, IEnumerable recipients) { ArgumentNullException.ThrowIfNull(order); @@ -135,14 +142,14 @@ private async Task> GetSmsRecipientsAsync(Notification } /// - /// Finds the email recipient matching the given recipient. + /// Finds the SMS recipient matching the given recipient. /// - /// The list of email recipients. + /// The list of SMS recipients. /// The recipient to match. - /// The matching email recipient, or null if no match is found. - private static SmsRecipient? FindEmailRecipient(IEnumerable emailRecipients, Recipient recipient) + /// The matching SMS recipient, or null if no match is found. + private static SmsRecipient? FindSmsRecipient(IEnumerable smsRecipients, Recipient recipient) { - return emailRecipients.FirstOrDefault(er => + return smsRecipients.FirstOrDefault(er => (!string.IsNullOrWhiteSpace(recipient.OrganizationNumber) && er.OrganizationNumber == recipient.OrganizationNumber) || (!string.IsNullOrWhiteSpace(recipient.NationalIdentityNumber) && er.NationalIdentityNumber == recipient.NationalIdentityNumber)); } From 5a57ce72cc872256f8f16f8001f2332743cd4f30 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 2 Dec 2024 11:59:46 +0100 Subject: [PATCH 67/75] Remove the organization number --- .../v0.36/02-functions-and-procedures.sql | 517 +++++++++++++++++- .../Utils/TestdataUtil.cs | 1 - 2 files changed, 516 insertions(+), 2 deletions(-) diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql index e6b99873..c4868af8 100644 --- a/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql @@ -1 +1,516 @@ --- This script is auto generated from the tool DbTools. Do not edit manually. \ No newline at end of file +-- This script is autogenerated from the tool DbTools. Do not edit manually. + +-- cancelorder.sql: +CREATE OR REPLACE FUNCTION notifications.cancelorder( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + cancelallowed boolean, + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + ignorereservation boolean, + resourceid text, + conditionendpoint text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE plpgsql +AS $$ +DECLARE + order_record RECORD; +BEGIN + -- Retrieve the order and its status + SELECT o.requestedsendtime, o.processedstatus + INTO order_record + FROM notifications.orders o + WHERE o.alternateid = _alternateid AND o.creatorname = _creatorname; + + -- If no order is found, return an empty result set + IF NOT FOUND THEN + RETURN; + END IF; + + -- Check if order is already cancelled + IF order_record.processedstatus = 'Cancelled' THEN + RETURN QUERY + SELECT TRUE AS cancelallowed, + order_details.* + FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; + ELSEIF (order_record.requestedsendtime <= NOW() + INTERVAL '5 minutes' or order_record.processedstatus != 'Registered') THEN + RETURN QUERY + SELECT FALSE AS cancelallowed, NULL::uuid, NULL::text, NULL::text, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::timestamp with time zone, NULL::orderprocessingstate, NULL::text, NULL::boolean, NULL::text, NULL::text, NULL::bigint, NULL::bigint, NULL::bigint, NULL::bigint; + ELSE + -- Cancel the order by updating its status + UPDATE notifications.orders + SET processedstatus = 'Cancelled', processed = NOW() + WHERE notifications.orders.alternateid = _alternateid; + + -- Retrieve the updated order details + RETURN QUERY + SELECT TRUE AS cancelallowed, + order_details.* + FROM notifications.getorder_includestatus_v4(_alternateid, _creatorname) AS order_details; + END IF; +END; +$$; + + +-- getemailrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + toaddress text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _alternateid); +BEGIN +RETURN query + SELECT e.recipientorgno, e.recipientnin, e.toaddress + FROM notifications.emailnotifications e + WHERE e._orderid = __orderid; +END; +$BODY$; + +-- getemailsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + latest_email_timeout TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT emaillimittimeout + INTO latest_email_timeout + FROM notifications.resourcelimitlog + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + + -- Check if the latest email timeout is set and valid + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY + SELECT NULL::uuid AS alternateid, + NULL::text AS subject, + NULL::text AS body, + NULL::text AS fromaddress, + NULL::text AS toaddress, + NULL::text AS contenttype + WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog + SET emaillimittimeout = NULL + WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN QUERY + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, + _orderid, + notifications.emailnotifications.toaddress, + notifications.emailnotifications.customizedsubject, + notifications.emailnotifications.customizedbody + ) + SELECT u.alternateid, + CASE WHEN u.customizedsubject IS NOT NULL AND u.customizedsubject <> '' THEN u.customizedsubject ELSE et.subject END AS subject, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE et.body END AS body, + et.fromaddress, + u.toaddress, + et.contenttype + FROM updated u + JOIN notifications.emailtexts et ON u._orderid = et._orderid; +END; +$BODY$; + + +-- getemailsummary.sql: +CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + toaddress text, + result emailnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime + FROM notifications.emailnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- getmetrics.sql: +CREATE OR REPLACE FUNCTION notifications.getmetrics( + month_input int, + year_input int +) +RETURNS TABLE ( + org text, + placed_orders bigint, + sent_emails bigint, + succeeded_emails bigint, + sent_sms bigint, + succeeded_sms bigint +) +AS $$ +BEGIN + RETURN QUERY + SELECT + o.creatorname, + COUNT(DISTINCT o._id) AS placed_orders, + SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails, + SUM(CASE WHEN e.result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END) AS succeeded_emails, + SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms, + SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms + FROM notifications.orders o + LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid + LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid + WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input + AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input + GROUP BY o.creatorname; +END; +$$ LANGUAGE plpgsql; + + +-- getorderincludestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v4( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + ignorereservation boolean, + resourceid text, + conditionendpoint text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + _target_orderid INTEGER; + _succeededEmailCount BIGINT; + _generatedEmailCount BIGINT; + _succeededSmsCount BIGINT; + _generatedSmsCount BIGINT; +BEGIN + SELECT _id INTO _target_orderid + FROM notifications.orders + WHERE orders.alternateid = _alternateid + AND orders.creatorname = _creatorname; + + SELECT + SUM(CASE WHEN result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END), + COUNT(1) AS generatedEmailCount + INTO _succeededEmailCount, _generatedEmailCount + FROM notifications.emailnotifications + WHERE _orderid = _target_orderid; + + SELECT + SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END), + COUNT(1) AS generatedSmsCount + INTO _succeededSmsCount, _generatedSmsCount + FROM notifications.smsnotifications + WHERE _orderid = _target_orderid; + + RETURN QUERY + SELECT + orders.alternateid, + orders.creatorname, + orders.sendersreference, + orders.created, + orders.requestedsendtime, + orders.processed, + orders.processedstatus, + orders.notificationorder->>'NotificationChannel', + CASE + WHEN orders.notificationorder->>'IgnoreReservation' IS NULL THEN NULL + ELSE (orders.notificationorder->>'IgnoreReservation')::BOOLEAN + END AS IgnoreReservation, + orders.notificationorder->>'ResourceId', + orders.notificationorder->>'ConditionEndpoint', + _generatedEmailCount, + _succeededEmailCount, + _generatedSmsCount, + _succeededSmsCount + FROM + notifications.orders AS orders + WHERE + orders.alternateid = _alternateid; +END; +$BODY$; + + +-- getorderspastsendtimeupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus() + RETURNS TABLE(notificationorders jsonb) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +RETURN QUERY + UPDATE notifications.orders + SET processedstatus = 'Processing' + WHERE _id IN (select _id + from notifications.orders + where processedstatus = 'Registered' + and requestedsendtime <= now() + INTERVAL '1 minute' + limit 50) + RETURNING notificationorder AS notificationorders; +END; +$BODY$; + +-- getsmsrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + mobilenumber text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN +RETURN query + SELECT s.recipientorgno, s.recipientnin, s.mobilenumber + FROM notifications.smsnotifications s + WHERE s._orderid = __orderid; +END; +$BODY$; + +-- getsmsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + RETURN QUERY + WITH updated AS ( + UPDATE notifications.smsnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.smsnotifications.alternateid, + _orderid, + notifications.smsnotifications.mobilenumber, + notifications.smsnotifications.customizedbody + ) + SELECT u.alternateid, + st.sendernumber, + u.mobilenumber, + CASE WHEN u.customizedbody IS NOT NULL AND u.customizedbody <> '' THEN u.customizedbody ELSE st.body END AS body + FROM updated u + JOIN notifications.smstexts st ON u._orderid = st._orderid; +END; +$BODY$; + + +-- getsmssummary.sql: +CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + mobilenumber text, + result smsnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime + FROM notifications.smsnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- insertemailnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _toaddress TEXT, + _customizedbody TEXT, + _customizedsubject TEXT, + _result TEXT, + _resulttime timestamptz, + _expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + __orderid BIGINT; +BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; + + INSERT INTO notifications.emailnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + toaddress, + customizedbody, + customizedsubject, + result, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _toaddress, + _customizedbody, + _customizedsubject, + _result::emailnotificationresulttype, + _resulttime, + _expirytime + ); +END; +$BODY$; + +-- insertemailtext.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype) + VALUES (__orderid, _fromaddress, _subject, _body, _contenttype); +END; +$BODY$; + + +-- insertorder.sql: +CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB) +RETURNS BIGINT + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +_orderid BIGINT; +BEGIN + INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder) + VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder) + RETURNING _id INTO _orderid; + + RETURN _orderid; +END; +$BODY$; + +-- insertsmsnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( + _orderid uuid, + _alternateid uuid, + _recipientorgno TEXT, + _recipientnin TEXT, + _mobilenumber TEXT, + _customizedbody TEXT, + _result TEXT, + _smscount integer, + _resulttime timestamptz, + _expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + __orderid BIGINT; +BEGIN + SELECT _id INTO __orderid + FROM notifications.orders + WHERE alternateid = _orderid; + + INSERT INTO notifications.smsnotifications( + _orderid, + alternateid, + recipientorgno, + recipientnin, + mobilenumber, + customizedbody, + result, + smscount, + resulttime, + expirytime + ) + VALUES ( + __orderid, + _alternateid, + _recipientorgno, + _recipientnin, + _mobilenumber, + _customizedbody, + _result::smsnotificationresulttype, + _smscount, + _resulttime, + _expirytime + ); +END; +$BODY$; + +-- updateemailstatus.sql: +CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + UPDATE notifications.emailnotifications + SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid + WHERE alternateid = _alternateid; +END; +$BODY$; + diff --git a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs index 3d0f0028..3bf535fb 100644 --- a/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs +++ b/test/Altinn.Notifications.IntegrationTests/Utils/TestdataUtil.cs @@ -79,7 +79,6 @@ public static NotificationOrder NotificationOrder_EmailTemplate_OneRecipient() { new Recipient() { - OrganizationNumber = "314396189", AddressInfo = new() { new EmailAddressPoint() From 9cd7ec35372dbfc4c60d1b4856b2077828f06f86 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 08:18:35 +0100 Subject: [PATCH 68/75] Refactor: Remove unused PersonNameComponents type. --- .../Models/Parties/PartyDetails.cs | 6 ----- .../Models/Parties/PersonNameComponents.cs | 22 ------------------- .../Register/RegisterClient.cs | 1 - 3 files changed, 29 deletions(-) delete mode 100644 src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs diff --git a/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs b/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs index 0957df70..b07351e9 100644 --- a/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs @@ -19,12 +19,6 @@ public class PartyDetails [JsonPropertyName("orgNo")] public string? OrganizationNumber { get; set; } - /// - /// Gets or sets the components of the person's name, if available. - /// - [JsonPropertyName("personName")] - public PersonNameComponents? PersonName { get; set; } - /// /// Gets or sets the social security number of the party, if applicable. /// diff --git a/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs b/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs deleted file mode 100644 index fd41be2d..00000000 --- a/src/Altinn.Notifications.Core/Models/Parties/PersonNameComponents.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Altinn.Notifications.Core.Models; - -/// -/// Represents the components of a person's name. -/// -public record PersonNameComponents -{ - /// - /// Gets the first name. - /// - public string? FirstName { get; init; } - - /// - /// Gets the middle name. - /// - public string? MiddleName { get; init; } - - /// - /// Gets the surname. - /// - public string? LastName { get; init; } -} diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index 38bd3b00..503a2f57 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -121,7 +121,6 @@ public async Task> GetPartyDetailsForPersons(List soc { Content = content }; - request.Headers.Add("partyComponentOption", "person-name"); var response = await _client.SendAsync(request); From b562e4b3c99cbf11f7831c65990c6dc26d9fd3c7 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 13:25:01 +0100 Subject: [PATCH 69/75] #545 Added tests to increase coverage. --- .../PartyDetailsLookupBatchTests.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupBatchTests.cs diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupBatchTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupBatchTests.cs new file mode 100644 index 00000000..41e0f692 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupBatchTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +using Altinn.Notifications.Core.Models.Parties; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingModels; + +public class PartyDetailsLookupBatchTests +{ + [Fact] + public void Constructor_DoesNotThrow_WhenBothListsAreProvided() + { + // Arrange + var organizationNumbers = new List { "313556263" }; + var socialSecurityNumbers = new List { "16877298896" }; + + // Act + var batch = new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers); + + // Assert + Assert.NotNull(batch); + Assert.NotEmpty(batch.OrganizationNumbers); + Assert.NotEmpty(batch.SocialSecurityNumbers); + Assert.Single(batch.OrganizationNumbers, "313556263"); + Assert.Single(batch.SocialSecurityNumbers, "16877298896"); + } + + [Fact] + public void Constructor_DoesNotThrow_WhenOrganizationNumbersIsProvided() + { + // Arrange + List? socialSecurityNumbers = null; + var organizationNumbers = new List { "314727878" }; + + // Act + var batch = new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers); + + // Assert + Assert.NotNull(batch); + Assert.NotEmpty(batch.OrganizationNumbers); + Assert.Single(batch.OrganizationNumbers, "314727878"); + } + + [Fact] + public void Constructor_DoesNotThrow_WhenSocialSecurityNumbersIsProvided() + { + // Arrange + List? organizationNumbers = null; + var socialSecurityNumbers = new List { "55869600449" }; + + // Act + var batch = new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers); + + // Assert + Assert.NotNull(batch); + Assert.NotEmpty(batch.SocialSecurityNumbers); + Assert.Single(batch.SocialSecurityNumbers, "55869600449"); + } + + [Fact] + public void Constructor_ThrowsArgumentException_WhenBothListsAreEmpty() + { + // Arrange + List? organizationNumbers = []; + List? socialSecurityNumbers = []; + + // Act & Assert + var exception = Assert.Throws(() => new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers)); + + Assert.Equal("At least one of organizationNumbers or socialSecurityNumbers must be provided.", exception.Message); + } + + [Fact] + public void Constructor_ThrowsArgumentException_WhenBothListsAreNull() + { + // Arrange + List? organizationNumbers = null; + List? socialSecurityNumbers = null; + + // Act & Assert + var exception = Assert.Throws(() => new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers)); + + Assert.Equal("At least one of organizationNumbers or socialSecurityNumbers must be provided.", exception.Message); + } + + [Fact] + public void Constructor_ThrowsArgumentException_WhenOrganizationNumbersIsEmptyAndSocialSecurityNumbersIsNull() + { + // Arrange + List? organizationNumbers = []; + List? socialSecurityNumbers = null; + + // Act & Assert + var exception = Assert.Throws(() => new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers)); + + Assert.Equal("At least one of organizationNumbers or socialSecurityNumbers must be provided.", exception.Message); + } + + [Fact] + public void Constructor_ThrowsArgumentException_WhenOrganizationNumbersIsNullAndSocialSecurityNumbersIsEmpty() + { + // Arrange + List? organizationNumbers = null; + List? socialSecurityNumbers = []; + + // Act & Assert + var exception = Assert.Throws(() => new PartyDetailsLookupBatch(organizationNumbers, socialSecurityNumbers)); + + Assert.Equal("At least one of organizationNumbers or socialSecurityNumbers must be provided.", exception.Message); + } +} From 7a8c64b2bfef11d69b84159669f3ecd311818c62 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 13:46:15 +0100 Subject: [PATCH 70/75] #545 Validate the constructors and JSON serialization behavior of the PartyDetailsLookupRequest class across diverse scenarios. --- .../PartyDetailsLookupRequestTests.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupRequestTests.cs diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupRequestTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupRequestTests.cs new file mode 100644 index 00000000..63ee8b36 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingModels/PartyDetailsLookupRequestTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Text.Json; + +using Altinn.Notifications.Core.Models.Parties; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingModels; + +public class PartyDetailsLookupRequestTests +{ + [Fact] + public void Constructor_WithBothOrganizationNumberAndSocialSecurityNumber_ThrowsArgumentException() + { + // Arrange + var organizationNumber = "314204298"; + var socialSecurityNumber = "09827398702"; + + // Act & Assert + var exception = Assert.Throws(() => new PartyDetailsLookupRequest(organizationNumber, socialSecurityNumber)); + Assert.Equal("You can specify either an OrganizationNumber or a SocialSecurityNumber, but not both.", exception.Message); + } + + [Fact] + public void Constructor_WithNoParameters_SetsBothPropertiesToNull() + { + // Act + var request = new PartyDetailsLookupRequest(); + + // Assert + Assert.Null(request.OrganizationNumber); + Assert.Null(request.SocialSecurityNumber); + } + + [Fact] + public void Constructor_WithOrganizationNumberOnly_SetsOrganizationNumber() + { + // Arrange + var organizationNumber = "314204298"; + + // Act + var request = new PartyDetailsLookupRequest(organizationNumber: organizationNumber); + + // Assert + Assert.Equal(organizationNumber, request.OrganizationNumber); + Assert.Null(request.SocialSecurityNumber); + } + + [Fact] + public void Constructor_WithSocialSecurityNumberOnly_SetsSocialSecurityNumber() + { + // Arrange + var socialSecurityNumber = "09827398702"; + + // Act + var request = new PartyDetailsLookupRequest(socialSecurityNumber: socialSecurityNumber); + + // Assert + Assert.Equal(socialSecurityNumber, request.SocialSecurityNumber); + Assert.Null(request.OrganizationNumber); + } + + [Fact] + public void JsonSerialization_WithNullProperties_ExcludesNullProperties() + { + // Arrange + var request = new PartyDetailsLookupRequest(); + + // Act + var json = JsonSerializer.Serialize(request); + + // Assert + Assert.DoesNotContain("\"orgNo\"", json); + Assert.DoesNotContain("\"ssn\"", json); + Assert.Equal("{}", json); + } + + [Fact] + public void JsonSerialization_WithOrganizationNumber_OnlyIncludesOrganizationNumber() + { + // Arrange + var request = new PartyDetailsLookupRequest(organizationNumber: "314204298"); + + // Act + var json = JsonSerializer.Serialize(request); + + // Assert + Assert.Contains("\"orgNo\":\"314204298\"", json); + Assert.DoesNotContain("\"ssn\"", json); + } + + [Fact] + public void JsonSerialization_WithSocialSecurityNumber_OnlyIncludesSocialSecurityNumber() + { + // Arrange + var request = new PartyDetailsLookupRequest(socialSecurityNumber: "09827398702"); + + // Act + var json = JsonSerializer.Serialize(request); + + // Assert + Assert.Contains("\"ssn\":\"09827398702\"", json); + Assert.DoesNotContain("\"orgNo\"", json); + } +} From 6c3f8835be3dc4282aa15c35ca7ff153cab5dcb0 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 13:56:06 +0100 Subject: [PATCH 71/75] Change the type of a field to improve performance --- .../Notifications.Core/TestingServices/KeywordsServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs index 67a8d500..2b305632 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs @@ -16,8 +16,8 @@ namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class KeywordsServiceTests { + private readonly KeywordsService _keywordsService; private readonly Mock _registerClientMock; - private readonly IKeywordsService _keywordsService; public KeywordsServiceTests() { From d11933f3d8b6e20eb1c325d1f258bf554f32dc3d Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 15:09:44 +0100 Subject: [PATCH 72/75] Add three test units to cover more use cases --- .../Register/RegisterClientTests.cs | 160 +++++++++++------- 1 file changed, 103 insertions(+), 57 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs index 0083d86f..9678f8e7 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs @@ -86,6 +86,40 @@ public async Task GetOrganizationContactPoints_FailureResponse_ExceptionIsThrown Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } + [Fact] + public async Task GetOrganizationContactPoints_NullOrganizationNumbers_ReturnsEmpty() + { + // Act + List actual = await _registerClient.GetOrganizationContactPoints(null); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public async Task GetOrganizationContactPoints_EmptyOrganizationNumbers_ReturnsEmpty() + { + // Act + List actual = await _registerClient.GetOrganizationContactPoints([]); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public async Task GetOrganizationContactPoints_NullContactPointsList_ReturnsEmpty() + { + // Arrange + var organizationNumbers = new List { "null-contact-points-list" }; + + // Act + var result = await _registerClient.GetOrganizationContactPoints(organizationNumbers); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + [Fact] public async Task GetPartyDetailsForOrganizations_SuccessResponse_NoMatches() { @@ -148,16 +182,28 @@ public async Task GetPartyDetailsForPersons_FailureResponse_ExceptionIsThrown() Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } + private Task CreateMockResponse(object? contentData, HttpStatusCode statusCode) + { + JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; + + return Task.FromResult(new HttpResponseMessage() + { + StatusCode = statusCode, + Content = content + }); + } + private Task GetResponse(OrgContactPointLookup lookup) { - HttpStatusCode statusCode = HttpStatusCode.OK; object? contentData = null; + HttpStatusCode statusCode = HttpStatusCode.OK; switch (lookup.OrganizationNumbers[0]) { case "empty-list": contentData = new OrgContactPointsList() { ContactPointsList = [] }; break; + case "populated-list": contentData = new OrgContactPointsList { @@ -168,77 +214,77 @@ private Task GetResponse(OrgContactPointLookup lookup) ] }; break; + case "unavailable": statusCode = HttpStatusCode.ServiceUnavailable; break; - } - JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; + case "null-contact-points-list": + contentData = new OrgContactPointsList { ContactPointsList = null }; + break; + } - return Task.FromResult( - new HttpResponseMessage() - { - StatusCode = statusCode, - Content = content - }); + return CreateMockResponse(contentData, statusCode); } private Task GetPartyDetailsResponse(PartyDetailsLookupBatch lookup) { - HttpStatusCode statusCode = HttpStatusCode.OK; object? contentData = null; + HttpStatusCode statusCode = HttpStatusCode.OK; - switch (lookup.PartyDetailsLookupRequestList?.FirstOrDefault()?.OrganizationNumber) + var firstRequest = lookup.PartyDetailsLookupRequestList?.FirstOrDefault(); + if (firstRequest != null) { - case "empty-list": - contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; - break; - - case "populated-list": - contentData = new PartyDetailsLookupResult + if (firstRequest.OrganizationNumber != null) + { + switch (firstRequest.OrganizationNumber) { - PartyDetailsList = - [ - new() { OrganizationNumber = "313600947", Name = "Test Organization 1" }, - new() { OrganizationNumber = "315058384", Name = "Test Organization 2" } - ] - }; - break; - - case "unavailable": - statusCode = HttpStatusCode.ServiceUnavailable; - break; - } - - switch (lookup.PartyDetailsLookupRequestList?.FirstOrDefault()?.SocialSecurityNumber) - { - case "empty-list": - contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; - break; - - case "populated-list": - contentData = new PartyDetailsLookupResult + case "empty-list": + contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; + break; + + case "populated-list": + contentData = new PartyDetailsLookupResult + { + PartyDetailsList = + [ + new() { OrganizationNumber = "313600947", Name = "Test Organization 1" }, + new() { OrganizationNumber = "315058384", Name = "Test Organization 2" } + ] + }; + break; + + case "unavailable": + statusCode = HttpStatusCode.ServiceUnavailable; + break; + } + } + else if (firstRequest.SocialSecurityNumber != null) + { + switch (firstRequest.SocialSecurityNumber) { - PartyDetailsList = - [ - new() { NationalIdentityNumber = "07837399275", Name = "Test Person 1" }, - new() { NationalIdentityNumber = "04917199103", Name = "Test Person 2" } - ] - }; - break; - - case "unavailable": - statusCode = HttpStatusCode.ServiceUnavailable; - break; + case "empty-list": + contentData = new PartyDetailsLookupResult() { PartyDetailsList = [] }; + break; + + case "populated-list": + contentData = new PartyDetailsLookupResult + { + PartyDetailsList = + [ + new() { NationalIdentityNumber = "07837399275", Name = "Test Person 1" }, + new() { NationalIdentityNumber = "04917199103", Name = "Test Person 2" } + ] + }; + break; + + case "unavailable": + statusCode = HttpStatusCode.ServiceUnavailable; + break; + } + } } - JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; - - return Task.FromResult( - new HttpResponseMessage() - { - StatusCode = statusCode, - Content = content - }); + return CreateMockResponse(contentData, statusCode); } } From aefc45845bca9e7f9a638056890cf046cd0a1c8e Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Thu, 5 Dec 2024 15:37:09 +0100 Subject: [PATCH 73/75] Remove test cases that check for the nullability of parameters that are defined as non-nullable. --- .../Register/RegisterClientTests.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs index 9678f8e7..54a87b46 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs @@ -86,16 +86,6 @@ public async Task GetOrganizationContactPoints_FailureResponse_ExceptionIsThrown Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } - [Fact] - public async Task GetOrganizationContactPoints_NullOrganizationNumbers_ReturnsEmpty() - { - // Act - List actual = await _registerClient.GetOrganizationContactPoints(null); - - // Assert - Assert.Empty(actual); - } - [Fact] public async Task GetOrganizationContactPoints_EmptyOrganizationNumbers_ReturnsEmpty() { @@ -220,7 +210,7 @@ private Task GetResponse(OrgContactPointLookup lookup) break; case "null-contact-points-list": - contentData = new OrgContactPointsList { ContactPointsList = null }; + contentData = new OrgContactPointsList { ContactPointsList = [] }; break; } From 51aefcf8b2a5cfab2eea6289c2c51ee9b669a7d5 Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 16 Dec 2024 10:34:07 +0100 Subject: [PATCH 74/75] Replace the national identity number with an empty string --- .../Services/KeywordsService.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 7eb65341..634f4d79 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -111,7 +111,7 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< } /// - /// Replaces placeholders in the given text with actual values from the provided details. + /// Replaces placeholders in the given text with actual values from the provided party details. /// /// The text containing placeholders. /// The organization number. @@ -128,7 +128,7 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< { customizedText = ReplaceWithDetails(customizedText, organizationNumber, organizationDetails, p => p.OrganizationNumber); - customizedText = ReplaceWithDetails(customizedText, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber); + customizedText = ReplaceWithDetails(customizedText, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber, true); return customizedText; } @@ -136,16 +136,18 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< /// /// Replaces placeholders in the given text with actual values from the provided details. /// - /// The text containing placeholders. - /// The key to match in the details. - /// The list of details. - /// The function to select the key from the details. - /// The text with placeholders replaced by actual values. + /// The text containing placeholders to be replaced. + /// The key used to find the matching detail in the list of details. + /// The list of details from which to retrieve the actual values. + /// A function to select the key from the details for matching purposes. + /// Indicates whether the detail is for a person. If true, the $recipientNumber$ placeholder will be replaced with an empty string. + /// The text with placeholders replaced by actual values from the matching detail, or the original text if no matching detail is found. private static string? ReplaceWithDetails( string? customizedText, string? searchKey, IEnumerable details, - Func keySelector) + Func keySelector, + bool isPerson = false) { if (string.IsNullOrWhiteSpace(searchKey)) { @@ -158,7 +160,9 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< { customizedText = customizedText?.Replace(_recipientNamePlaceholder, detail.Name ?? string.Empty); - customizedText = customizedText?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); + customizedText = isPerson + ? customizedText?.Replace(_recipientNumberPlaceholder, string.Empty) + : customizedText?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); } return customizedText; From 83f31c4d0fadf329cd7334b27a4dd9da6103edab Mon Sep 17 00:00:00 2001 From: Ahmed-Ghanam Date: Mon, 16 Dec 2024 11:08:23 +0100 Subject: [PATCH 75/75] Do not replace the $recipientNumber$ with the national identity number --- .../Services/KeywordsService.cs | 42 +++++++++++-------- .../TestingServices/KeywordsServiceTests.cs | 4 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs index 634f4d79..bf4b2380 100644 --- a/src/Altinn.Notifications.Core/Services/KeywordsService.cs +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -97,11 +97,11 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< List organizationNumbers, List nationalIdentityNumbers) { - var personDetailsTask = nationalIdentityNumbers.Count != 0 + var personDetailsTask = (nationalIdentityNumbers != null && nationalIdentityNumbers.Count > 0) ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) : Task.FromResult(new List()); - var organizationDetailsTask = organizationNumbers.Count != 0 + var organizationDetailsTask = (organizationNumbers != null && organizationNumbers.Count > 0) ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) : Task.FromResult(new List()); @@ -134,37 +134,45 @@ public async Task> ReplaceKeywordsAsync(IEnumerable< } /// - /// Replaces placeholders in the given text with actual values from the provided details. + /// Replaces placeholders in the provided text with values from the matching party details. /// /// The text containing placeholders to be replaced. - /// The key used to find the matching detail in the list of details. - /// The list of details from which to retrieve the actual values. - /// A function to select the key from the details for matching purposes. - /// Indicates whether the detail is for a person. If true, the $recipientNumber$ placeholder will be replaced with an empty string. - /// The text with placeholders replaced by actual values from the matching detail, or the original text if no matching detail is found. + /// The key used to locate a matching party details from the list of party details. + /// The list of party details to search for a match. + /// A function to extract the key from a party detail for matching purposes. + /// + /// A flag indicating whether the detail represents a person. If true, the $recipientNumber$ placeholder will + /// be removed from the text. Otherwise, it will be replaced with the corresponding detail key value. + /// + /// + /// The text with placeholders replaced by values from the matching detail. If no match is found, the original text is returned. + /// private static string? ReplaceWithDetails( string? customizedText, string? searchKey, - IEnumerable details, + IEnumerable partyDetails, Func keySelector, bool isPerson = false) { - if (string.IsNullOrWhiteSpace(searchKey)) + if (string.IsNullOrWhiteSpace(customizedText) || string.IsNullOrWhiteSpace(searchKey)) { return customizedText; } - var detail = details.FirstOrDefault(e => keySelector(e) == searchKey); + var matchingDetail = partyDetails.FirstOrDefault(detail => keySelector(detail) == searchKey); - if (detail != null) + if (matchingDetail == null) { - customizedText = customizedText?.Replace(_recipientNamePlaceholder, detail.Name ?? string.Empty); - - customizedText = isPerson - ? customizedText?.Replace(_recipientNumberPlaceholder, string.Empty) - : customizedText?.Replace(_recipientNumberPlaceholder, keySelector(detail) ?? string.Empty); + return customizedText; } + // Replace the $recipientName$ placeholder with the detail's name or an empty string if null. + customizedText = customizedText.Replace(_recipientNamePlaceholder, matchingDetail.Name ?? string.Empty); + + // Replace the $recipientNumber$ placeholder based on whether the detail represents a person or not. + string recipientNumberReplacement = isPerson ? string.Empty : (keySelector(matchingDetail) ?? string.Empty); + customizedText = customizedText.Replace(_recipientNumberPlaceholder, recipientNumberReplacement); + return customizedText; } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs index 2b305632..88d680d2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/KeywordsServiceTests.cs @@ -104,7 +104,7 @@ public async Task ReplaceKeywordsAsync_EmailRecipients_ShouldReplacePlaceholders // Assert var recipient = result.First(); Assert.Equal("Hello Person name", recipient.CustomizedBody); - Assert.Equal("Subject 07837399275", recipient.CustomizedSubject); + Assert.Equal("Subject ", recipient.CustomizedSubject); } [Fact] @@ -162,7 +162,7 @@ public async Task ReplaceKeywordsAsync_SmsRecipient_ShouldReplacePlaceholdersFor // Assert var recipient = result.First(); - Assert.Equal("Hello Person name your national identity number is 07837399275", recipient.CustomizedBody); + Assert.Equal("Hello Person name your national identity number is ", recipient.CustomizedBody); } [Fact]