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/Integrations/IRegisterClient.cs b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs index 44135b33..677a7db5 100644 --- a/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IRegisterClient.cs @@ -1,17 +1,40 @@ 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 party details for the specified organizations. + /// + /// 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); + + /// + /// Asynchronously retrieves party details for the specified persons. /// - 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 individuals. + /// + Task> GetPartyDetailsForPersons(List socialSecurityNumbers); } diff --git a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs index dbee9a59..a8e14f06 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/EmailTemplate.cs @@ -3,43 +3,51 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an email notification +/// Represents a 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 from adress of emails created by the template + /// Gets the content type of the email. /// - public string FromAddress { get; internal set; } = string.Empty; + public EmailContentType ContentType { get; internal set; } /// - /// Gets the subject of emails created by the template + /// Gets the sender address of the email. /// - public string Subject { get; internal set; } = string.Empty; + public string FromAddress { get; internal set; } = string.Empty; /// - /// 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; } = 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..cd5fbba2 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,7 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; public interface INotificationTemplate { /// - /// Gets the type for the template + /// Gets the type of the notification template. /// - public NotificationTemplateType Type { get; } + 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..1658cba7 100644 --- a/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs +++ b/src/Altinn.Notifications.Core/Models/NotificationTemplate/SmsTemplate.cs @@ -3,31 +3,37 @@ namespace Altinn.Notifications.Core.Models.NotificationTemplate; /// -/// Template for an SMS notification +/// Represents a 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 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; } = 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..b07351e9 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetails.cs @@ -0,0 +1,27 @@ +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 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..7af07f89 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupBatch.cs @@ -0,0 +1,57 @@ +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 +{ + /// + /// 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. + [JsonConstructor] + 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."); + } + + OrganizationNumbers = organizationNumbers ?? []; + SocialSecurityNumbers = socialSecurityNumbers ?? []; + + PartyDetailsLookupRequestList = []; + + if (OrganizationNumbers.Count != 0) + { + PartyDetailsLookupRequestList.AddRange(OrganizationNumbers.Select(orgNum => new PartyDetailsLookupRequest(organizationNumber: orgNum))); + } + + if (SocialSecurityNumbers.Count != 0) + { + PartyDetailsLookupRequestList.AddRange(SocialSecurityNumbers.Select(ssn => new PartyDetailsLookupRequest(socialSecurityNumber: ssn))); + } + } + + /// + /// 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; private 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..11d9709f --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Parties/PartyDetailsLookupRequest.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Parties; + +/// +/// Represents a lookup criterion for a single party. +/// +public record PartyDetailsLookupRequest +{ + /// + /// 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("You can specify either an OrganizationNumber or a SocialSecurityNumber, but not both."); + } + + 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; } + + /// + /// 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; } +} 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..47e89ff3 --- /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/Recipient.cs b/src/Altinn.Notifications.Core/Models/Recipient.cs index 1fc089dd..8890b1fe 100644 --- a/src/Altinn.Notifications.Core/Models/Recipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipient.cs @@ -5,50 +5,54 @@ namespace Altinn.Notifications.Core.Models; /// -/// Class representing a notification recipient +/// Represents 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; } = []; /// - /// 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's national identity number. /// - public bool? IsReserved { get; set; } + public string? NationalIdentityNumber { get; set; } /// - /// Gets a list of address points for the recipient + /// Gets or sets the recipient's organization number. /// - public List AddressInfo { get; set; } = new List(); + 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. + /// Initializes a new instance of the class with the specified address information, organization number, and national identity number. /// - public Recipient() + /// The list of address points for the recipient. + /// The recipient's organization number. + /// The recipient's national identity number. + public Recipient(List addressInfo, string? organizationNumber = null, string? nationalIdentityNumber = null) { + AddressInfo = addressInfo; + 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/Recipients/EmailRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/EmailRecipient.cs index bb99c76c..17e95692 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 +/// Represents an email recipient with various properties for customization and identification. /// public class EmailRecipient { /// - /// Gets or sets the recipient's organization number + /// Gets or sets the customized body of the email after replacing the keywords with actual values. /// - public string? OrganizationNumber { get; set; } = null; + public string? CustomizedBody { get; set; } = null; + + /// + /// Gets or sets the customized subject of the email after replacing the keywords with actual values. + /// + 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/Models/Recipients/SmsRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs index 70e7cf60..73510553 100644 --- a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs +++ b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs @@ -1,27 +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 a value indicating whether the recipient is reserved from digital communication + /// Gets or sets the recipient's national identity number. /// - public bool? IsReserved { get; set; } + public string? NationalIdentityNumber { get; set; } = null; + + /// + /// Gets or sets the recipient's organization number. + /// + public string? OrganizationNumber { get; set; } = null; } diff --git a/src/Altinn.Notifications.Core/Models/Sms.cs b/src/Altinn.Notifications.Core/Models/Sms.cs index c3139cbe..c4309149 100644 --- a/src/Altinn.Notifications.Core/Models/Sms.cs +++ b/src/Altinn.Notifications.Core/Models/Sms.cs @@ -3,36 +3,40 @@ 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; } /// - /// 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. public Sms(Guid notificationId, string sender, string recipient, string message) { NotificationId = notificationId; @@ -42,8 +46,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); diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index ff506db5..30319993 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -6,314 +6,313 @@ 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)); } - - private static void AddPreferredOrFallbackContactPoint( - Recipient recipient, - TPreferred preferredContact, - TFallback fallbackContact, - Func preferredSelector, - Func fallbackSelector) + else if (!string.IsNullOrEmpty(fallbackContact?.ToString())) { - if (!string.IsNullOrEmpty(preferredContact?.ToString())) - { - recipient.AddressInfo.Add(preferredSelector(preferredContact)); - } - else if (!string.IsNullOrEmpty(fallbackContact?.ToString())) - { - recipient.AddressInfo.Add(fallbackSelector(fallbackContact)); - } + recipient.AddressInfo.Add(fallbackSelector(fallbackContact)); } + } - private async Task> AugmentRecipients( - List recipients, - string? resourceId, - Func createUserContactPoint, - Func createOrgContactPoint) - { - List augmentedRecipients = []; + private async Task> AugmentRecipients( + List recipients, + string? resourceId, + Func createUserContactPoint, + Func createOrgContactPoint) + { + List augmentedRecipients = []; - var userLookupTask = LookupPersonContactPoints(recipients); - var orgLookupTask = LookupOrganizationContactPoints(recipients, resourceId); - await Task.WhenAll(userLookupTask, orgLookupTask); + var userLookupTask = LookupPersonContactPoints(recipients); + var orgLookupTask = LookupOrganizationContactPoints(recipients, resourceId); + await Task.WhenAll(userLookupTask, orgLookupTask); - List userContactPointsList = userLookupTask.Result; - List organizationContactPointList = orgLookupTask.Result; + List userContactPointsList = userLookupTask.Result; + List organizationContactPointList = orgLookupTask.Result; - foreach (Recipient recipient in recipients) + foreach (Recipient recipient in recipients) + { + if (!string.IsNullOrEmpty(recipient.NationalIdentityNumber)) { - if (!string.IsNullOrEmpty(recipient.NationalIdentityNumber)) - { - UserContactPoints? userContactPoints = userContactPointsList! - .Find(u => u.NationalIdentityNumber == recipient.NationalIdentityNumber); + 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(recipient.OrganizationNumber)) + if (userContactPoints != null) { - OrganizationContactPoints? organizationContactPoints = organizationContactPointList! - .Find(o => o.OrganizationNumber == recipient.OrganizationNumber); - - if (organizationContactPoints != null) - { - augmentedRecipients.Add(createOrgContactPoint(recipient, organizationContactPoints)); - } + recipient.IsReserved = userContactPoints.IsReserved; + augmentedRecipients.Add(createUserContactPoint(recipient, userContactPoints)); } } + else if (!string.IsNullOrEmpty(recipient.OrganizationNumber)) + { + OrganizationContactPoints? organizationContactPoints = organizationContactPointList! + .Find(o => o.OrganizationNumber == recipient.OrganizationNumber); - return augmentedRecipients; + if (organizationContactPoints != null) + { + augmentedRecipients.Add(createOrgContactPoint(recipient, organizationContactPoints)); + } + } } - private async Task> LookupPersonContactPoints(List recipients) + 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) { - List nins = recipients - .Where(r => !string.IsNullOrEmpty(r.NationalIdentityNumber)) - .Select(r => r.NationalIdentityNumber!) - .ToList(); + return []; + } - if (nins.Count == 0) - { - return []; - } + List contactPoints = await _profileClient.GetUserContactPoints(nins); - List contactPoints = await _profileClient.GetUserContactPoints(nins); + contactPoints.ForEach(contactPoint => + { + contactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(contactPoint.MobileNumber); + }); - contactPoints.ForEach(contactPoint => - { - contactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(contactPoint.MobileNumber); - }); + return contactPoints; + } - return contactPoints; - } + private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId) + { + List orgNos = recipients + .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); - private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId) + if (orgNos.Count == 0) { - List orgNos = recipients - .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) - .Select(r => r.OrganizationNumber!) - .ToList(); - - if (orgNos.Count == 0) - { - return []; - } + return []; + } - Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); - List authorizedUserContactPoints = new(); + List authorizedUserContactPoints = []; + Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); - if (!string.IsNullOrEmpty(resourceId)) - { - var allUserContactPoints = await _profileClient.GetUserRegisteredContactPoints(orgNos, resourceId); - authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); - } + if (!string.IsNullOrEmpty(resourceId)) + { + var allUserContactPoints = await _profileClient.GetUserRegisteredContactPoints(orgNos, resourceId); + authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); + } - List contactPoints = await registerTask; + List contactPoints = await registerTask; - if (!string.IsNullOrEmpty(resourceId)) + if (!string.IsNullOrEmpty(resourceId)) + { + foreach (var userContactPoint in authorizedUserContactPoints) { - foreach (var userContactPoint in authorizedUserContactPoints) + userContactPoint.UserContactPoints.ForEach(userContactPoint => { - userContactPoint.UserContactPoints.ForEach(userContactPoint => - { - userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); - }); + userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); + }); - var existingContactPoint = contactPoints.Find(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); + var existingContactPoint = contactPoints.Find(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); - if (existingContactPoint != null) - { - existingContactPoint.UserContactPoints.AddRange(userContactPoint.UserContactPoints); - } - else - { - contactPoints.Add(userContactPoint); - } + if (existingContactPoint != null) + { + existingContactPoint.UserContactPoints.AddRange(userContactPoint.UserContactPoints); + } + else + { + contactPoints.Add(userContactPoint); } } + } - contactPoints.ForEach(contactPoint => - { - contactPoint.MobileNumberList = contactPoint.MobileNumberList - .Select(mobileNumber => - { - return MobileNumberHelper.EnsureCountryCodeIfValidNumber(mobileNumber); - }) - .ToList(); - }); + contactPoints.ForEach(contactPoint => + { + contactPoint.MobileNumberList = contactPoint.MobileNumberList + .Select(mobileNumber => + { + return MobileNumberHelper.EnsureCountryCodeIfValidNumber(mobileNumber); + }) + .ToList(); + }); - return contactPoints; - } + return contactPoints; } } diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index 5f4bbe8d..3398f944 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -13,19 +13,24 @@ 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; /// /// 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, @@ -35,34 +40,22 @@ public EmailNotificationService( { _guid = guid; _dateTime = dateTime; - _repository = repository; _producer = producer; + _repository = repository; _emailQueueTopicName = kafkaSettings.Value.EmailQueueTopicName; } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false) + 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() - { - OrganizationNumber = recipient.OrganizationNumber, - NationalIdentityNumber = recipient.NationalIdentityNumber, - IsReserved = recipient.IsReserved - }; - - 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; @@ -71,8 +64,9 @@ 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); - } + } } /// @@ -93,7 +87,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; @@ -102,14 +96,22 @@ 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() { - Id = _guid.NewGuid(), OrderId = orderId, - RequestedSendTime = requestedSendTime, + Id = _guid.NewGuid(), Recipient = recipient, + RequestedSendTime = requestedSendTime, SendResult = new(result, _dateTime.UtcNow()) }; @@ -121,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 e271bd11..7bd67ea5 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; @@ -9,74 +10,174 @@ 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) - { - foreach (Recipient recipient in recipients) - { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false); - } - } - - /// public async Task ProcessOrderRetry(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 ProcessOrderRetryWithoutAddressLookup(order, recipients); } - /// + /// public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) { - List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); + var allEmailRecipients = await GetEmailRecipientsAsync(order, recipients); + var registeredEmailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); - foreach (Recipient recipient in recipients) + foreach (var recipient in recipients) { - EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint; + var addressPoint = recipient.AddressInfo.OfType().FirstOrDefault(); - if (!emailRecipients.Exists(er => - er.NationalIdentityNumber == recipient.NationalIdentityNumber - && er.OrganizationNumber == recipient.OrganizationNumber - && er.ToAddress == addressPoint?.EmailAddress)) + var isEmailRecipientRegistered = + registeredEmailRecipients.Exists(er => er.ToAddress == addressPoint?.EmailAddress && + er.OrganizationNumber == recipient.OrganizationNumber && + er.NationalIdentityNumber == recipient.NationalIdentityNumber); + if (isEmailRecipientRegistered) { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation ?? false); + continue; } + + var matchedEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); + var emailRecipient = matchedEmailRecipient ?? new EmailRecipient { IsReserved = recipient.IsReserved }; + + await _emailService.CreateNotification( + order.Id, + order.RequestedSendTime, + [addressPoint], + emailRecipient, + order.IgnoreReservation ?? false); } } + + /// + public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) + { + var allEmailRecipients = await GetEmailRecipientsAsync(order, recipients); + + foreach (var recipient in recipients) + { + var emailAddresses = recipient.AddressInfo + .OfType() + .Where(a => !string.IsNullOrWhiteSpace(a.EmailAddress)) + .ToList(); + + var matchedEmailRecipient = FindEmailRecipient(allEmailRecipients, recipient); + var emailRecipient = matchedEmailRecipient ?? new EmailRecipient { IsReserved = recipient.IsReserved }; + + await _emailService.CreateNotification( + order.Id, + order.RequestedSendTime, + emailAddresses, + emailRecipient, + order.IgnoreReservation ?? false); + } + } + + /// + /// Determines whether the specified template part requires customization by checking for placeholder keywords. + /// + /// 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 = RequiresCustomization(emailTemplate?.Body) ? emailTemplate?.Body : null, + CustomizedSubject = RequiresCustomization(emailTemplate?.Subject) ? emailTemplate?.Subject : null, + }).ToList(); + + return await _keywordsService.ReplaceKeywordsAsync(emailRecipients); + } + + /// + /// 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) + { + 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 recipientsWithoutEmail = order.Recipients + .Where(r => !r.AddressInfo.Exists(a => a.AddressType == AddressType.Email)) + .ToList(); + + if (recipientsWithoutEmail.Count > 0) + { + await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); + } + + return order.Recipients; + } } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs index 8a0e1eb3..2796b36a 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs @@ -1,39 +1,38 @@ using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models; -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. /// - 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 AddEmailContactPoints(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 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. + /// 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 - /// - /// 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); - } + /// + /// 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); } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs index 70f9b68a..e7d07b3a 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); + /// 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 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); /// - /// 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/Interfaces/IKeywordsService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs new file mode 100644 index 00000000..84a03c85 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IKeywordsService.cs @@ -0,0 +1,38 @@ +using Altinn.Notifications.Core.Models.Recipients; + +namespace Altinn.Notifications.Core.Services.Interfaces +{ + /// + /// Provides methods for handling keyword placeholders in and . + /// + public interface IKeywordsService + { + /// + /// Checks whether the specified string contains the placeholder keyword $recipientName$. + /// + /// 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 for the placeholder keyword. + /// 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 the placeholder keywords replaced by actual values. + Task> ReplaceKeywordsAsync(IEnumerable smsRecipients); + + /// + /// 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 the placeholder keywords replaced by actual values. + Task> ReplaceKeywordsAsync(IEnumerable emailRecipients); + } +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs index 099c7550..940fea8d 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs @@ -1,25 +1,26 @@ -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 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); + 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 + /// 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); diff --git a/src/Altinn.Notifications.Core/Services/KeywordsService.cs b/src/Altinn.Notifications.Core/Services/KeywordsService.cs new file mode 100644 index 00000000..bf4b2380 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/KeywordsService.cs @@ -0,0 +1,179 @@ +using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Models.Parties; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services +{ + /// + /// Provides methods for handling keyword placeholders in and . + /// + public class KeywordsService : IKeywordsService + { + private readonly IRegisterClient _registerClient; + + private const string _recipientNamePlaceholder = "$recipientName$"; + private const string _recipientNumberPlaceholder = "$recipientNumber$"; + + /// + /// Initializes a new instance of the class. + /// + /// The register client to interact with the register service. + public KeywordsService(IRegisterClient registerClient) + { + _registerClient = registerClient ?? throw new ArgumentNullException(nameof(registerClient)); + } + + /// + public bool ContainsRecipientNamePlaceholder(string? value) => + !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNamePlaceholder); + + /// + public bool ContainsRecipientNumberPlaceholder(string? value) => + !string.IsNullOrWhiteSpace(value) && value.Contains(_recipientNumberPlaceholder); + + /// + public async Task> ReplaceKeywordsAsync(IEnumerable smsRecipients) + { + 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 (personDetails, organizationDetails) = await FetchPartyDetailsAsync(organizationNumbers, nationalIdentityNumbers); + + foreach (var smsRecipient in smsRecipients) + { + smsRecipient.CustomizedBody = + ReplacePlaceholders(smsRecipient.CustomizedBody, smsRecipient.OrganizationNumber, smsRecipient.NationalIdentityNumber, organizationDetails, personDetails); + } + + return smsRecipients; + } + + /// + public async Task> ReplaceKeywordsAsync(IEnumerable emailRecipients) + { + ArgumentNullException.ThrowIfNull(emailRecipients); + + var organizationNumbers = emailRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.OrganizationNumber)) + .Select(r => r.OrganizationNumber!) + .ToList(); + + var nationalIdentityNumbers = emailRecipients + .Where(r => !string.IsNullOrWhiteSpace(r.NationalIdentityNumber)) + .Select(r => r.NationalIdentityNumber!) + .ToList(); + + var (personDetails, organizationDetails) = await FetchPartyDetailsAsync(organizationNumbers, nationalIdentityNumbers); + + foreach (var emailRecipient in emailRecipients) + { + emailRecipient.CustomizedBody = + ReplacePlaceholders(emailRecipient.CustomizedBody, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); + + emailRecipient.CustomizedSubject = + ReplacePlaceholders(emailRecipient.CustomizedSubject, emailRecipient.OrganizationNumber, emailRecipient.NationalIdentityNumber, organizationDetails, personDetails); + } + + return emailRecipients; + } + + /// + /// Fetches party details for the given organization and national identity numbers. + /// + /// 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 personDetailsTask = (nationalIdentityNumbers != null && nationalIdentityNumbers.Count > 0) + ? _registerClient.GetPartyDetailsForPersons(nationalIdentityNumbers) + : Task.FromResult(new List()); + + var organizationDetailsTask = (organizationNumbers != null && organizationNumbers.Count > 0) + ? _registerClient.GetPartyDetailsForOrganizations(organizationNumbers) + : 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 party 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 placeholders replaced by actual values. + private static string? ReplacePlaceholders( + string? customizedText, + string? organizationNumber, + string? nationalIdentityNumber, + IEnumerable organizationDetails, + IEnumerable personDetails) + { + customizedText = ReplaceWithDetails(customizedText, organizationNumber, organizationDetails, p => p.OrganizationNumber); + + customizedText = ReplaceWithDetails(customizedText, nationalIdentityNumber, personDetails, p => p.NationalIdentityNumber, true); + + return customizedText; + } + + /// + /// Replaces placeholders in the provided text with values from the matching party details. + /// + /// The text containing placeholders to be replaced. + /// 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 partyDetails, + Func keySelector, + bool isPerson = false) + { + if (string.IsNullOrWhiteSpace(customizedText) || string.IsNullOrWhiteSpace(searchKey)) + { + return customizedText; + } + + var matchingDetail = partyDetails.FirstOrDefault(detail => keySelector(detail) == searchKey); + + if (matchingDetail == null) + { + 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/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs index a9d9383b..aa853702 100644 --- a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -41,22 +41,9 @@ public SmsNotificationService( } /// - public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, int smsCount, bool ignoreReservation = false) + 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() - { - OrganizationNumber = recipient.OrganizationNumber, - NationalIdentityNumber = recipient.NationalIdentityNumber, - IsReserved = recipient.IsReserved - }; - - 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; @@ -77,8 +64,7 @@ public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, R /// public async Task SendNotifications() { - List smsList = await _repository.GetNewNotifications(); - + var smsList = await _repository.GetNewNotifications(); 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..5a785f78 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -19,67 +19,158 @@ 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); + + 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) { int smsCount = GetSmsCountForOrder(order); - foreach (Recipient recipient in recipients) + var allSmsRecipients = await GetSmsRecipientsAsync(order, recipients); + var registeredSmsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); + + foreach (var recipient in recipients) { - await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount, order.IgnoreReservation ?? false); + var smsAddress = recipient.AddressInfo.OfType().FirstOrDefault(); + + 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, + order.RequestedSendTime, + [smsAddress], + smsRecipient, + smsCount); } } /// - public async Task ProcessOrderRetry(NotificationOrder order) + public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) { - var recipients = order.Recipients; - var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); + int smsCount = GetSmsCountForOrder(order); - await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + var allSmsRecipients = await GetSmsRecipientsAsync(order, recipients); - await ProcessOrderRetryWithoutAddressLookup(order, recipients); + foreach (var recipient in recipients) + { + 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, + smsRecipient, + smsCount, + order.IgnoreReservation ?? false); + } } - /// - public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) + /// + /// Retrieves a list of recipients for sending SMS, replacing keywords in the body with actual values. + /// + /// 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) { - int smsCount = GetSmsCountForOrder(order); - List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(order.Templates); - foreach (Recipient recipient in recipients) + var smsTemplate = order.Templates.OfType().FirstOrDefault(); + var smsRecipients = recipients.Select(recipient => new SmsRecipient { - SmsAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Sms) as SmsAddressPoint; + IsReserved = recipient.IsReserved, + OrganizationNumber = recipient.OrganizationNumber, + NationalIdentityNumber = recipient.NationalIdentityNumber, + CustomizedBody = (_keywordsService.ContainsRecipientNumberPlaceholder(smsTemplate?.Body) || _keywordsService.ContainsRecipientNamePlaceholder(smsTemplate?.Body)) ? smsTemplate?.Body : null, + }).ToList(); - if (!smsRecipients.Exists(sr => - sr.NationalIdentityNumber == recipient.NationalIdentityNumber - && sr.OrganizationNumber == recipient.OrganizationNumber - && sr.MobileNumber == addressPoint?.MobileNumber)) - { - await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient, smsCount); - } + return await _keywordsService.ReplaceKeywordsAsync(smsRecipients); + } + + /// + /// Finds the SMS recipient matching the given recipient. + /// + /// The list of SMS recipients. + /// The recipient to match. + /// The matching SMS recipient, or null if no match is found. + private static SmsRecipient? FindSmsRecipient(IEnumerable smsRecipients, Recipient recipient) + { + return smsRecipients.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; } /// diff --git a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs index 56dcd777..503a2f57 100644 --- a/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs +++ b/src/Altinn.Notifications.Integrations/Register/RegisterClient.cs @@ -1,52 +1,136 @@ 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 _contactPointLookupEndpoint = "organizations/contactpoint/lookup"; + private readonly string _nameComponentsLookupEndpoint = "parties/nameslookup"; + /// - /// 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) + { + if (organizationNumbers == null || organizationNumbers.Count == 0) { - _client = client; - _client.BaseAddress = new Uri(settings.Value.ApiRegisterEndpoint); + return []; } - /// - public async Task> GetOrganizationContactPoints(List organizationNumbers) + var lookupObject = new OrgContactPointLookup { - var lookupObject = new OrgContactPointLookup - { - OrganizationNumbers = organizationNumbers - }; + OrganizationNumbers = organizationNumbers + }; - HttpContent content = new StringContent(JsonSerializer.Serialize(lookupObject, JsonSerializerOptionsProvider.Options), Encoding.UTF8, "application/json"); + HttpContent content = new StringContent(JsonSerializer.Serialize(lookupObject, _jsonSerializerOptions), Encoding.UTF8, "application/json"); - var response = await _client.PostAsync("organizations/contactpoint/lookup", content); + var response = await _client.PostAsync(_contactPointLookupEndpoint, content); - if (!response.IsSuccessStatusCode) - { - throw await PlatformHttpException.CreateAsync(response); - } + if (!response.IsSuccessStatusCode) + { + throw await PlatformHttpException.CreateAsync(response); + } + + string responseContent = await response.Content.ReadAsStringAsync(); + var contactPoints = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions)?.ContactPointsList; + return contactPoints ?? []; + } - string responseContent = await response.Content.ReadAsStringAsync(); - List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; - return contactPoints!; + /// + /// Asynchronously retrieves party details for the specified organizations. + /// + /// 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(organizationNumbers: organizationNumbers); + + 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 ?? []; + } + + /// + /// Asynchronously retrieves party details for the specified persons. + /// + /// 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 individuals. + /// + public async Task> GetPartyDetailsForPersons(List socialSecurityNumbers) + { + if (socialSecurityNumbers == null || socialSecurityNumbers.Count == 0) + { + return []; + } + + var partyDetailsLookupBatch = new PartyDetailsLookupBatch(socialSecurityNumbers: socialSecurityNumbers); + + HttpContent content = new StringContent(JsonSerializer.Serialize(partyDetailsLookupBatch), Encoding.UTF8, "application/json"); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{_nameComponentsLookupEndpoint}") + { + Content = content + }; + + var response = await _client.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + throw await PlatformHttpException.CreateAsync(response); + } + + string responseContent = await response.Content.ReadAsStringAsync(); + var partyNamesLookupResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + return partyNamesLookupResponse?.PartyDetailsList ?? []; } } 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 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; 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 new file mode 100644 index 00000000..c4868af8 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.36/02-functions-and-procedures.sql @@ -0,0 +1,516 @@ +-- 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/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 3f68b070..afd602b1 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 = @@ -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. /// @@ -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); @@ -105,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); @@ -114,11 +116,11 @@ public async Task> GetRecipients(Guid orderId) { while (await reader.ReadAsync()) { - searchResult.Add(new EmailRecipient() + searchResult.Add(new EmailRecipient() { + ToAddress = reader.GetValue("toaddress"), OrganizationNumber = reader.GetValue("recipientorgno"), NationalIdentityNumber = reader.GetValue("recipientnin"), - ToAddress = reader.GetValue("toaddress") }); } } diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index 9d7c47f4..abd0e4bd 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); 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; } 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; } 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); 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 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.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..694e6b56 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", @@ -81,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" } } } }, @@ -94,12 +93,6 @@ public NotificationOrderTests() { new JsonObject() { - { - "nationalIdentityNumber", "nationalidentitynumber" - }, - { - "isReserved", false - }, { "addressInfo", new JsonArray() { @@ -110,10 +103,16 @@ public NotificationOrderTests() { "emailAddress", "recipient1@domain.com" } } } + }, + { + "isReserved", false + }, + { + "nationalIdentityNumber", "nationalidentitynumber" } } } - } + } }.ToJsonString(); } 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); + } +} 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); + } +} diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index 6266a682..557d3108 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; @@ -23,7 +24,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); [Fact] public async Task SendNotifications_ProducerCalledOnceForEachRetrievedEmail() @@ -82,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() { @@ -102,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); @@ -117,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() { @@ -136,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); @@ -151,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() { @@ -171,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); @@ -186,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(); + var emailRecipient = new EmailRecipient() { OrganizationNumber = "skd-orgno" }; EmailNotification expected = new() { @@ -205,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(); @@ -214,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())); @@ -227,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)); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index 3a03818a..300c13d2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -28,23 +28,23 @@ 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(); - 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())); 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()), Times.Exactly(2)); } [Fact] @@ -67,16 +67,22 @@ public async Task ProcessOrder_ExpectedInputToNotificationService() 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") + ] }; - 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())); + 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); @@ -94,14 +100,14 @@ public async Task ProcessOrder_NotificationServiceThrowsException_RepositoryNotC var order = new NotificationOrder() { NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { + Recipients = + [ new() - } + ] }; 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())) .ThrowsAsync(new Exception()); var repoMock = new Mock(); @@ -113,7 +119,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()), Times.Once); repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); } @@ -140,7 +146,8 @@ public async Task ProcessOrder_RecipientMissingEmail_ContactPointServiceCalled() s => s.CreateNotification( It.IsAny(), It.IsAny(), - It.Is(r => r.NationalIdentityNumber == "123456"), + It.IsAny>(), + It.Is(r => r.NationalIdentityNumber == "123456"), It.IsAny())); var contactPointServiceMock = new Mock(); @@ -164,17 +171,17 @@ 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(); - 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())); var emailRepoMock = new Mock(); emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() @@ -190,13 +197,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()), 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) + IEmailNotificationRepository? emailRepo = null, + IEmailNotificationService? emailService = null, + IContactPointService? contactPointService = null, + IKeywordsService? keywordsService = null) { if (emailRepo == null) { @@ -218,6 +226,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); } } 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..88d680d2 --- /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 KeywordsService _keywordsService; + private readonly Mock _registerClientMock; + + 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 ", 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 ", 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); + } +} diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs index a972cab3..243c3a46 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; @@ -8,6 +9,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; @@ -25,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() { @@ -77,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); @@ -111,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); @@ -146,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(); @@ -176,7 +154,7 @@ 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(); @@ -189,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(); @@ -198,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)); @@ -210,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())) @@ -232,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())); @@ -253,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(); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs index c53b205d..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())); + 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())); + 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()), 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,7 +120,8 @@ 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())); @@ -124,7 +131,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 +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())); + 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 +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()), Times.Exactly(2)); + notificationServiceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Theory] @@ -195,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) { @@ -217,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); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs index 154f9f4b..54a87b46 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,38 +86,195 @@ public async Task GetOrganizationContactPoints_FailureResponse_ExceptionIsThrown Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } + [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() + { + // 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 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 = 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; + + case "null-contact-points-list": + contentData = new OrgContactPointsList { ContactPointsList = [] }; + break; } - JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; + return CreateMockResponse(contentData, statusCode); + } - return Task.FromResult( - new HttpResponseMessage() + private Task GetPartyDetailsResponse(PartyDetailsLookupBatch lookup) + { + object? contentData = null; + HttpStatusCode statusCode = HttpStatusCode.OK; + + var firstRequest = lookup.PartyDetailsLookupRequestList?.FirstOrDefault(); + if (firstRequest != null) + { + if (firstRequest.OrganizationNumber != null) + { + switch (firstRequest.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; + } + } + else if (firstRequest.SocialSecurityNumber != null) { - StatusCode = statusCode, - Content = content - }); + switch (firstRequest.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; + } + } + } + + return CreateMockResponse(contentData, statusCode); } }