diff --git a/src/Altinn.Notifications.Core/Enums/NotificationChannel.cs b/src/Altinn.Notifications.Core/Enums/NotificationChannel.cs index 49e30aa8..c49014bb 100644 --- a/src/Altinn.Notifications.Core/Enums/NotificationChannel.cs +++ b/src/Altinn.Notifications.Core/Enums/NotificationChannel.cs @@ -13,5 +13,15 @@ public enum NotificationChannel /// /// The selected channel for the notification is SMS. /// - Sms + Sms, + + /// + /// The selected channel for the notification is email and to use SMS if email is not available. + /// + EmailPreferred, + + /// + /// The selected channel for the notification is SMS and to use email if SMS is not available. + /// + SmsPreferred } diff --git a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs index c3004c28..47bf5f31 100644 --- a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static void AddCoreServices(this IServiceCollection services, IConfigurat .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 8170235e..ff506db5 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -1,4 +1,5 @@ -using Altinn.Notifications.Core.Helpers; +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Helpers; using Altinn.Notifications.Core.Integrations; using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Address; @@ -84,6 +85,114 @@ await AugmentRecipients( }); } + /// + public async Task AddPreferredContactPoints(NotificationChannel channel, List recipients, string? resourceId) + { + await AugmentRecipients( + recipients, + resourceId, + (recipient, userContactPoints) => + { + if (channel == NotificationChannel.EmailPreferred) + { + 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)); + } + + 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; + }); + } + + 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 AddPreferredOrFallbackContactPoint( + Recipient recipient, + TPreferred preferredContact, + TFallback fallbackContact, + Func preferredSelector, + Func fallbackSelector) + { + if (!string.IsNullOrEmpty(preferredContact?.ToString())) + { + recipient.AddressInfo.Add(preferredSelector(preferredContact)); + } + else if (!string.IsNullOrEmpty(fallbackContact?.ToString())) + { + recipient.AddressInfo.Add(fallbackSelector(fallbackContact)); + } + } + private async Task> AugmentRecipients( List recipients, string? resourceId, diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index f131e67e..e271bd11 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -38,6 +38,12 @@ public async Task ProcessOrder(NotificationOrder order) await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); + 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); @@ -52,9 +58,15 @@ public async Task ProcessOrderRetry(NotificationOrder order) await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); + await ProcessOrderRetryWithoutAddressLookup(order, recipients); + } + + /// + public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) + { List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); - foreach (Recipient recipient in order.Recipients) + foreach (Recipient recipient in recipients) { EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint; diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs index 48f21384..8a0e1eb3 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs @@ -1,4 +1,5 @@ -using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; namespace Altinn.Notifications.Core.Services.Interfaces { @@ -24,5 +25,15 @@ public interface IContactPointService /// 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 + /// + /// 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); } } diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs index e011af04..b073ef60 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs @@ -1,4 +1,5 @@ -using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Orders; namespace Altinn.Notifications.Core.Services.Interfaces; @@ -13,7 +14,19 @@ public interface IEmailOrderProcessingService public Task ProcessOrder(NotificationOrder order); /// - /// Retry processing of an order + /// Processes a notification order for the provided list of recipients + /// without looking up additional recipient data + /// + public Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients); + + /// + /// Retry processing of a notification order /// public Task ProcessOrderRetry(NotificationOrder order); + + /// + /// Retryprocessing 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/Interfaces/IPreferredChannelProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IPreferredChannelProcessingService.cs new file mode 100644 index 00000000..bda36e6e --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IPreferredChannelProcessingService.cs @@ -0,0 +1,19 @@ +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface for the order processing service speficic to email or sms preferred orders +/// +public interface IPreferredChannelProcessingService +{ + /// + /// Processes a notification order + /// + public Task ProcessOrder(NotificationOrder order); + + /// + /// Retry processing of an order + /// + public Task ProcessOrderRetry(NotificationOrder order); +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs index 535368dc..06ab12f9 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs @@ -1,4 +1,5 @@ -using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Orders; namespace Altinn.Notifications.Core.Services.Interfaces; @@ -12,8 +13,20 @@ public interface ISmsOrderProcessingService /// public Task ProcessOrder(NotificationOrder order); + /// + /// Processes a notification order for the provided list of recipients + /// without looking up additional recipient data + /// + public Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients); + /// /// Retry processing of an order /// public Task ProcessOrderRetry(NotificationOrder order); + + /// + /// Retryprocessing 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/OrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs index 8df75812..22ba1cde 100644 --- a/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs @@ -21,6 +21,7 @@ public class OrderProcessingService : IOrderProcessingService private readonly IOrderRepository _orderRepository; private readonly IEmailOrderProcessingService _emailProcessingService; private readonly ISmsOrderProcessingService _smsProcessingService; + private readonly IPreferredChannelProcessingService _preferredChannelProcessingService; private readonly IConditionClient _conditionClient; private readonly IKafkaProducer _producer; private readonly string _pastDueOrdersTopic; @@ -33,6 +34,7 @@ public OrderProcessingService( IOrderRepository orderRepository, IEmailOrderProcessingService emailProcessingService, ISmsOrderProcessingService smsProcessingService, + IPreferredChannelProcessingService preferredChannelProcessingService, IConditionClient conditionClient, IKafkaProducer producer, IOptions kafkaSettings, @@ -41,6 +43,7 @@ public OrderProcessingService( _orderRepository = orderRepository; _emailProcessingService = emailProcessingService; _smsProcessingService = smsProcessingService; + _preferredChannelProcessingService = preferredChannelProcessingService; _conditionClient = conditionClient; _producer = producer; _pastDueOrdersTopic = kafkaSettings.Value.PastDueOrdersTopicName; @@ -89,6 +92,10 @@ public async Task ProcessOrder(NotificationOrder order) case NotificationChannel.Sms: await _smsProcessingService.ProcessOrder(order); break; + case NotificationChannel.EmailPreferred: + case NotificationChannel.SmsPreferred: + await _preferredChannelProcessingService.ProcessOrder(order); + break; } await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.Completed); @@ -113,6 +120,10 @@ public async Task ProcessOrderRetry(NotificationOrder order) case NotificationChannel.Sms: await _smsProcessingService.ProcessOrderRetry(order); break; + case NotificationChannel.EmailPreferred: + case NotificationChannel.SmsPreferred: + await _preferredChannelProcessingService.ProcessOrderRetry(order); + break; } await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.Completed); diff --git a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs index 3676a5cb..e45cd877 100644 --- a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs @@ -75,32 +75,25 @@ public async Task RegisterNotificationOrder(No private async Task GetRecipientLookupResult(List originalRecipients, NotificationChannel channel, string? resourceId) { - List recipientsWithoutContactPoint = []; - - foreach (var recipient in originalRecipients) - { - if (channel == NotificationChannel.Email && !recipient.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)) - { - recipientsWithoutContactPoint.Add(recipient.DeepCopy()); - } - else if (channel == NotificationChannel.Sms && !recipient.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)) - { - recipientsWithoutContactPoint.Add(recipient.DeepCopy()); - } - } + List recipientsWithoutContactPoint = GetMissingContactRecipientList(channel, originalRecipients); if (recipientsWithoutContactPoint.Count == 0) { return null; } - if (channel == NotificationChannel.Email) - { - await _contactPointService.AddEmailContactPoints(recipientsWithoutContactPoint, resourceId); - } - else if (channel == NotificationChannel.Sms) + switch (channel) { - await _contactPointService.AddSmsContactPoints(recipientsWithoutContactPoint, resourceId); + case NotificationChannel.Email: + await _contactPointService.AddEmailContactPoints(recipientsWithoutContactPoint, resourceId); + break; + case NotificationChannel.Sms: + await _contactPointService.AddSmsContactPoints(recipientsWithoutContactPoint, resourceId); + break; + case NotificationChannel.EmailPreferred: + case NotificationChannel.SmsPreferred: + await _contactPointService.AddPreferredContactPoints(channel, recipientsWithoutContactPoint, resourceId); + break; } var isReserved = recipientsWithoutContactPoint.Where(r => r.IsReserved.HasValue && r.IsReserved.Value).Select(r => r.NationalIdentityNumber!).ToList(); @@ -108,13 +101,7 @@ public async Task RegisterNotificationOrder(No RecipientLookupResult lookupResult = new() { IsReserved = isReserved, - MissingContact = recipientsWithoutContactPoint - .Where(r => channel == NotificationChannel.Email ? - !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email) : - !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)) - .Select(r => r.OrganizationNumber ?? r.NationalIdentityNumber!) - .Except(isReserved) - .ToList() + MissingContact = GetMissingContactListIds(channel, recipientsWithoutContactPoint).Except(isReserved).ToList() }; int recipientsWeCannotReach = lookupResult.MissingContact.Union(lookupResult.IsReserved).ToList().Count; @@ -131,6 +118,46 @@ public async Task RegisterNotificationOrder(No return lookupResult; } + private static List GetMissingContactListIds(NotificationChannel channel, List recipients) + { + return channel switch + { + NotificationChannel.Email => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)) + .Select(r => r.OrganizationNumber ?? r.NationalIdentityNumber!) + .ToList(), + NotificationChannel.Sms => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)) + .Select(r => r.OrganizationNumber ?? r.NationalIdentityNumber!) + .ToList(), + NotificationChannel.EmailPreferred or NotificationChannel.SmsPreferred => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email || ap.AddressType == AddressType.Sms)) + .Select(r => r.OrganizationNumber ?? r.NationalIdentityNumber!) + .ToList(), + _ => [], + }; + } + + private static List GetMissingContactRecipientList(NotificationChannel channel, List recipients) + { + return channel switch + { + NotificationChannel.Email => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)) + .Select(r => r.DeepCopy()) + .ToList(), + NotificationChannel.Sms => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)) + .Select(r => r.DeepCopy()) + .ToList(), + NotificationChannel.EmailPreferred or NotificationChannel.SmsPreferred => recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email || ap.AddressType == AddressType.Sms)) + .Select(r => r.DeepCopy()) + .ToList(), + _ => [], + }; + } + private List SetSenderIfNotDefined(List templates) { foreach (var template in templates.OfType().Where(template => string.IsNullOrEmpty(template.FromAddress))) diff --git a/src/Altinn.Notifications.Core/Services/PreferredChannelProcessingService.cs b/src/Altinn.Notifications.Core/Services/PreferredChannelProcessingService.cs new file mode 100644 index 00000000..65401d86 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/PreferredChannelProcessingService.cs @@ -0,0 +1,116 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the +/// +public class PreferredChannelProcessingService : IPreferredChannelProcessingService +{ + private readonly IEmailOrderProcessingService _emailProcessingService; + private readonly ISmsOrderProcessingService _smsProcessingService; + private readonly IContactPointService _contactPointService; + + /// + /// Initializes a new instance of the class. + /// + public PreferredChannelProcessingService( + IEmailOrderProcessingService emailProcessingService, + ISmsOrderProcessingService smsProcessingService, + IContactPointService contactPointService) + { + _emailProcessingService = emailProcessingService; + _smsProcessingService = smsProcessingService; + _contactPointService = contactPointService; + } + + /// + public async Task ProcessOrder(NotificationOrder order) + { + await ProcessOrderInternal(order, false); + } + + /// + public async Task ProcessOrderRetry(NotificationOrder order) + { + await ProcessOrderInternal(order, true); + } + + private async Task ProcessOrderInternal(NotificationOrder order, bool isRetry) + { + List recipients = order.Recipients; + List recipientsWithoutContactPoint = + recipients + .Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email || ap.AddressType == AddressType.Sms)) + .ToList(); + + await _contactPointService.AddPreferredContactPoints(order.NotificationChannel, recipientsWithoutContactPoint, order.ResourceId); + + List preferredChannelRecipients; + List fallBackChannelRecipients; + + switch (order.NotificationChannel) + { + case NotificationChannel.EmailPreferred: + (preferredChannelRecipients, fallBackChannelRecipients) = + GenerateRecipientLists(recipients, AddressType.Email, AddressType.Sms); + + if (isRetry) + { + await _emailProcessingService.ProcessOrderRetryWithoutAddressLookup(order, preferredChannelRecipients); + await _smsProcessingService.ProcessOrderRetryWithoutAddressLookup(order, fallBackChannelRecipients); + } + else + { + await _emailProcessingService.ProcessOrderWithoutAddressLookup(order, preferredChannelRecipients); + await _smsProcessingService.ProcessOrderWithoutAddressLookup(order, fallBackChannelRecipients); + } + + break; + + case NotificationChannel.SmsPreferred: + (preferredChannelRecipients, fallBackChannelRecipients) = + GenerateRecipientLists(recipients, AddressType.Sms, AddressType.Email); + + if (isRetry) + { + await _smsProcessingService.ProcessOrderRetryWithoutAddressLookup(order, preferredChannelRecipients); + await _emailProcessingService.ProcessOrderRetryWithoutAddressLookup(order, fallBackChannelRecipients); + } + else + { + await _smsProcessingService.ProcessOrderWithoutAddressLookup(order, preferredChannelRecipients); + await _emailProcessingService.ProcessOrderWithoutAddressLookup(order, fallBackChannelRecipients); + } + + break; + } + } + + private static (List PreferredChannelRecipients, List FallBackChannelRecipients) GenerateRecipientLists( + List recipients, + AddressType preferredAddressType, + AddressType fallbackAddressType) + { + List preferredChannelRecipients = recipients + .Where(r => r.AddressInfo.Exists(ap => ap.AddressType == preferredAddressType)) + .ToList(); + + List fallBackChannelRecipients = recipients + .Where(r => r.AddressInfo.Exists(ap => ap.AddressType == fallbackAddressType)) + .Except(preferredChannelRecipients) + .ToList(); + + List missingContactRecipients = recipients + .Except(preferredChannelRecipients) + .Except(fallBackChannelRecipients) + .ToList(); + + preferredChannelRecipients.AddRange(missingContactRecipients); + + return (preferredChannelRecipients, fallBackChannelRecipients); + } +} diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs index 4169843e..688da616 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using System.Web; +using System.Web; using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models; @@ -38,6 +37,12 @@ public async Task ProcessOrder(NotificationOrder order) var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + await ProcessOrderWithoutAddressLookup(order, recipients); + } + + /// + public async Task ProcessOrderWithoutAddressLookup(NotificationOrder order, List recipients) + { int smsCount = GetSmsCountForOrder(order); foreach (Recipient recipient in recipients) @@ -54,10 +59,16 @@ public async Task ProcessOrderRetry(NotificationOrder order) await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); + await ProcessOrderRetryWithoutAddressLookup(order, recipients); + } + + /// + public async Task ProcessOrderRetryWithoutAddressLookup(NotificationOrder order, List recipients) + { int smsCount = GetSmsCountForOrder(order); List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); - foreach (Recipient recipient in order.Recipients) + foreach (Recipient recipient in recipients) { SmsAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Sms) as SmsAddressPoint; diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 4945270d..f9d13a46 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Integrations; using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Address; @@ -261,12 +263,12 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_N MobileNumber = "+4748123456", Email = "user-1@domain.com" }, - new UserContactPoints() - { - UserId = 200009, - MobileNumber = "004699999999", - Email = "user-9@domain.com" - } + new UserContactPoints() + { + UserId = 200009, + MobileNumber = "004699999999", + Email = "user-9@domain.com" + } ] } ]); @@ -362,6 +364,312 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_A Assert.Equivalent(expectedOutput, input); } + [Fact] + public async Task AddPreferredContactPoints_EmailPreferred_PersonRecipient() + { + // Arrange + List recipients = [ + new Recipient() { NationalIdentityNumber = "0" }, + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" } + ]; + + var profileClientMock = new Mock(); + + profileClientMock + .Setup(p => p.GetUserContactPoints(It.IsAny>())) + .ReturnsAsync([ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" }]); + + var service = GetTestService(profileClientMock.Object, null, null); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.EmailPreferred, recipients, null); + + // Assert + Recipient user0 = recipients[0]; + Assert.Single(user0.AddressInfo); + Assert.IsType(user0.AddressInfo[0]); + Assert.Equal("user0@email.com", ((EmailAddressPoint)user0.AddressInfo[0]).EmailAddress); + + Recipient user1 = recipients[1]; + Assert.Single(user1.AddressInfo); + Assert.IsType(user1.AddressInfo[0]); + Assert.Equal("+4799999991", ((SmsAddressPoint)user1.AddressInfo[0]).MobileNumber); + + Recipient user2 = recipients[2]; + Assert.Single(user2.AddressInfo); + Assert.IsType(user2.AddressInfo[0]); + Assert.Equal("user2@email.com", ((EmailAddressPoint)user2.AddressInfo[0]).EmailAddress); + + Recipient user3 = recipients[3]; + Assert.Empty(user3.AddressInfo); + } + + [Fact] + public async Task AddPreferredContactPoints_SmsPreferred_PersonRecipient() + { + // Arrange + List recipients = [ + new Recipient() { NationalIdentityNumber = "0" }, + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + ]; + + var profileClientMock = new Mock(); + + profileClientMock + .Setup(p => p.GetUserContactPoints(It.IsAny>())) + .ReturnsAsync([ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" }]); + + var service = GetTestService(profileClientMock.Object, null, null); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.SmsPreferred, recipients, null); + + // Assert + Recipient user0 = recipients[0]; + Assert.Single(user0.AddressInfo); + Assert.IsType(user0.AddressInfo[0]); + Assert.Equal("user0@email.com", ((EmailAddressPoint)user0.AddressInfo[0]).EmailAddress); + + Recipient user1 = recipients[1]; + Assert.Single(user1.AddressInfo); + Assert.IsType(user1.AddressInfo[0]); + Assert.Equal("+4799999991", ((SmsAddressPoint)user1.AddressInfo[0]).MobileNumber); + + Recipient user2 = recipients[2]; + Assert.Single(user2.AddressInfo); + Assert.IsType(user2.AddressInfo[0]); + Assert.Equal("+4799999992", ((SmsAddressPoint)user2.AddressInfo[0]).MobileNumber); + + Recipient user3 = recipients[3]; + Assert.Empty(user3.AddressInfo); + } + + [Fact] + public async Task AddPreferredContactPoints_EmailPreferred_OrgRecipient() + { + // Arrange + List recipients = [ + new Recipient() { OrganizationNumber = "0" }, + new Recipient() { OrganizationNumber = "1" }, + new Recipient() { OrganizationNumber = "2" }, + new Recipient() { OrganizationNumber = "3" }, + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([ + new OrganizationContactPoints() { OrganizationNumber = "0", EmailList = ["0@org.com"] }, + new OrganizationContactPoints() { OrganizationNumber = "1", MobileNumberList = ["+4799999991"] }, + new OrganizationContactPoints() { OrganizationNumber = "2", EmailList = ["2@org.com"], MobileNumberList = ["+4799999992"] }, + new OrganizationContactPoints() { OrganizationNumber = "3" }]); + + var service = GetTestService(null, registerClientMock.Object, null); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.EmailPreferred, recipients, null); + + // Assert + Recipient org0 = recipients[0]; + Assert.Single(org0.AddressInfo); + Assert.IsType(org0.AddressInfo[0]); + Assert.Equal("0@org.com", ((EmailAddressPoint)org0.AddressInfo[0]).EmailAddress); + + Recipient org1 = recipients[1]; + Assert.Single(org1.AddressInfo); + Assert.IsType(org1.AddressInfo[0]); + Assert.Equal("+4799999991", ((SmsAddressPoint)org1.AddressInfo[0]).MobileNumber); + + Recipient org2 = recipients[2]; + Assert.Single(org2.AddressInfo); + Assert.IsType(org2.AddressInfo[0]); + Assert.Equal("2@org.com", ((EmailAddressPoint)org2.AddressInfo[0]).EmailAddress); + + Recipient org3 = recipients[3]; + Assert.Empty(org3.AddressInfo); + } + + [Fact] + public async Task AddPreferredContactPoints_SmsPreferred_OrgOfficialRecipient() + { + // Arrange + List recipients = [ + new Recipient() { OrganizationNumber = "0" }, + new Recipient() { OrganizationNumber = "1" }, + new Recipient() { OrganizationNumber = "2" }, + new Recipient() { OrganizationNumber = "3" }, + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([ + new OrganizationContactPoints() { OrganizationNumber = "0", EmailList = ["0@org.com"] }, + new OrganizationContactPoints() { OrganizationNumber = "1", MobileNumberList = ["+4799999991"] }, + new OrganizationContactPoints() { OrganizationNumber = "2", EmailList = ["2@org.com"], MobileNumberList = ["+4799999992"] }, + new OrganizationContactPoints() { OrganizationNumber = "3" }]); + + var service = GetTestService(null, registerClientMock.Object, null); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.SmsPreferred, recipients, null); + + // Assert + Recipient org0 = recipients[0]; + Assert.Single(org0.AddressInfo); + Assert.IsType(org0.AddressInfo[0]); + Assert.Equal("0@org.com", ((EmailAddressPoint)org0.AddressInfo[0]).EmailAddress); + + Recipient org1 = recipients[1]; + Assert.Single(org1.AddressInfo); + Assert.IsType(org1.AddressInfo[0]); + Assert.Equal("+4799999991", ((SmsAddressPoint)org1.AddressInfo[0]).MobileNumber); + + Recipient org2 = recipients[2]; + Assert.Single(org2.AddressInfo); + Assert.IsType(org2.AddressInfo[0]); + Assert.Equal("+4799999992", ((SmsAddressPoint)org2.AddressInfo[0]).MobileNumber); + + Recipient org3 = recipients[3]; + Assert.Empty(org3.AddressInfo); + } + + [Fact] + public async Task AddPreferredContactPoints_EmailPreferred_OrgUserRegisteredRecipient() + { + // Arrange + List recipients = [ + new Recipient() { OrganizationNumber = "0" } + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([]); + + var profileClientMock = new Mock(); + + profileClientMock + .Setup(p => p.GetUserRegisteredContactPoints(It.IsAny>(), It.IsAny())) + .ReturnsAsync([ + new OrganizationContactPoints() + { + OrganizationNumber = "0", + UserContactPoints = + [ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" } + ] + } + ]); + + var authorizationServiceMock = new Mock(); + + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.IsAny())) + .ReturnsAsync([ + new OrganizationContactPoints() + { + OrganizationNumber = "0", + UserContactPoints = + [ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" } + ] + } + ]); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.EmailPreferred, recipients, "resource"); + + // Assert + Recipient org0 = recipients[0]; + Assert.Equal(3, org0.AddressInfo.Count); + Assert.Equal("user0@email.com", ((EmailAddressPoint)org0.AddressInfo[0]).EmailAddress); + Assert.Equal("+4799999991", ((SmsAddressPoint)org0.AddressInfo[1]).MobileNumber); + Assert.Equal("user2@email.com", ((EmailAddressPoint)org0.AddressInfo[2]).EmailAddress); + } + + [Fact] + public async Task AddPreferredContactPoints_SmsPreferred_OrgUserRegisteredRecipient() + { + // Arrange + List recipients = [ + new Recipient() { OrganizationNumber = "0" } + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([]); + + var profileClientMock = new Mock(); + + profileClientMock + .Setup(p => p.GetUserRegisteredContactPoints(It.IsAny>(), It.IsAny())) + .ReturnsAsync([ + new OrganizationContactPoints() + { + OrganizationNumber = "0", + UserContactPoints = + [ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" } + ] + } + ]); + + var authorizationServiceMock = new Mock(); + + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.IsAny())) + .ReturnsAsync([ + new OrganizationContactPoints() + { + OrganizationNumber = "0", + UserContactPoints = + [ + new UserContactPoints() { NationalIdentityNumber = "0", Email = "user0@email.com" }, + new UserContactPoints() { NationalIdentityNumber = "1", MobileNumber = "+4799999991" }, + new UserContactPoints() { NationalIdentityNumber = "2", Email = "user2@email.com", MobileNumber = "+4799999992" }, + new UserContactPoints() { NationalIdentityNumber = "3" } + ] + } + ]); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + + // Act + await service.AddPreferredContactPoints(NotificationChannel.SmsPreferred, recipients, "resource"); + + // Assert + Recipient org0 = recipients[0]; + Assert.Equal(3, org0.AddressInfo.Count); + Assert.Equal("user0@email.com", ((EmailAddressPoint)org0.AddressInfo[0]).EmailAddress); + Assert.Equal("+4799999991", ((SmsAddressPoint)org0.AddressInfo[1]).MobileNumber); + Assert.Equal("+4799999992", ((SmsAddressPoint)org0.AddressInfo[2]).MobileNumber); + } + private static ContactPointService GetTestService( IProfileClient? profileClient = null, IRegisterClient? registerClient = null, diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs index 458c5167..ceb67f0f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs @@ -97,6 +97,37 @@ public async Task ProcessOrder_SmsOrder_SmsServiceCalled() emailMockService.Verify(e => e.ProcessOrder(It.IsAny()), Times.Never); } + [Theory] + [InlineData(NotificationChannel.EmailPreferred)] + [InlineData(NotificationChannel.SmsPreferred)] + public async Task ProcessOrder_EmailOrSmsPreferredOrder_PreferredServiceCalled(NotificationChannel notificationChannel) + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = notificationChannel, + }; + + var emailMockService = new Mock(); + emailMockService.Setup(e => e.ProcessOrder(It.IsAny())); + + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrder(It.IsAny())); + + var preferredMockService = new Mock(); + preferredMockService.Setup(e => e.ProcessOrder(It.IsAny())); + + var orderProcessingService = GetTestService(emailMock: emailMockService.Object, smsMock: smsMockService.Object, preferredMock: preferredMockService.Object); + + // Act + await orderProcessingService.ProcessOrder(order); + + // Assert + emailMockService.Verify(e => e.ProcessOrder(It.IsAny()), Times.Never); + smsMockService.Verify(s => s.ProcessOrder(It.IsAny()), Times.Never); + preferredMockService.Verify(s => s.ProcessOrder(It.IsAny()), Times.Once); + } + [Fact] public async Task ProcessOrder_SendConditionNotMet_ProcessingStops() { @@ -182,6 +213,36 @@ public async Task ProcessOrderRetry_SmsOrder_SmsServiceCalled() emailMockService.Verify(e => e.ProcessOrderRetry(It.IsAny()), Times.Never); } + [Theory] + [InlineData(NotificationChannel.EmailPreferred)] + [InlineData(NotificationChannel.SmsPreferred)] + public async Task ProcessOrderRetry_EmailOrSmsPreferredOrder_PreferredServiceCalled(NotificationChannel notificationChannel) + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = notificationChannel, + }; + var emailMockService = new Mock(); + emailMockService.Setup(e => e.ProcessOrderRetry(It.IsAny())); + + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrderRetry(It.IsAny())); + + var preferredMockService = new Mock(); + preferredMockService.Setup(e => e.ProcessOrderRetry(It.IsAny())); + + var orderProcessingService = GetTestService(emailMock: emailMockService.Object, smsMock: smsMockService.Object, preferredMock: preferredMockService.Object); + + // Act + await orderProcessingService.ProcessOrderRetry(order); + + // Assert + emailMockService.Verify(e => e.ProcessOrderRetry(It.IsAny()), Times.Never); + smsMockService.Verify(s => s.ProcessOrderRetry(It.IsAny()), Times.Never); + preferredMockService.Verify(s => s.ProcessOrderRetry(It.IsAny()), Times.Once); + } + [Fact] public async Task ProcessOrderRetry_SendConditionNotMet_ProcessingStops() { @@ -312,6 +373,7 @@ private static OrderProcessingService GetTestService( IOrderRepository? repo = null, IEmailOrderProcessingService? emailMock = null, ISmsOrderProcessingService? smsMock = null, + IPreferredChannelProcessingService? preferredMock = null, IKafkaProducer? producer = null, IConditionClient? conditionClient = null) { @@ -333,6 +395,12 @@ private static OrderProcessingService GetTestService( smsMock = smsMockService.Object; } + if (preferredMock == null) + { + var preferredMockService = new Mock(); + preferredMock = preferredMockService.Object; + } + if (producer == null) { var producerMock = new Mock(); @@ -347,6 +415,6 @@ private static OrderProcessingService GetTestService( var kafkaSettings = new Altinn.Notifications.Core.Configuration.KafkaSettings() { PastDueOrdersTopicName = _pastDueTopicName }; - return new OrderProcessingService(repo, emailMock, smsMock, conditionClient, producer, Options.Create(kafkaSettings), new LoggerFactory().CreateLogger()); + return new OrderProcessingService(repo, emailMock, smsMock, preferredMock, conditionClient, producer, Options.Create(kafkaSettings), new LoggerFactory().CreateLogger()); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs index cc070e27..9e368b02 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs @@ -213,7 +213,7 @@ public async Task RegisterNotificationOrder_ForSms_NoSenderNumberDefaultInserted } [Fact] - public async Task RegisterNotificationOrder_LookupFails_OrderCreated() + public async Task RegisterNotificationOrder_ForSms_LookupFails_OrderCreated() { // Arrange DateTime sendTime = DateTime.UtcNow; @@ -317,7 +317,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupPartialSuccess_OrderCre repoMock.VerifyAll(); contactPointMock.VerifyAll(); } - + [Fact] public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() { @@ -335,7 +335,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() RequestedSendTime = sendTime, Recipients = [ new Recipient() { NationalIdentityNumber = "16069412345" }, - ], + ], Templates = { new SmsTemplate { Body = "sms-body", SenderNumber = "TestDefaultSmsSenderNumberNumber" } } }; @@ -346,7 +346,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() NotificationChannel = NotificationChannel.Sms, Recipients = [ new Recipient() { NationalIdentityNumber = "16069412345" }, - ], + ], RequestedSendTime = sendTime, Templates = { new SmsTemplate { Body = "sms-body" } } }; @@ -385,6 +385,175 @@ public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() contactPointMock.VerifyAll(); } + [Fact] + public async Task RegisterNotificationOrder_ForSmsPreferred_LookupFails_OrderCreated() + { + // Arrange + DateTime sendTime = DateTime.UtcNow; + DateTime createdTime = DateTime.UtcNow.AddMinutes(-2); + Guid id = Guid.NewGuid(); + + NotificationOrderRequest input = new() + { + Creator = new Creator("ttd"), + + NotificationChannel = NotificationChannel.SmsPreferred, + Recipients = [new Recipient() { NationalIdentityNumber = "1" }], + SendersReference = "senders-reference", + RequestedSendTime = sendTime, + Templates = [ + new SmsTemplate { Body = "sms-body" }, + new EmailTemplate { Body = "email-body", FromAddress = "noreply@altinn.no" }] + }; + + Mock repoMock = new(); + repoMock + .Setup(r => r.Create(It.IsAny())) + .ReturnsAsync((NotificationOrder order) => order); + + var service = GetTestService(repoMock.Object, null, id, createdTime); + + // Act + NotificationOrderRequestResponse actual = await service.RegisterNotificationOrder(input); + + // Assert + Assert.Equal(RecipientLookupStatus.Failed, actual.RecipientLookup?.Status); + repoMock.Verify(r => r.Create(It.IsAny()), Times.Once); + } + + [Fact] + public async Task RegisterNotificationOrder_ForSmsPreferred_LookupPartialSuccess_OrderCreated() + { + // Arrange + DateTime sendTime = DateTime.UtcNow; + DateTime createdTime = DateTime.UtcNow.AddMinutes(-2); + Guid id = Guid.NewGuid(); + + NotificationOrderRequest input = new() + { + Creator = new Creator("ttd"), + + NotificationChannel = NotificationChannel.SmsPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + new Recipient() { NationalIdentityNumber = "4" } + + ], + RequestedSendTime = sendTime, + Templates = [ + new SmsTemplate { Body = "sms-body" }, + new EmailTemplate { Body = "email-body", FromAddress = "noreply@altinn.no" }] + }; + + Mock repoMock = new(); + repoMock + .Setup(r => r.Create(It.IsAny())) + .ReturnsAsync((NotificationOrder order) => order); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(input.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "3") + { + recipient.IsReserved = true; + } + else if (recipient.NationalIdentityNumber == "4") + { + recipient.IsReserved = false; + } + } + }); + + var service = GetTestService(repoMock.Object, contactPointMock.Object, id, createdTime); + + // Act + NotificationOrderRequestResponse actual = await service.RegisterNotificationOrder(input); + + // Assert + Assert.Equal(RecipientLookupStatus.PartialSuccess, actual.RecipientLookup?.Status); + Assert.Single(actual.RecipientLookup!.IsReserved!); + Assert.Single(actual.RecipientLookup!.MissingContact!); + repoMock.VerifyAll(); + contactPointMock.VerifyAll(); + } + + [Fact] + public async Task RegisterNotificationOrder_ForSmsPreferred_LookupSuccess_OrderCreated() + { + // Arrange + DateTime sendTime = DateTime.UtcNow; + DateTime createdTime = DateTime.UtcNow.AddMinutes(-2); + Guid id = Guid.NewGuid(); + + NotificationOrderRequest input = new() + { + Creator = new Creator("ttd"), + + NotificationChannel = NotificationChannel.SmsPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" } + ], + RequestedSendTime = sendTime, + Templates = [ + new SmsTemplate { Body = "sms-body" }, + new EmailTemplate { Body = "email-body", FromAddress = "noreply@altinn.no" }] + }; + + Mock repoMock = new(); + repoMock + .Setup(r => r.Create(It.IsAny())) + .ReturnsAsync((NotificationOrder order) => order); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(input.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + } + }); + + var service = GetTestService(repoMock.Object, contactPointMock.Object, id, createdTime); + + // Act + NotificationOrderRequestResponse actual = await service.RegisterNotificationOrder(input); + + // Assert + Assert.Equal(RecipientLookupStatus.Success, actual.RecipientLookup?.Status); + Assert.Equal(0, actual.RecipientLookup!.IsReserved?.Count); + Assert.Equal(0, actual.RecipientLookup!.MissingContact?.Count); + repoMock.VerifyAll(); + contactPointMock.VerifyAll(); + } + public static OrderRequestService GetTestService(IOrderRepository? repository = null, IContactPointService? contactPointService = null, Guid? guid = null, DateTime? dateTime = null) { if (repository == null) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/PreferredChannelProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/PreferredChannelProcessingServiceTests.cs new file mode 100644 index 00000000..f221d6c9 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/PreferredChannelProcessingServiceTests.cs @@ -0,0 +1,299 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices +{ + public class PreferredChannelProcessingServiceTests + { + /// + /// Scenario: 1 recipient with email, 1 recipient with mobile, 1 reserved alid recipient, 1 recipient without contact point + /// Expected; Email service is called with three recipients, SMS service is called with one recipient + /// + /// + /// Assertion logic is mostly within the setup of the mocked functions in the processing services + /// + [Fact] + public async Task ProcessOrder_EmailPreferredChannel_WithoutRetry() + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = NotificationChannel.EmailPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + new Recipient() { NationalIdentityNumber = "4" }] + }; + + Mock emailProcessingMock = new(); + emailProcessingMock + .Setup(x => x.ProcessOrderWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 3 && !l.Exists(r => r.NationalIdentityNumber == "1")))); + + Mock smsProcessingMock = new(); + smsProcessingMock + .Setup(x => x.ProcessOrderWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 1 && l.Exists(r => r.NationalIdentityNumber == "1")))); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(order.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "3") + { + recipient.IsReserved = true; + } + else if (recipient.NationalIdentityNumber == "4") + { + recipient.IsReserved = false; + } + } + }); + + var preferredChannelProcessingService = new PreferredChannelProcessingService( + emailProcessingMock.Object, + smsProcessingMock.Object, + contactPointMock.Object); + + // Act + await preferredChannelProcessingService.ProcessOrder(order); + + // Assert + emailProcessingMock.VerifyAll(); + smsProcessingMock.VerifyAll(); + } + + /// + /// Scenario: 1 recipient with email, 1 recipient with mobile, 1 reserved alid recipient, 1 recipient without contact point + /// Expected; Email service is called with three recipients, SMS service is called with one recipient + /// + /// + /// Assertion logic is mostly within the setup of the mocked functions in the processing services + /// + [Fact] + public async Task ProcessOrder_EmailPreferredChannel_WithRetry() + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = NotificationChannel.EmailPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + new Recipient() { NationalIdentityNumber = "4" }] + }; + + Mock emailProcessingMock = new(); + emailProcessingMock + .Setup(x => x.ProcessOrderRetryWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 3 && !l.Exists(r => r.NationalIdentityNumber == "1")))); + + Mock smsProcessingMock = new(); + smsProcessingMock + .Setup(x => x.ProcessOrderRetryWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 1 && l.Exists(r => r.NationalIdentityNumber == "1")))); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(order.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "3") + { + recipient.IsReserved = true; + } + else if (recipient.NationalIdentityNumber == "4") + { + recipient.IsReserved = false; + } + } + }); + + var preferredChannelProcessingService = new PreferredChannelProcessingService( + emailProcessingMock.Object, + smsProcessingMock.Object, + contactPointMock.Object); + + // Act + await preferredChannelProcessingService.ProcessOrderRetry(order); + + // Assert + emailProcessingMock.VerifyAll(); + smsProcessingMock.VerifyAll(); + } + + /// + /// Scenario: 1 recipient with email, 1 recipient with mobile, 1 reserved alid recipient, 1 recipient without contact point + /// Expected; SMS service is called with three recipients, email service is called with one recipient + /// + /// + /// Assertion logic is mostly within the setup of the mocked functions in the processing services + /// + [Fact] + public async Task ProcessOrder_SmsPreferredChannel_WithoutRetry() + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = NotificationChannel.SmsPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + new Recipient() { NationalIdentityNumber = "4" }] + }; + + Mock emailProcessingMock = new(); + emailProcessingMock + .Setup(x => x.ProcessOrderWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 1 && l.Exists(r => r.NationalIdentityNumber == "2")))); + + Mock smsProcessingMock = new(); + smsProcessingMock + .Setup(x => x.ProcessOrderWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 3 && !l.Exists(r => r.NationalIdentityNumber == "2")))); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(order.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "3") + { + recipient.IsReserved = true; + } + else if (recipient.NationalIdentityNumber == "4") + { + recipient.IsReserved = false; + } + } + }); + + var preferredChannelProcessingService = new PreferredChannelProcessingService( + emailProcessingMock.Object, + smsProcessingMock.Object, + contactPointMock.Object); + + // Act + await preferredChannelProcessingService.ProcessOrder(order); + + // Assert + emailProcessingMock.VerifyAll(); + smsProcessingMock.VerifyAll(); + } + + /// + /// Scenario: 1 recipient with email, 1 recipient with mobile, 1 reserved alid recipient, 1 recipient without contact point + /// Expected; SMS service is called with three recipients, email service is called with one recipient + /// + /// + /// Assertion logic is mostly within the setup of the mocked functions in the processing services + /// + [Fact] + public async Task ProcessOrder_SmsPreferredChannel_WithRetry() + { + // Arrange + NotificationOrder order = new() + { + NotificationChannel = NotificationChannel.SmsPreferred, + Recipients = [ + new Recipient() { NationalIdentityNumber = "1" }, + new Recipient() { NationalIdentityNumber = "2" }, + new Recipient() { NationalIdentityNumber = "3" }, + new Recipient() { NationalIdentityNumber = "4" }] + }; + + Mock emailProcessingMock = new(); + emailProcessingMock + .Setup(x => x.ProcessOrderRetryWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 1 && l.Exists(r => r.NationalIdentityNumber == "2")))); + + Mock smsProcessingMock = new(); + smsProcessingMock + .Setup(x => x.ProcessOrderRetryWithoutAddressLookup(It.IsAny(), It.Is>(l => l.Count == 3 && !l.Exists(r => r.NationalIdentityNumber == "2")))); + + Mock contactPointMock = new(); + contactPointMock + .Setup(cp => cp.AddPreferredContactPoints(order.NotificationChannel, It.IsAny>(), It.IsAny())) + .Callback, string?>((_, recipients, _) => + { + foreach (var recipient in recipients) + { + if (recipient.NationalIdentityNumber == "1") + { + recipient.AddressInfo.Add(new SmsAddressPoint("+4799999999")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "2") + { + recipient.AddressInfo.Add(new EmailAddressPoint("2@user.com")); + recipient.IsReserved = false; + } + else if (recipient.NationalIdentityNumber == "3") + { + recipient.IsReserved = true; + } + else if (recipient.NationalIdentityNumber == "4") + { + recipient.IsReserved = false; + } + } + }); + + var preferredChannelProcessingService = new PreferredChannelProcessingService( + emailProcessingMock.Object, + smsProcessingMock.Object, + contactPointMock.Object); + + // Act + await preferredChannelProcessingService.ProcessOrderRetry(order); + + // Assert + emailProcessingMock.VerifyAll(); + smsProcessingMock.VerifyAll(); + } + } +}