Skip to content

Commit

Permalink
#328 Implementation of API logic for placing a SMS notifications order (
Browse files Browse the repository at this point in the history
#384)

* RecipientExt Phone number property added

* SMS notification order request data model added

* SMS notification orders controller added

* SMS phone number validation initiated

* Phone number validation added to singleton

* Changed PhoneNumber to MobileNumber

* With rebase phone changed to mobile

* Change of phone to mobile

* Attribute for SMS address point added

* Mobile number validation rules regex fix

* Validator test initiated for SMS notifiaction order request

* Validatee recipient provided for SMS return true added

* Validatee recipient provided for SMS return false added

* Validate SMS recipient not defined return false added

* Validate send time has local timezone return true added

* Validate send time has UTC Now timezone return true added

* Validate send time has unspecified timezone return false added

* Validate Send Numbwer missing returns false

* Type added to the validationResult variable

* Test for valid SMS number added

* Explicit type added to the variable

* Explicit type added to the variable

* Type definition for recipients variable fixed

* Sender Number removed from must rule

* Order mapper helper function added for getting mobile number

* Norwegian phone number strict validation added

* Tests adapted for Norwegian phone numbet validation

* Test refactored for readability purpose

* Typo fixed

* Both MapToRecipientExt tests merged into one

* SMS order mapper moved to OrderMapper class

* SMS order mapper tests added

* Fixed merge conflicts with main

* Integration tests added for the SMS notificaitons API

* Sender number changed to Altinn for integration tests

* PostTests API base path fixed for integration tests

* Revert:Removed the k6 tests added with integration tests

* Ternary to if-else to remove code smell

* Removed 5 digit pass as LinkMobility need 9 digits

* Validation logic changed for SMS mobile numbers

* RegEx removed from validation logic

* Invalid mobile number error message updated

Co-authored-by: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com>

* Arguments fixed for the tests

* Tests messgae update

---------

Co-authored-by: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com>
  • Loading branch information
khanrn and acn-sbuad authored Jan 30, 2024
1 parent 8864150 commit fb1f0ff
Show file tree
Hide file tree
Showing 13 changed files with 973 additions and 6 deletions.
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
Expand Up @@ -247,6 +247,7 @@ void AddInputModelValidators(IServiceCollection services)
{
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,56 @@
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 starting with country code must be provided for all recipients.");

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)
{
if (string.IsNullOrEmpty(mobileNumber) || (!mobileNumber.StartsWith('+') && !mobileNumber.StartsWith("00")))
{
return false;
}

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

PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.GetInstance();
PhoneNumber phoneNumber = phoneNumberUtil.Parse(mobileNumber, null);
return phoneNumberUtil.IsValidNumber(phoneNumber);
}
}
Loading

0 comments on commit fb1f0ff

Please sign in to comment.