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

#328 Implementation of API logic for placing a SMS notifications order #384

Merged
merged 44 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d9b6cbc
RecipientExt Phone number property added
khanrn Jan 22, 2024
c20ccd4
SMS notification order request data model added
khanrn Jan 22, 2024
4294d51
SMS notification orders controller added
khanrn Jan 22, 2024
0f53787
SMS phone number validation initiated
khanrn Jan 22, 2024
12ac5a0
Phone number validation added to singleton
khanrn Jan 22, 2024
722e39f
Changed PhoneNumber to MobileNumber
khanrn Jan 22, 2024
c41041f
With rebase phone changed to mobile
khanrn Jan 22, 2024
fa6d76d
Change of phone to mobile
khanrn Jan 22, 2024
927b8e0
Attribute for SMS address point added
khanrn Jan 23, 2024
f4c4e63
Mobile number validation rules regex fix
khanrn Jan 23, 2024
c7114b1
Validator test initiated for SMS notifiaction order request
khanrn Jan 23, 2024
4eba881
Validatee recipient provided for SMS return true added
khanrn Jan 23, 2024
eaa40fb
Validatee recipient provided for SMS return false added
khanrn Jan 23, 2024
2e6fc83
Validate SMS recipient not defined return false added
khanrn Jan 24, 2024
5c8c7f2
Validate send time has local timezone return true added
khanrn Jan 24, 2024
1934ae1
Validate send time has UTC Now timezone return true added
khanrn Jan 24, 2024
c54743a
Validate send time has unspecified timezone return false added
khanrn Jan 24, 2024
d04e985
Validate Send Numbwer missing returns false
khanrn Jan 24, 2024
3876f2e
Type added to the validationResult variable
khanrn Jan 24, 2024
65f2aa9
Test for valid SMS number added
khanrn Jan 24, 2024
881b85d
Explicit type added to the variable
khanrn Jan 24, 2024
e43fff5
Explicit type added to the variable
khanrn Jan 24, 2024
ae7653f
Type definition for recipients variable fixed
khanrn Jan 24, 2024
2638f57
Sender Number removed from must rule
khanrn Jan 24, 2024
13c0450
Order mapper helper function added for getting mobile number
khanrn Jan 24, 2024
f443503
Norwegian phone number strict validation added
khanrn Jan 26, 2024
04818b2
Tests adapted for Norwegian phone numbet validation
khanrn Jan 26, 2024
e421edc
Test refactored for readability purpose
khanrn Jan 26, 2024
11b9e58
Typo fixed
khanrn Jan 26, 2024
b2ef892
Both MapToRecipientExt tests merged into one
khanrn Jan 26, 2024
e35fb59
SMS order mapper moved to OrderMapper class
khanrn Jan 26, 2024
0abff14
SMS order mapper tests added
khanrn Jan 26, 2024
48ba971
Fixed merge conflicts with main
khanrn Jan 28, 2024
3ec6bc6
Integration tests added for the SMS notificaitons API
khanrn Jan 28, 2024
711d047
Sender number changed to Altinn for integration tests
khanrn Jan 28, 2024
3e3f03c
PostTests API base path fixed for integration tests
khanrn Jan 28, 2024
64ccdb7
Revert:Removed the k6 tests added with integration tests
khanrn Jan 29, 2024
0ad1192
Ternary to if-else to remove code smell
khanrn Jan 29, 2024
6932f97
Removed 5 digit pass as LinkMobility need 9 digits
khanrn Jan 29, 2024
87f1409
Validation logic changed for SMS mobile numbers
khanrn Jan 29, 2024
b66aded
RegEx removed from validation logic
khanrn Jan 30, 2024
8812141
Invalid mobile number error message updated
khanrn Jan 30, 2024
9ad1343
Arguments fixed for the tests
khanrn Jan 30, 2024
143fd69
Tests messgae update
khanrn Jan 30, 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 @@ -8,6 +8,7 @@ namespace Altinn.Notifications.Core.Models.Address;
/// Interface describing an address point
/// </summary>
[JsonDerivedType(typeof(EmailAddressPoint), "email")]
[JsonDerivedType(typeof(SmsAddressPoint), "sms")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$")]
public interface IAddressPoint
{
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.Notifications/Altinn.Notifications.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Altinn.Common.PEP" Version="1.3.0" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="JWTCookieAuthentication" Version="4.0.1" />
<PackageReference Include="libphonenumber-csharp" Version="8.13.28" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections;
using Altinn.Notifications.Configuration;
using Altinn.Notifications.Core.Models;
using Altinn.Notifications.Core.Models.Orders;
using Altinn.Notifications.Core.Services.Interfaces;
using Altinn.Notifications.Extensions;
using Altinn.Notifications.Mappers;
using Altinn.Notifications.Models;
using Altinn.Notifications.Validators;

using FluentValidation;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.Filters;

namespace Altinn.Notifications.Controllers;

/// <summary>
/// Controller for all operations related to SMS notification orders
/// </summary>
[Route("notifications/api/v1/orders/sms")]
[ApiController]
[Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)]
[SwaggerResponse(401, "Caller is unauthorized")]
[SwaggerResponse(403, "Caller is not authorized to access the requested resource")]

public class SmsNotificationOrdersController : ControllerBase
{
private readonly IValidator<SmsNotificationOrderRequestExt> _validator;
private readonly IOrderRequestService _orderRequestService;

/// <summary>
/// Initializes a new instance of the <see cref="SmsNotificationOrdersController"/> class.
/// </summary>
public SmsNotificationOrdersController(IValidator<SmsNotificationOrderRequestExt> validator, IOrderRequestService orderRequestService)
{
_validator = validator;
_orderRequestService = orderRequestService;
}

/// <summary>
/// Add an SMS notification order.
/// </summary>
/// <remarks>
/// The API will accept the request after som basic validation of the request.
/// The system will also attempt to verify that it will be possible to fulfill the order.
/// </remarks>
/// <returns>The id of the registered notification order</returns>
[HttpPost]
[Consumes("application/json")]
[Produces("application/json")]
[SwaggerResponse(202, "The notification order was accepted", typeof(OrderIdExt))]
[SwaggerResponse(400, "The notification order is invalid", typeof(ValidationProblemDetails))]
[SwaggerResponseHeader(202, "Location", "string", "Link to access the newly created notification order.")]
public async Task<ActionResult<OrderIdExt>> Post(SmsNotificationOrderRequestExt smsNotificationOrderRequest)
{
FluentValidation.Results.ValidationResult validationResult = _validator.Validate(smsNotificationOrderRequest);
if (!validationResult.IsValid)
{
validationResult.AddToModelState(this.ModelState);
return ValidationProblem(ModelState);
}

string? creator = HttpContext.GetOrg();

if (creator == null)
{
return Forbid();
}

NotificationOrderRequest orderRequest = smsNotificationOrderRequest.MapToOrderRequest(creator);
(NotificationOrder? registeredOrder, ServiceError? error) = await _orderRequestService.RegisterNotificationOrder(orderRequest);

if (error != null)
{
return StatusCode(error.ErrorCode, error.ErrorMessage);
}

string selfLink = registeredOrder!.GetSelfLink();
return Accepted(selfLink, new OrderIdExt(registeredOrder!.Id));
}
}
33 changes: 32 additions & 1 deletion src/Altinn.Notifications/Mappers/OrderMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@ public static NotificationOrderRequest MapToOrderRequest(this EmailNotificationO
recipients);
}

/// <summary>
/// Maps a <see cref="SmsNotificationOrderRequestExt"/> to a <see cref="NotificationOrderRequest"/>
/// </summary>
public static NotificationOrderRequest MapToOrderRequest(this SmsNotificationOrderRequestExt extRequest, string creator)
{
INotificationTemplate smsTemplate = new SmsTemplate(extRequest.SenderNumber, extRequest.Body);

List<Recipient> recipients = new();

recipients.AddRange(
extRequest.Recipients.Select(r => new Recipient(string.Empty, new List<IAddressPoint>() { new SmsAddressPoint(r.MobileNumber!) })));

return new NotificationOrderRequest(
extRequest.SendersReference,
creator,
new List<INotificationTemplate>() { smsTemplate },
extRequest.RequestedSendTime.ToUniversalTime(),
NotificationChannel.Sms,
recipients);
}

/// <summary>
/// Maps a <see cref="NotificationOrder"/> to a <see cref="NotificationOrderExt"/>
/// </summary>
Expand Down Expand Up @@ -138,7 +159,8 @@ internal static List<RecipientExt> MapToRecipientExt(this List<Recipient> recipi
recipientExt.AddRange(
recipients.Select(r => new RecipientExt
{
EmailAddress = GetEmailFromAddressList(r.AddressInfo)
EmailAddress = GetEmailFromAddressList(r.AddressInfo),
MobileNumber = GetMobileNumberFromAddressList(r.AddressInfo)
}));

return recipientExt;
Expand All @@ -164,4 +186,13 @@ private static IBaseNotificationOrderExt MapBaseNotificationOrder(this IBaseNoti

return emailAddressPoint?.EmailAddress;
}

private static string? GetMobileNumberFromAddressList(List<IAddressPoint> addressPoints)
{
var smsAddressPoint = addressPoints
.Find(a => a.AddressType.Equals(AddressType.Sms))
as SmsAddressPoint;

return smsAddressPoint?.MobileNumber;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Altinn.Notifications.Models;
/// Class representing an email notiication order request
/// </summary>
/// <remarks>
/// External representaion to be used in the API.
/// External representation to be used in the API.
/// </remarks>
public class EmailNotificationOrderRequestExt
{
Expand Down
6 changes: 6 additions & 0 deletions src/Altinn.Notifications/Models/RecipientExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ public class RecipientExt
/// </summary>
[JsonPropertyName("emailAddress")]
public string? EmailAddress { get; set; }

/// <summary>
/// Gets or sets the mobile number of the recipient
/// </summary>
[JsonPropertyName("mobileNumber")]
public string? MobileNumber { get; set; }
}
51 changes: 51 additions & 0 deletions src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Altinn.Notifications.Models;

/// <summary>
/// Class representing an SMS notiication order request
/// </summary>
/// <remarks>
/// External representation to be used in the API.
/// </remarks>
public class SmsNotificationOrderRequestExt
{
/// <summary>
/// Gets or sets the sender number of the SMS
/// </summary>
[JsonPropertyName("senderNumber")]
public string SenderNumber { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the body of the SMS
/// </summary>
[JsonPropertyName("body")]
public string Body { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the send time of the SMS. Defaults to UtcNow.
/// </summary>
[JsonPropertyName("requestedSendTime")]
public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow;

/// <summary>
/// Gets or sets the senders reference on the notification
/// </summary>
[JsonPropertyName("sendersReference")]
public string? SendersReference { get; set; }

/// <summary>
/// Gets or sets the list of recipients
/// </summary>
[JsonPropertyName("recipients")]
public List<RecipientExt> Recipients { get; set; } = new List<RecipientExt>();

/// <summary>
/// Json serialized the <see cref="SmsNotificationOrderRequestExt"/>
/// </summary>
public string Serialize()
{
return JsonSerializer.Serialize(this);
}
}
1 change: 1 addition & 0 deletions src/Altinn.Notifications/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable disable

Check warning on line 1 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Refactor this top-level file to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 1 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Refactor this top-level file to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -140,7 +140,7 @@
services.AddSingleton(config);
if (!string.IsNullOrEmpty(applicationInsightsConnectionString))
{
services.AddSingleton(typeof(ITelemetryChannel), new ServerTelemetryChannel() { StorageFolder = "/tmp/logtelemetry" });

Check warning on line 143 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure publicly writable directories are used safely here. (https://rules.sonarsource.com/csharp/RSPEC-5443)

Check warning on line 143 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure publicly writable directories are used safely here. (https://rules.sonarsource.com/csharp/RSPEC-5443)

services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions
{
Expand Down Expand Up @@ -247,6 +247,7 @@
{
ValidatorOptions.Global.LanguageManager.Enabled = false;
services.AddSingleton<IValidator<EmailNotificationOrderRequestExt>, EmailNotificationOrderRequestValidator>();
services.AddSingleton<IValidator<SmsNotificationOrderRequestExt>, SmsNotificationOrderRequestValidator>();
}

void IncludeXmlComments(SwaggerGenOptions swaggerGenOptions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Text.RegularExpressions;

using Altinn.Notifications.Models;

using FluentValidation;
using PhoneNumbers;

namespace Altinn.Notifications.Validators;

/// <summary>
/// Class containing validation logic for the <see cref="SmsNotificationOrderRequestExt"/> model
/// </summary>
public class SmsNotificationOrderRequestValidator : AbstractValidator<SmsNotificationOrderRequestExt>
{
/// <summary>
/// Initializes a new instance of the <see cref="SmsNotificationOrderRequestValidator"/> class.
/// </summary>
public SmsNotificationOrderRequestValidator()
{
RuleFor(order => order.Recipients)
.NotEmpty()
.WithMessage("One or more recipient is required.")
.Must(recipients => recipients.TrueForAll(a => IsValidMobileNumber(a.MobileNumber)))
.WithMessage("A valid mobile number must be provided for all recipients.");
khanrn marked this conversation as resolved.
Show resolved Hide resolved

RuleFor(order => order.RequestedSendTime)
.Must(sendTime => sendTime.Kind != DateTimeKind.Unspecified)
.WithMessage("The requested send time value must have specified a time zone.")
.Must(sendTime => sendTime >= DateTime.UtcNow.AddMinutes(-5))
.WithMessage("Send time must be in the future. Leave blank to send immediately.");

RuleFor(order => order.Body).NotEmpty();
}

/// <summary>
/// Validated as mobile number based on the Altinn 2 regex
/// </summary>
/// <param name="mobileNumber">The string to validate as an mobile number</param>
/// <returns>A boolean indicating that the mobile number is valid or not</returns>
internal static bool IsValidMobileNumber(string? mobileNumber)
{
khanrn marked this conversation as resolved.
Show resolved Hide resolved
if (string.IsNullOrEmpty(mobileNumber) || (!mobileNumber.StartsWith('+') && !mobileNumber.StartsWith("00")))
{
return false;
}

if (mobileNumber.StartsWith("00"))
{
mobileNumber = "+" + mobileNumber.Remove(0, 2);
}

string mobileNumberRegexPattern = @"^(?:(\+47[49]\d{7})|(0047[49]\d{7})|(?!((\+47[0-9]*)|(0047[0-9]*)))(([0-9]{8})|(00[0-9]{3,})|(\+[0-9]{3,})))$";

Regex regex = new(mobileNumberRegexPattern, RegexOptions.None, TimeSpan.FromSeconds(1));

Match match = regex.Match(mobileNumber);

if (!match.Success)
{
return false;
}

PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.GetInstance();
PhoneNumber phoneNumber = phoneNumberUtil.Parse(mobileNumber, null);
return phoneNumberUtil.IsValidNumber(phoneNumber);
khanrn marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading
Loading