Skip to content

Commit 682f951

Browse files
tba76acn-sbuad
andauthored
Consumer and service for update status sms (#409)
* Consumer and service for update status sms * Fixed codesmells * re-wrote get tests * fixed code smell * Update src/Altinn.Notifications.Core/Models/Notification/EmailSendOperationResult.cs Co-authored-by: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com> * Removed nullable from SmsSendOperationResult * Update test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/GetTests.cs * Add test for invalid SmsSendOperationResult * Adjust test for failed parsing --------- Co-authored-by: acn-sbuad <stephanie.buadu@avanade.com> Co-authored-by: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com>
1 parent 1c61702 commit 682f951

File tree

16 files changed

+311
-33
lines changed

16 files changed

+311
-33
lines changed

src/Altinn.Notifications.Core/Models/Notification/SendOperationResult.cs renamed to src/Altinn.Notifications.Core/Models/Notification/EmailSendOperationResult.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
namespace Altinn.Notifications.Core.Models.Notification;
66

77
/// <summary>
8-
/// A class representing a send operation update object
8+
/// A class representing an email send operation result object
99
/// </summary>
10-
public class SendOperationResult
10+
public class EmailSendOperationResult
1111
{
1212
/// <summary>
1313
/// The notification id
@@ -25,29 +25,29 @@ public class SendOperationResult
2525
public EmailNotificationResultType? SendResult { get; set; }
2626

2727
/// <summary>
28-
/// Json serializes the <see cref="SendOperationResult"/>
28+
/// Json serializes the <see cref="EmailSendOperationResult"/>
2929
/// </summary>
3030
public string Serialize()
3131
{
3232
return JsonSerializer.Serialize(this, JsonSerializerOptionsProvider.Options);
3333
}
3434

3535
/// <summary>
36-
/// Deserialize a json string into the <see cref="SendOperationResult"/>
36+
/// Deserialize a json string into the <see cref="EmailSendOperationResult"/>
3737
/// </summary>
38-
public static SendOperationResult? Deserialize(string serializedString)
38+
public static EmailSendOperationResult? Deserialize(string serializedString)
3939
{
40-
return JsonSerializer.Deserialize<SendOperationResult>(
40+
return JsonSerializer.Deserialize<EmailSendOperationResult>(
4141
serializedString, JsonSerializerOptionsProvider.Options);
4242
}
4343

4444
/// <summary>
45-
/// Try to parse a json string into a<see cref="SendOperationResult"/>
45+
/// Try to parse a json string into a<see cref="EmailSendOperationResult"/>
4646
/// </summary>
47-
public static bool TryParse(string input, out SendOperationResult value)
47+
public static bool TryParse(string input, out EmailSendOperationResult value)
4848
{
49-
SendOperationResult? parsedOutput;
50-
value = new SendOperationResult();
49+
EmailSendOperationResult? parsedOutput;
50+
value = new EmailSendOperationResult();
5151

5252
if (string.IsNullOrEmpty(input))
5353
{
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Text.Json;
2+
3+
using Altinn.Notifications.Core.Enums;
4+
5+
namespace Altinn.Notifications.Core.Models.Notification;
6+
7+
/// <summary>
8+
/// A class representing a sms send operation update object
9+
/// </summary>
10+
public class SmsSendOperationResult
11+
{
12+
/// <summary>
13+
/// The notification id
14+
/// </summary>
15+
public Guid NotificationId { get; set; }
16+
17+
/// <summary>
18+
/// The reference to the delivery in sms gateway
19+
/// </summary>
20+
public string GatewayReference { get; set; } = string.Empty;
21+
22+
/// <summary>
23+
/// The sms send result
24+
/// </summary>
25+
public SmsNotificationResultType SendResult { get; set; }
26+
27+
/// <summary>
28+
/// Json serializes the <see cref="SmsSendOperationResult"/>
29+
/// </summary>
30+
public string Serialize()
31+
{
32+
return JsonSerializer.Serialize(this, JsonSerializerOptionsProvider.Options);
33+
}
34+
35+
/// <summary>
36+
/// Deserialize a json string into the <see cref="SmsSendOperationResult"/>
37+
/// </summary>
38+
public static SmsSendOperationResult? Deserialize(string serializedString)
39+
{
40+
return JsonSerializer.Deserialize<SmsSendOperationResult>(
41+
serializedString, JsonSerializerOptionsProvider.Options);
42+
}
43+
44+
/// <summary>
45+
/// Try to parse a json string into a<see cref="SmsSendOperationResult"/>
46+
/// </summary>
47+
public static bool TryParse(string input, out SmsSendOperationResult value)
48+
{
49+
SmsSendOperationResult? parsedOutput;
50+
value = new SmsSendOperationResult();
51+
52+
if (string.IsNullOrEmpty(input))
53+
{
54+
return false;
55+
}
56+
57+
try
58+
{
59+
parsedOutput = Deserialize(input!);
60+
61+
value = parsedOutput!;
62+
return value.NotificationId != Guid.Empty;
63+
}
64+
catch
65+
{
66+
// try parse, we simply return false if fails
67+
}
68+
69+
return false;
70+
}
71+
}

src/Altinn.Notifications.Core/Services/EmailNotificationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public async Task SendNotifications()
7070
}
7171

7272
/// <inheritdoc/>
73-
public async Task UpdateSendStatus(SendOperationResult sendOperationResult)
73+
public async Task UpdateSendStatus(EmailSendOperationResult sendOperationResult)
7474
{
7575
// set to new to allow new iteration of regular proceessing if transient error
7676
if (sendOperationResult.SendResult == EmailNotificationResultType.Failed_TransientError)

src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ public interface IEmailNotificationService
2121
/// <summary>
2222
/// Update send status for a notification
2323
/// </summary>
24-
public Task UpdateSendStatus(SendOperationResult sendOperationResult);
24+
public Task UpdateSendStatus(EmailSendOperationResult sendOperationResult);
2525
}

src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Altinn.Notifications.Core.Models;
2+
using Altinn.Notifications.Core.Models.Notification;
23

34
namespace Altinn.Notifications.Core.Services.Interfaces;
45

@@ -16,4 +17,9 @@ public interface ISmsNotificationService
1617
/// Starts the process of sending all ready sms notifications
1718
/// </summary>
1819
public Task SendNotifications();
20+
21+
/// <summary>
22+
/// Update send status for an sms notification
23+
/// </summary>
24+
public Task UpdateSendStatus(SmsSendOperationResult sendOperationResult);
1925
}

src/Altinn.Notifications.Core/Services/SmsNotificationService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public async Task SendNotifications()
6969
}
7070
}
7171

72+
/// <inheritdoc/>
73+
public async Task UpdateSendStatus(SmsSendOperationResult sendOperationResult)
74+
{
75+
await _repository.UpdateSendStatus(sendOperationResult.NotificationId, sendOperationResult.SendResult, sendOperationResult.GatewayReference);
76+
}
77+
7278
private async Task CreateNotificationForRecipient(Guid orderId, DateTime requestedSendTime, string recipientId, string recipientNumber, SmsNotificationResultType type)
7379
{
7480
var smsNotification = new SmsNotification()

src/Altinn.Notifications.Integrations/Configuration/KafkaSettings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public class KafkaSettings
4545
/// </summary>
4646
public string EmailStatusUpdatedTopicName { get; set; } = string.Empty;
4747

48+
/// <summary>
49+
/// The name of the sms status updated topic
50+
/// </summary>
51+
public string SmsStatusUpdatedTopicName { get; set; } = string.Empty;
52+
4853
/// <summary>
4954
/// The name of the platform service update topic
5055
/// </summary>

src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public static void AddKafkaServices(this IServiceCollection services, IConfigura
3030
.AddHostedService<PastDueOrdersConsumer>()
3131
.AddHostedService<PastDueOrdersRetryConsumer>()
3232
.AddHostedService<EmailStatusConsumer>()
33+
.AddHostedService<SmsStatusConsumer>()
3334
.AddHostedService<AltinnServiceUpdateConsumer>()
3435
.Configure<KafkaSettings>(config.GetSection(nameof(KafkaSettings)));
3536
}

src/Altinn.Notifications.Integrations/Kafka/Consumers/EmailStatusConsumer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
4242

4343
private async Task ProcessStatus(string message)
4444
{
45-
bool succeeded = SendOperationResult.TryParse(message, out SendOperationResult result);
45+
bool succeeded = EmailSendOperationResult.TryParse(message, out EmailSendOperationResult result);
4646

4747
if (!succeeded)
4848
{
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Altinn.Notifications.Core.Integrations;
2+
using Altinn.Notifications.Core.Models.Notification;
3+
using Altinn.Notifications.Core.Services.Interfaces;
4+
using Altinn.Notifications.Integrations.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Altinn.Notifications.Integrations.Kafka.Consumers;
9+
10+
/// <summary>
11+
/// Kafka consumer class for status messages about sms notifications
12+
/// </summary>
13+
public class SmsStatusConsumer : KafkaConsumerBase<SmsStatusConsumer>
14+
{
15+
private readonly ISmsNotificationService _smsNotificationsService;
16+
private readonly IKafkaProducer _producer;
17+
private readonly string _retryTopicName;
18+
private readonly ILogger<SmsStatusConsumer> _logger;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="SmsStatusConsumer"/> class.
22+
/// </summary>
23+
public SmsStatusConsumer(
24+
ISmsNotificationService smsNotificationsService,
25+
IKafkaProducer producer,
26+
IOptions<KafkaSettings> settings,
27+
ILogger<SmsStatusConsumer> logger)
28+
: base(settings, logger, settings.Value.SmsStatusUpdatedTopicName)
29+
{
30+
_smsNotificationsService = smsNotificationsService;
31+
_producer = producer;
32+
_retryTopicName = settings.Value.SmsStatusUpdatedTopicName;
33+
_logger = logger;
34+
}
35+
36+
/// <inheritdoc/>
37+
protected override Task ExecuteAsync(CancellationToken stoppingToken)
38+
{
39+
return Task.Run(() => ConsumeMessage(ProcessStatus, RetryStatus, stoppingToken), stoppingToken);
40+
}
41+
42+
private async Task ProcessStatus(string message)
43+
{
44+
bool succeeded = SmsSendOperationResult.TryParse(message, out SmsSendOperationResult result);
45+
46+
if (!succeeded)
47+
{
48+
_logger.LogError("// SmsStatusConsumer // ProcessStatus // Deserialization of message failed. {Message}", message);
49+
return;
50+
}
51+
52+
await _smsNotificationsService.UpdateSendStatus(result);
53+
}
54+
55+
private async Task RetryStatus(string message)
56+
{
57+
await _producer.ProduceAsync(_retryTopicName, message!);
58+
}
59+
}

src/Altinn.Notifications/appsettings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"EmailQueueTopicName": "altinn.notifications.email.queue",
2626
"EmailStatusUpdatedTopicName": "altinn.notifications.email.status.updated",
2727
"SmsQueueTopicName": "altinn.notifications.sms.queue",
28+
"SmsStatusUpdatedTopicName": "altinn.notifications.sms.status.updated",
2829
"HealthCheckTopic": "altinn.notifications.health.check",
2930
"AltinnServiceUpdateTopicName": "altinn.platform.service.updated",
3031
"Admin": {
@@ -35,6 +36,7 @@
3536
"altinn.notifications.email.queue",
3637
"altinn.notifications.email.status.updated",
3738
"altinn.notifications.sms.queue",
39+
"altinn.notifications.sms.status.updated",
3840
"altinn.platform.service.updated"
3941

4042
]

test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task RunTask_ConfirmExpectedSideEffects()
3030

3131
(_, EmailNotification notification) = await PostgreUtil.PopulateDBWithOrderAndEmailNotification(_sendersRef);
3232

33-
SendOperationResult sendOperationResult = new()
33+
EmailSendOperationResult sendOperationResult = new()
3434
{
3535
NotificationId = notification.Id,
3636
OperationId = Guid.NewGuid().ToString(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using Altinn.Notifications.Core.Enums;
2+
using Altinn.Notifications.Core.Models.Notification;
3+
using Altinn.Notifications.Integrations.Kafka.Consumers;
4+
using Altinn.Notifications.IntegrationTests.Utils;
5+
using Microsoft.Extensions.Hosting;
6+
7+
using Xunit;
8+
9+
namespace Altinn.Notifications.IntegrationTests.Notifications.Integrations.TestingConsumers;
10+
11+
public class SmsStatusConsumerTests : IAsyncLifetime
12+
{
13+
private readonly string _statusUpdatedTopicName = Guid.NewGuid().ToString();
14+
private readonly string _sendersRef = $"ref-{Guid.NewGuid()}";
15+
16+
[Fact]
17+
public async Task RunTask_ConfirmExpectedSideEffects()
18+
{
19+
// Arrange
20+
Dictionary<string, string> vars = new()
21+
{
22+
{ "KafkaSettings__SmsStatusUpdatedTopicName", _statusUpdatedTopicName },
23+
{ "KafkaSettings__Admin__TopicList", $"[\"{_statusUpdatedTopicName}\"]" }
24+
};
25+
26+
using SmsStatusConsumer consumerService = (SmsStatusConsumer)ServiceUtil
27+
.GetServices(new List<Type>() { typeof(IHostedService) }, vars)
28+
.First(s => s.GetType() == typeof(SmsStatusConsumer))!;
29+
30+
(_, SmsNotification notification) = await PostgreUtil.PopulateDBWithOrderAndSmsNotification(_sendersRef);
31+
32+
SmsSendOperationResult sendOperationResult = new()
33+
{
34+
NotificationId = notification.Id,
35+
SendResult = SmsNotificationResultType.Accepted,
36+
GatewayReference = Guid.NewGuid().ToString()
37+
};
38+
39+
await KafkaUtil.PublishMessageOnTopic(_statusUpdatedTopicName, sendOperationResult.Serialize());
40+
41+
// Act
42+
await consumerService.StartAsync(CancellationToken.None);
43+
await Task.Delay(10000);
44+
await consumerService.StopAsync(CancellationToken.None);
45+
46+
// Assert
47+
string smsNotificationStatus = await SelectSmsNotificationStatus(notification.Id);
48+
Assert.Equal(SmsNotificationResultType.Accepted.ToString(), smsNotificationStatus);
49+
}
50+
51+
[Fact]
52+
public async Task RunTask_ParseSmsSendOperationResult_StatusNotUpdated()
53+
{
54+
// Arrange
55+
Dictionary<string, string> vars = new()
56+
{
57+
{ "KafkaSettings__SmsStatusUpdatedTopicName", _statusUpdatedTopicName },
58+
{ "KafkaSettings__Admin__TopicList", $"[\"{_statusUpdatedTopicName}\"]" }
59+
};
60+
61+
using SmsStatusConsumer consumerService = (SmsStatusConsumer)ServiceUtil
62+
.GetServices(new List<Type>() { typeof(IHostedService) }, vars)
63+
.First(s => s.GetType() == typeof(SmsStatusConsumer))!;
64+
65+
(_, SmsNotification notification) = await PostgreUtil.PopulateDBWithOrderAndSmsNotification(_sendersRef);
66+
67+
await KafkaUtil.PublishMessageOnTopic(_statusUpdatedTopicName, string.Empty);
68+
69+
// Act
70+
await consumerService.StartAsync(CancellationToken.None);
71+
await Task.Delay(10000);
72+
await consumerService.StopAsync(CancellationToken.None);
73+
74+
// Assert
75+
string smsNotificationStatus = await SelectSmsNotificationStatus(notification.Id);
76+
Assert.Equal(SmsNotificationResultType.New.ToString(), smsNotificationStatus);
77+
}
78+
79+
public Task InitializeAsync()
80+
{
81+
return Task.CompletedTask;
82+
}
83+
84+
public async Task DisposeAsync()
85+
{
86+
await Dispose(true);
87+
}
88+
89+
protected virtual async Task Dispose(bool disposing)
90+
{
91+
await PostgreUtil.DeleteOrderFromDb(_sendersRef);
92+
await KafkaUtil.DeleteTopicAsync(_statusUpdatedTopicName);
93+
}
94+
95+
private static async Task<string> SelectSmsNotificationStatus(Guid notificationId)
96+
{
97+
string sql = $"select result from notifications.smsnotifications where alternateid = '{notificationId}'";
98+
return await PostgreUtil.RunSqlReturnOutput<string>(sql);
99+
}
100+
}

0 commit comments

Comments
 (0)