Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Business logic for retrieving recipient contact points from national identity number #464

Merged
merged 24 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eebd424
Added profile client for retrieving contact points
acn-sbuad Mar 11, 2024
9d7599d
Added missing endpoint
acn-sbuad Mar 11, 2024
4301d78
implemented tests - not run -kafka not working
acn-sbuad Mar 11, 2024
2120b39
added unit tests
acn-sbuad Mar 12, 2024
3d204ce
added missing changes in profile client
acn-sbuad Mar 12, 2024
2c755b3
fixed code smells
acn-sbuad Mar 12, 2024
b7c3aff
undid changes in int tests
acn-sbuad Mar 12, 2024
b322ab8
fixed options for settings
acn-sbuad Mar 12, 2024
0e8c8db
added unit test for exception
acn-sbuad Mar 12, 2024
fe6e485
core logic for generating notification from nin
acn-sbuad Mar 12, 2024
5414cd8
merged main
acn-sbuad Mar 13, 2024
6bfbcc4
removed obsolete code
acn-sbuad Mar 13, 2024
ee8cf43
fixed code smell
acn-sbuad Mar 13, 2024
74b7108
added order processing unit tests
acn-sbuad Mar 13, 2024
dee5144
added unit test for notification services
acn-sbuad Mar 13, 2024
7c99cd6
added contactpoint service tests
acn-sbuad Mar 13, 2024
696b104
added availability logic
acn-sbuad Mar 13, 2024
8ed8350
changed syntax for adding to list
acn-sbuad Mar 13, 2024
a55b599
removed unused reference
acn-sbuad Mar 14, 2024
7cf84cd
added implementation for order processing retry
acn-sbuad Mar 14, 2024
b5053a6
Update src/Altinn.Notifications.Core/Services/ContactPointService.cs
acn-sbuad Mar 18, 2024
c60e19b
Ensuring reference type is only updated with contact point once
acn-sbuad Mar 18, 2024
98891dc
renamed tests
acn-sbuad Mar 18, 2024
ba81579
Merge branch 'main' into feature/437-core-logic
acn-sbuad Mar 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public enum EmailNotificationResultType
/// </summary>
Failed,

/// <summary>
/// Failed, recipient is reserved in KRR
/// </summary>
Failed_RecipientReserved,

/// <summary>
/// Recipient to address was not identified
/// </summary>
Expand Down
15 changes: 10 additions & 5 deletions src/Altinn.Notifications.Core/Enums/SmsNotificationResultType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ public enum SmsNotificationResultType
/// </summary>
Failed,

/// <summary>
/// Sms notification send operation failed due to invalid recipient
/// </summary>
Failed_InvalidRecipient,

/// <summary>
/// Failed, recipient is reserved in KRR
/// </summary>
Failed_RecipientReserved,

/// <summary>
/// Sms notification send operation failed because the receiver number is barred/blocked/not in use.
/// </summary>
Expand All @@ -45,11 +55,6 @@ public enum SmsNotificationResultType
/// </summary>
Failed_Expired,

/// <summary>
/// Sms notification send operation failed due to invalid recipient
/// </summary>
Failed_InvalidRecipient,

/// <summary>
/// Sms notification send operation failed due to the SMS being undeliverable.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static void AddCoreServices(this IServiceCollection services, IConfigurat
.AddSingleton<IEmailNotificationService, EmailNotificationService>()
.AddSingleton<ISmsNotificationService, SmsNotificationService>()
.AddSingleton<ISmsNotificationSummaryService, SmsNotificationSummaryService>()
.AddSingleton<IContactPointService, ContactPointService>()
.AddSingleton<IAltinnServiceUpdateService, AltinnServiceUpdateService>()
.AddSingleton<INotificationsEmailServiceUpdateService, NotificationsEmailServiceUpdateService>()
.AddSingleton<IMetricsService, MetricsService>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@
/// </summary>
public List<Recipient> Recipients { get; internal set; } = new List<Recipient>();

/// <summary>
/// Gets the boolean indicating if the KRR reservation should be ignored
/// </summary>
public bool IgnoreReservation { get; internal set; }

/// <summary>
/// Initializes a new instance of the <see cref="NotificationOrder"/> class.
/// </summary>
public NotificationOrder(Guid id, string? sendersReference, List<INotificationTemplate> templates, DateTime requestedSendTime, NotificationChannel notificationChannel, Creator creator, DateTime created, List<Recipient> recipients)
public NotificationOrder(Guid id, string? sendersReference, List<INotificationTemplate> templates, DateTime requestedSendTime, NotificationChannel notificationChannel, Creator creator, DateTime created, List<Recipient> recipients, bool ignoreReservation = false)

Check warning on line 49 in src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 49 in src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 49 in src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
{
Id = id;
SendersReference = sendersReference;
Expand All @@ -51,6 +56,7 @@
Creator = creator;
Created = created;
Recipients = recipients;
IgnoreReservation = ignoreReservation;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,23 @@ public class NotificationOrderRequest
/// </summary>
public Creator Creator { get; internal set; }

/// <summary>
/// Gets the boolean indicating if the KRR reservation should be ignored
/// </summary>
public bool IgnoreReservation { get; internal set; }

/// <summary>
/// Initializes a new instance of the <see cref="NotificationOrderRequest"/> class.
/// </summary>
public NotificationOrderRequest(string? sendersReference, string creatorShortName, List<INotificationTemplate> templates, DateTime requestedSendTime, NotificationChannel notificationChannel, List<Recipient> recipients)
public NotificationOrderRequest(string? sendersReference, string creatorShortName, List<INotificationTemplate> templates, DateTime requestedSendTime, NotificationChannel notificationChannel, List<Recipient> recipients, bool ignoreReservation = false)
{
SendersReference = sendersReference;
Creator = new(creatorShortName);
Templates = templates;
RequestedSendTime = requestedSendTime;
NotificationChannel = notificationChannel;
Recipients = recipients;
IgnoreReservation = ignoreReservation;
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Altinn.Notifications.Core/Models/Recipient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public class Recipient
/// </summary>
public string? NationalIdentityNumber { get; set; } = null;

/// <summary>
/// Gets or sets a value indicating whether the recipient is reserved from digital communication
/// </summary>
public bool IsReserved { get; set; }

/// <summary>
/// Gets a list of address points for the recipient
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public class EmailRecipient
/// Gets or sets the toaddress
/// </summary>
public string ToAddress { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether the recipient is reserved from digital communication
/// </summary>
public bool IsReserved { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public class SmsRecipient
/// Gets or sets the mobile number
/// </summary>
public string MobileNumber { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether the recipient is reserved from digital communication
/// </summary>
public bool IsReserved { get; set; }
}
116 changes: 116 additions & 0 deletions src/Altinn.Notifications.Core/Services/ContactPointService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Altinn.Notifications.Core.Integrations;
using Altinn.Notifications.Core.Models;
using Altinn.Notifications.Core.Models.Address;
using Altinn.Notifications.Core.Models.ContactPoints;
using Altinn.Notifications.Core.Services.Interfaces;

namespace Altinn.Notifications.Core.Services
{
/// <summary>
/// Implementation of the <see cref="IContactPointService"/> using Altinn platform services to lookup contact points
/// </summary>
public class ContactPointService : IContactPointService
{
private readonly IProfileClient _profileClient;

/// <summary>
/// Initializes a new instance of the <see cref="ContactPointService"/> class.
/// </summary>
public ContactPointService(IProfileClient profile)
{
_profileClient = profile;
}

/// <inheritdoc/>
public async Task AddEmailContactPoints(List<Recipient> recipients)
{
await AugmentRecipients(
recipients,
(recipient, userContactPoints) =>
{
recipient.AddressInfo.Add(new EmailAddressPoint(userContactPoints.Email));
return recipient;
});
}

/// <inheritdoc/>
public async Task AddSmsContactPoints(List<Recipient> recipients)
{
await AugmentRecipients(
recipients,
(recipient, userContactPoints) =>
{
recipient.AddressInfo.Add(new SmsAddressPoint(userContactPoints.MobileNumber));
return recipient;
});
}

/// <inheritdoc/>
public async Task<List<UserContactPointAvailability>> GetContactPointAvailability(List<Recipient> recipients)
{
return await LookupContactPointAvailability(recipients);
}

private async Task<List<Recipient>> AugmentRecipients(List<Recipient> recipients, Func<Recipient, UserContactPoints, Recipient> createContactPoint)
{
List<Recipient> augmentedRecipients = [];

List<UserContactPoints> userContactPointsList = await LookupContactPoints(recipients);
foreach (Recipient recipient in recipients)
{
if (!string.IsNullOrEmpty(recipient.NationalIdentityNumber))
{
UserContactPoints? userContactPoints = userContactPointsList!
.Find(u => u.NationalIdentityNumber == recipient.NationalIdentityNumber);

if (userContactPoints != null)
{
recipient.IsReserved = userContactPoints.IsReserved;
augmentedRecipients.Add(createContactPoint(recipient, userContactPoints));
}
}
}

return augmentedRecipients;
}

private async Task<List<UserContactPoints>> LookupContactPoints(List<Recipient> recipients)
{
List<UserContactPoints> userContactPoints = new();
List<string> nins = recipients
.Where(r => !string.IsNullOrEmpty(r.NationalIdentityNumber))
.Select(r => r.NationalIdentityNumber!)
.ToList();

Task<List<UserContactPoints>> ninLookupTask = nins.Count > 0
? _profileClient.GetUserContactPoints(nins)
: Task.FromResult(new List<UserContactPoints>());

await Task.WhenAll(ninLookupTask);

userContactPoints.AddRange(ninLookupTask.Result);

return userContactPoints;
}

private async Task<List<UserContactPointAvailability>> LookupContactPointAvailability(List<Recipient> recipients)
{
List<UserContactPointAvailability> contactPointAvailabilityList = new();

List<string> nins = recipients
.Where(r => !string.IsNullOrEmpty(r.NationalIdentityNumber))
.Select(r => r.NationalIdentityNumber!)
.ToList();

Task<List<UserContactPointAvailability>> ninLookupTask = nins.Count > 0
? _profileClient.GetUserContactPointAvailabilities(nins)
: Task.FromResult(new List<UserContactPointAvailability>());

await Task.WhenAll(ninLookupTask);

contactPointAvailabilityList.AddRange(ninLookupTask.Result);

return contactPointAvailabilityList;
}
}
}
16 changes: 11 additions & 5 deletions src/Altinn.Notifications.Core/Services/EmailNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,31 @@ public EmailNotificationService(
}

/// <inheritdoc/>
public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient)
public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false)
{
EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint;

EmailRecipient emailRecipient = new()
{
OrganisationNumber = recipient.OrganisationNumber,
NationalIdentityNumber = recipient.NationalIdentityNumber,
ToAddress = addressPoint?.EmailAddress ?? string.Empty
ToAddress = addressPoint?.EmailAddress ?? string.Empty,
IsReserved = recipient.IsReserved
};

if (!string.IsNullOrEmpty(addressPoint?.EmailAddress))
if (recipient.IsReserved && !ignoreReservation)
{
await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.New);
emailRecipient.ToAddress = string.Empty; // not persisting email address for reserved recipients
await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.Failed_RecipientReserved);
return;
}
else
else if (string.IsNullOrEmpty(addressPoint?.EmailAddress))
{
await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.Failed_RecipientNotIdentified);
return;
}

await CreateNotificationForRecipient(orderId, requestedSendTime, emailRecipient, EmailNotificationResultType.New);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class EmailNotificationSummaryService : IEmailNotificationSummaryService
{ EmailNotificationResultType.Succeeded, "The email has been accepted by the third party email service and will be sent shortly." },
{ EmailNotificationResultType.Delivered, "The email was delivered to the recipient. No errors reported, making it likely it was received by the recipient." },
{ EmailNotificationResultType.Failed, "The email was not sent due to an unspecified failure." },
{ EmailNotificationResultType.Failed_RecipientReserved, "The email was not sent because the recipient has reserved themselves from electronic communication." },
{ EmailNotificationResultType.Failed_RecipientNotIdentified, "The email was not sent because the recipient's email address was not found." },
{ EmailNotificationResultType.Failed_InvalidEmailFormat, "The email was not sent because the recipient’s email address is in an invalid format." },
{ EmailNotificationResultType.Failed_SupressedRecipient, "The email was not sent because the recipient’s email address is suppressed by the third party email service." },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,45 @@ public class EmailOrderProcessingService : IEmailOrderProcessingService
{
private readonly IEmailNotificationRepository _emailNotificationRepository;
private readonly IEmailNotificationService _emailService;
private readonly IContactPointService _contactPointService;

/// <summary>
/// Initializes a new instance of the <see cref="OrderProcessingService"/> class.
/// </summary>
public EmailOrderProcessingService(
IEmailNotificationRepository emailNotificationRepository,
IEmailNotificationService emailService)
IEmailNotificationService emailService,
IContactPointService contactPointService)
{
_emailNotificationRepository = emailNotificationRepository;
_emailService = emailService;
_contactPointService = contactPointService;
}

/// <inheritdoc/>
public async Task ProcessOrder(NotificationOrder order)
{
foreach (Recipient recipient in order.Recipients)
var recipients = order.Recipients;
var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList();

await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail);

foreach (Recipient recipient in recipients)
{
await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient);
await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation);
}
}

/// <inheritdoc/>
public async Task ProcessOrderRetry(NotificationOrder order)
{
var recipients = order.Recipients;
var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList();

await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail);

List<EmailRecipient> emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id);

foreach (Recipient recipient in order.Recipients)
{
EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint;
Expand All @@ -49,7 +63,7 @@ public async Task ProcessOrderRetry(NotificationOrder order)
&& er.OrganisationNumber == recipient.OrganisationNumber
&& er.ToAddress == addressPoint?.EmailAddress))
{
await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient);
await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient, order.IgnoreReservation);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Altinn.Notifications.Core.Models;
using Altinn.Notifications.Core.Models.ContactPoints;

namespace Altinn.Notifications.Core.Services.Interfaces
{
/// <summary>
/// Service for retrieving contact points for recipients
/// </summary>
public interface IContactPointService
{
/// <summary>
/// Looks up and adds the email contact points for recipients based on their national identity number or organisation number
/// </summary>
/// <param name="recipients">List of recipients to retrieve contact points for</param>
/// <returns>The list of recipients augumented with email address points where available</returns>
/// <remarks>Implementation alters the recipient reference object directly</remarks>
public Task AddEmailContactPoints(List<Recipient> recipients);

/// <summary>
/// Looks up and adds the SMS contact points for recipients based on their national identity number or organisation number
/// </summary>
/// <param name="recipients">List of recipients to retrieve contact points for</param>
/// <returns>The list of recipients augumented with SMS address points where available</returns>
/// <remarks>Implementation alters the recipient reference object directly</remarks>
public Task AddSmsContactPoints(List<Recipient> recipients);

/// <summary>
/// Retrieves the availabililty of contact points for the provided recipient based on their national identity number or organisation number
/// </summary>
/// <param name="recipients">List of recipients to check contact point availability for</param>
/// <returns>The list of recipients with contact point availability details</returns>
public Task<List<UserContactPointAvailability>> GetContactPointAvailability(List<Recipient> recipients);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public interface IEmailNotificationService
/// <summary>
/// Creates a new email notification based on the provided orderId and recipient
/// </summary>
public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient);
public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient, bool ignoreReservation = false);

/// <summary>
/// Starts the process of sending all ready email notifications
Expand Down
Loading
Loading