diff --git a/dbsetup.sh b/dbsetup.sh
index 23c45aca..8d726b60 100755
--- a/dbsetup.sh
+++ b/dbsetup.sh
@@ -1,6 +1,10 @@
#!/bin/bash
export PGPASSWORD=Password
+# alter max connections
+psql -h localhost -p 5432 -U platform_notifications_admin -d notificationsdb \
+-c "ALTER SYSTEM SET max_connections TO '200';"
+
# set up platform_notifications role
psql -h localhost -p 5432 -U platform_notifications_admin -d notificationsdb \
-c "DO \$\$
diff --git a/src/Altinn.Notifications.Core/Enums/OrderProcessingStatus.cs b/src/Altinn.Notifications.Core/Enums/OrderProcessingStatus.cs
index 757bb44f..645cecab 100644
--- a/src/Altinn.Notifications.Core/Enums/OrderProcessingStatus.cs
+++ b/src/Altinn.Notifications.Core/Enums/OrderProcessingStatus.cs
@@ -8,6 +8,7 @@ public enum OrderProcessingStatus
{
Registered,
Processing,
- Completed
+ Completed,
+ SendConditionNotMet
}
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
diff --git a/src/Altinn.Notifications.Core/Exceptions/OrderProcessingException.cs b/src/Altinn.Notifications.Core/Exceptions/OrderProcessingException.cs
new file mode 100644
index 00000000..ff84773d
--- /dev/null
+++ b/src/Altinn.Notifications.Core/Exceptions/OrderProcessingException.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Altinn.Notifications.Core.Exceptions;
+
+///
+/// Represents errors that occur during order processing operations.
+///
+[ExcludeFromCodeCoverage]
+public class OrderProcessingException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OrderProcessingException() : base()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ /// The message that describes the error.
+ public OrderProcessingException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public OrderProcessingException(string message, Exception inner) : base(message, inner)
+ {
+ }
+}
diff --git a/src/Altinn.Notifications.Core/Services/GetOrderService.cs b/src/Altinn.Notifications.Core/Services/GetOrderService.cs
index 87bea858..69445850 100644
--- a/src/Altinn.Notifications.Core/Services/GetOrderService.cs
+++ b/src/Altinn.Notifications.Core/Services/GetOrderService.cs
@@ -17,6 +17,7 @@ public class GetOrderService : IGetOrderService
{ OrderProcessingStatus.Registered, "Order has been registered and is awaiting requested send time before processing." },
{ OrderProcessingStatus.Processing, "Order processing is ongoing. Notifications are being generated." },
{ OrderProcessingStatus.Completed, "Order processing is completed. All notifications have been generated." },
+ { OrderProcessingStatus.SendConditionNotMet, "Order processing was stopped due to send condition not being met." }
};
///
diff --git a/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs
index 68888568..8df75812 100644
--- a/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs
+++ b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs
@@ -2,11 +2,13 @@
using Altinn.Notifications.Core.Configuration;
using Altinn.Notifications.Core.Enums;
+using Altinn.Notifications.Core.Exceptions;
using Altinn.Notifications.Core.Integrations;
using Altinn.Notifications.Core.Models.Orders;
using Altinn.Notifications.Core.Persistence;
using Altinn.Notifications.Core.Services.Interfaces;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Altinn.Notifications.Core.Services;
@@ -19,8 +21,10 @@ public class OrderProcessingService : IOrderProcessingService
private readonly IOrderRepository _orderRepository;
private readonly IEmailOrderProcessingService _emailProcessingService;
private readonly ISmsOrderProcessingService _smsProcessingService;
+ private readonly IConditionClient _conditionClient;
private readonly IKafkaProducer _producer;
private readonly string _pastDueOrdersTopic;
+ private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
@@ -29,14 +33,18 @@ public OrderProcessingService(
IOrderRepository orderRepository,
IEmailOrderProcessingService emailProcessingService,
ISmsOrderProcessingService smsProcessingService,
+ IConditionClient conditionClient,
IKafkaProducer producer,
- IOptions kafkaSettings)
+ IOptions kafkaSettings,
+ ILogger logger)
{
_orderRepository = orderRepository;
_emailProcessingService = emailProcessingService;
_smsProcessingService = smsProcessingService;
+ _conditionClient = conditionClient;
_producer = producer;
_pastDueOrdersTopic = kafkaSettings.Value.PastDueOrdersTopicName;
+ _logger = logger;
}
///
@@ -65,6 +73,12 @@ public async Task StartProcessingPastDueOrders()
///
public async Task ProcessOrder(NotificationOrder order)
{
+ if (!await IsSendConditionMet(order, isRetry: false))
+ {
+ await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.SendConditionNotMet);
+ return;
+ }
+
NotificationChannel ch = order.NotificationChannel;
switch (ch)
@@ -83,6 +97,12 @@ public async Task ProcessOrder(NotificationOrder order)
///
public async Task ProcessOrderRetry(NotificationOrder order)
{
+ if (!await IsSendConditionMet(order, isRetry: true))
+ {
+ await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.SendConditionNotMet);
+ return;
+ }
+
NotificationChannel ch = order.NotificationChannel;
switch (ch)
@@ -97,4 +117,39 @@ public async Task ProcessOrderRetry(NotificationOrder order)
await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.Completed);
}
+
+ ///
+ /// Checks the send condition provided by the order request to determine if condition is met
+ ///
+ /// The notification order to check
+ /// Boolean indicating if this is a retry attempt
+ /// True if condition is met and processing should continue
+ /// Throws an exception if failure on first attempt ot check condition
+ internal async Task IsSendConditionMet(NotificationOrder order, bool isRetry)
+ {
+ if (order.ConditionEndpoint == null)
+ {
+ return true;
+ }
+
+ var conditionCheckResult = await _conditionClient.CheckSendCondition(order.ConditionEndpoint);
+
+ return conditionCheckResult.Match(
+ successResult =>
+ {
+ return successResult;
+ },
+ errorResult =>
+ {
+ if (!isRetry)
+ {
+ // Always send to retry on first error. Exception is caught by consumer and message is moved to retry topic.
+ throw new OrderProcessingException($"// OrderProcessingService // IsSendConditionMet // Condition check for order with ID '{order.Id}' failed with HTTP status code '{errorResult.StatusCode}' at endpoint '{order.ConditionEndpoint}'");
+ }
+
+ // notifications should always be created and sent if the condition check is not successful
+ _logger.LogInformation("// OrderProcessingService // IsSendConditionMet // Condition check for order with ID '{ID}' failed on retry. Processing regardless.", order.Id);
+ return true;
+ });
+ }
}
diff --git a/src/Altinn.Notifications.Integrations/Kafka/Consumers/PastDueOrdersRetryConsumer.cs b/src/Altinn.Notifications.Integrations/Kafka/Consumers/PastDueOrdersRetryConsumer.cs
index 09ec0212..6af88578 100644
--- a/src/Altinn.Notifications.Integrations/Kafka/Consumers/PastDueOrdersRetryConsumer.cs
+++ b/src/Altinn.Notifications.Integrations/Kafka/Consumers/PastDueOrdersRetryConsumer.cs
@@ -13,17 +13,22 @@ namespace Altinn.Notifications.Integrations.Kafka.Consumers;
public class PastDueOrdersRetryConsumer : KafkaConsumerBase
{
private readonly IOrderProcessingService _orderProcessingService;
+ private readonly IDateTimeService _dateTime;
+
+ private readonly int _processingDelayMins = 1;
///
/// Initializes a new instance of the class.
///
public PastDueOrdersRetryConsumer(
IOrderProcessingService orderProcessingService,
+ IDateTimeService dateTimeService,
IOptions settings,
ILogger logger)
: base(settings, logger, settings.Value.PastDueOrdersRetryTopicName)
{
_orderProcessingService = orderProcessingService;
+ _dateTime = dateTimeService;
}
///
@@ -41,6 +46,16 @@ private async Task ProcessOrder(string message)
return;
}
+ // adding a delay relative to send time to allow transient faults to be resolved
+ TimeSpan diff = _dateTime.UtcNow() - order.RequestedSendTime;
+
+ TimeSpan delayForRetryAttempt = TimeSpan.FromMinutes(_processingDelayMins) - diff;
+
+ if (delayForRetryAttempt > TimeSpan.Zero)
+ {
+ await Task.Delay(delayForRetryAttempt);
+ }
+
await _orderProcessingService.ProcessOrderRetry(order!);
}
diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.34/00-alter-types.sql b/src/Altinn.Notifications.Persistence/Migration/v0.34/00-alter-types.sql
new file mode 100644
index 00000000..0bb3a0d3
--- /dev/null
+++ b/src/Altinn.Notifications.Persistence/Migration/v0.34/00-alter-types.sql
@@ -0,0 +1 @@
+ALTER TYPE public.orderprocessingstate ADD VALUE IF NOT EXISTS 'SendConditionNotMet';
\ No newline at end of file
diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.34/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.34/01-functions-and-procedures.sql
new file mode 100644
index 00000000..f6298ea5
--- /dev/null
+++ b/src/Altinn.Notifications.Persistence/Migration/v0.34/01-functions-and-procedures.sql
@@ -0,0 +1,401 @@
+-- This script is autogenerated from the tool DbTools. Do not edit manually.
+
+-- getemailrecipients.sql:
+CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid)
+RETURNS TABLE(
+ recipientorgno text,
+ recipientnin text,
+ toaddress text
+)
+LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+__orderid BIGINT := (SELECT _id from notifications.orders
+ where alternateid = _alternateid);
+BEGIN
+RETURN query
+ SELECT e.recipientorgno, e.recipientnin, e.toaddress
+ FROM notifications.emailnotifications e
+ WHERE e._orderid = __orderid;
+END;
+$BODY$;
+
+-- getemailsstatusnewupdatestatus.sql:
+CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus()
+ RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ latest_email_timeout TIMESTAMP WITH TIME ZONE;
+BEGIN
+ SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog);
+ IF latest_email_timeout IS NOT NULL THEN
+ IF latest_email_timeout >= NOW() THEN
+ RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE;
+ RETURN;
+ ELSE
+ UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog);
+ END IF;
+ END IF;
+
+ RETURN query
+ WITH updated AS (
+ UPDATE notifications.emailnotifications
+ SET result = 'Sending', resulttime = now()
+ WHERE result = 'New'
+ RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress)
+ SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype
+ FROM updated u, notifications.emailtexts et
+ WHERE u._orderid = et._orderid;
+END;
+$BODY$;
+
+-- getemailsummary.sql:
+CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2(
+ _alternateorderid uuid,
+ _creatorname text)
+ RETURNS TABLE(
+ sendersreference text,
+ alternateid uuid,
+ recipientorgno text,
+ recipientnin text,
+ toaddress text,
+ result emailnotificationresulttype,
+ resulttime timestamptz)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+
+ BEGIN
+ RETURN QUERY
+ SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime
+ FROM notifications.emailnotifications n
+ LEFT JOIN notifications.orders o ON n._orderid = o._id
+ WHERE o.alternateid = _alternateorderid
+ and o.creatorname = _creatorname;
+ IF NOT FOUND THEN
+ RETURN QUERY
+ SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz
+ FROM notifications.orders o
+ WHERE o.alternateid = _alternateorderid
+ and o.creatorname = _creatorname;
+ END IF;
+ END;
+$BODY$;
+
+-- getmetrics.sql:
+CREATE OR REPLACE FUNCTION notifications.getmetrics(
+ month_input int,
+ year_input int
+)
+RETURNS TABLE (
+ org text,
+ placed_orders bigint,
+ sent_emails bigint,
+ succeeded_emails bigint,
+ sent_sms bigint,
+ succeeded_sms bigint
+)
+AS $$
+BEGIN
+ RETURN QUERY
+ SELECT
+ o.creatorname,
+ COUNT(DISTINCT o._id) AS placed_orders,
+ SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails,
+ SUM(CASE WHEN e.result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END) AS succeeded_emails,
+ SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms,
+ SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms
+ FROM notifications.orders o
+ LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid
+ LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid
+ WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input
+ AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input
+ GROUP BY o.creatorname;
+END;
+$$ LANGUAGE plpgsql;
+
+
+-- getorderincludestatus.sql:
+CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v4(
+ _alternateid uuid,
+ _creatorname text
+)
+RETURNS TABLE(
+ alternateid uuid,
+ creatorname text,
+ sendersreference text,
+ created timestamp with time zone,
+ requestedsendtime timestamp with time zone,
+ processed timestamp with time zone,
+ processedstatus orderprocessingstate,
+ notificationchannel text,
+ ignorereservation boolean,
+ resourceid text,
+ conditionendpoint text,
+ generatedemailcount bigint,
+ succeededemailcount bigint,
+ generatedsmscount bigint,
+ succeededsmscount bigint
+)
+LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+ _target_orderid INTEGER;
+ _succeededEmailCount BIGINT;
+ _generatedEmailCount BIGINT;
+ _succeededSmsCount BIGINT;
+ _generatedSmsCount BIGINT;
+BEGIN
+ SELECT _id INTO _target_orderid
+ FROM notifications.orders
+ WHERE orders.alternateid = _alternateid
+ AND orders.creatorname = _creatorname;
+
+ SELECT
+ SUM(CASE WHEN result IN ('Delivered', 'Succeeded') THEN 1 ELSE 0 END),
+ COUNT(1) AS generatedEmailCount
+ INTO _succeededEmailCount, _generatedEmailCount
+ FROM notifications.emailnotifications
+ WHERE _orderid = _target_orderid;
+
+ SELECT
+ SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END),
+ COUNT(1) AS generatedSmsCount
+ INTO _succeededSmsCount, _generatedSmsCount
+ FROM notifications.smsnotifications
+ WHERE _orderid = _target_orderid;
+
+ RETURN QUERY
+ SELECT
+ orders.alternateid,
+ orders.creatorname,
+ orders.sendersreference,
+ orders.created,
+ orders.requestedsendtime,
+ orders.processed,
+ orders.processedstatus,
+ orders.notificationorder->>'NotificationChannel',
+ CASE
+ WHEN orders.notificationorder->>'IgnoreReservation' IS NULL THEN NULL
+ ELSE (orders.notificationorder->>'IgnoreReservation')::BOOLEAN
+ END AS IgnoreReservation,
+ orders.notificationorder->>'ResourceId',
+ orders.notificationorder->>'ConditionEndpoint',
+ _generatedEmailCount,
+ _succeededEmailCount,
+ _generatedSmsCount,
+ _succeededSmsCount
+ FROM
+ notifications.orders AS orders
+ WHERE
+ orders.alternateid = _alternateid;
+END;
+$BODY$;
+
+
+-- getorderspastsendtimeupdatestatus.sql:
+CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus()
+ RETURNS TABLE(notificationorders jsonb)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+RETURN QUERY
+ UPDATE notifications.orders
+ SET processedstatus = 'Processing'
+ WHERE _id IN (select _id
+ from notifications.orders
+ where processedstatus = 'Registered'
+ and requestedsendtime <= now() + INTERVAL '1 minute'
+ limit 50)
+ RETURNING notificationorder AS notificationorders;
+END;
+$BODY$;
+
+-- getsmsrecipients.sql:
+CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid)
+RETURNS TABLE(
+ recipientorgno text,
+ recipientnin text,
+ mobilenumber text
+)
+LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+__orderid BIGINT := (SELECT _id from notifications.orders
+ where alternateid = _orderid);
+BEGIN
+RETURN query
+ SELECT s.recipientorgno, s.recipientnin, s.mobilenumber
+ FROM notifications.smsnotifications s
+ WHERE s._orderid = __orderid;
+END;
+$BODY$;
+
+-- getsmsstatusnewupdatestatus.sql:
+CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus()
+ RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+
+ RETURN query
+ WITH updated AS (
+ UPDATE notifications.smsnotifications
+ SET result = 'Sending', resulttime = now()
+ WHERE result = 'New'
+ RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber)
+ SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body
+ FROM updated u, notifications.smstexts st
+ WHERE u._orderid = st._orderid;
+END;
+$BODY$;
+
+-- getsmssummary.sql:
+CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2(
+ _alternateorderid uuid,
+ _creatorname text)
+ RETURNS TABLE(
+ sendersreference text,
+ alternateid uuid,
+ recipientorgno text,
+ recipientnin text,
+ mobilenumber text,
+ result smsnotificationresulttype,
+ resulttime timestamptz)
+ LANGUAGE 'plpgsql'
+AS $BODY$
+
+ BEGIN
+ RETURN QUERY
+ SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime
+ FROM notifications.smsnotifications n
+ LEFT JOIN notifications.orders o ON n._orderid = o._id
+ WHERE o.alternateid = _alternateorderid
+ and o.creatorname = _creatorname;
+ IF NOT FOUND THEN
+ RETURN QUERY
+ SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz
+ FROM notifications.orders o
+ WHERE o.alternateid = _alternateorderid
+ and o.creatorname = _creatorname;
+ END IF;
+ END;
+$BODY$;
+
+-- insertemailnotification.sql:
+CREATE OR REPLACE PROCEDURE notifications.insertemailnotification(
+_orderid uuid,
+_alternateid uuid,
+_recipientorgno TEXT,
+_recipientnin TEXT,
+_toaddress TEXT,
+_result text,
+_resulttime timestamptz,
+_expirytime timestamptz)
+LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+__orderid BIGINT := (SELECT _id from notifications.orders
+ where alternateid = _orderid);
+BEGIN
+
+INSERT INTO notifications.emailnotifications(
+_orderid,
+alternateid,
+recipientorgno,
+recipientnin,
+toaddress, result,
+resulttime,
+expirytime)
+VALUES (
+__orderid,
+_alternateid,
+_recipientorgno,
+_recipientnin,
+_toaddress,
+_result::emailnotificationresulttype,
+_resulttime,
+_expirytime);
+END;
+$BODY$;
+
+-- insertemailtext.sql:
+CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT)
+LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype)
+ VALUES (__orderid, _fromaddress, _subject, _body, _contenttype);
+END;
+$BODY$;
+
+
+-- insertorder.sql:
+CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB)
+RETURNS BIGINT
+ LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+_orderid BIGINT;
+BEGIN
+ INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder)
+ VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder)
+ RETURNING _id INTO _orderid;
+
+ RETURN _orderid;
+END;
+$BODY$;
+
+-- insertsmsnotification.sql:
+CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification(
+_orderid uuid,
+_alternateid uuid,
+_recipientorgno TEXT,
+_recipientnin TEXT,
+_mobilenumber TEXT,
+_result text,
+_smscount integer,
+_resulttime timestamptz,
+_expirytime timestamptz
+)
+LANGUAGE 'plpgsql'
+AS $BODY$
+DECLARE
+__orderid BIGINT := (SELECT _id from notifications.orders
+ where alternateid = _orderid);
+BEGIN
+
+INSERT INTO notifications.smsnotifications(
+_orderid,
+alternateid,
+recipientorgno,
+recipientnin,
+mobilenumber,
+result,
+smscount,
+resulttime,
+expirytime)
+VALUES (
+__orderid,
+_alternateid,
+_recipientorgno,
+_recipientnin,
+_mobilenumber,
+_result::smsnotificationresulttype,
+_smscount,
+_resulttime,
+_expirytime);
+END;
+$BODY$;
+
+-- updateemailstatus.sql:
+CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text)
+LANGUAGE 'plpgsql'
+AS $BODY$
+BEGIN
+ UPDATE notifications.emailnotifications
+ SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid
+ WHERE alternateid = _alternateid;
+END;
+$BODY$;
+
diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs
index e2b7bbd5..152b0d76 100644
--- a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs
+++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs
@@ -51,7 +51,7 @@ public async Task RunTask_ConfirmExpectedSideEffects()
}
///
- /// When a new order is picked up by the consumer and all email notifications are created befor, only processedstatus is changed.
+ /// When a new order is picked up by the consumer and all email notifications are created before processedstatus is changed.
/// We measure the sucess of this test by confirming that the processedstatus is completed.
///
[Fact]
@@ -59,10 +59,10 @@ public async Task RunTask_ConfirmChangeOfStatus()
{
// Arrange
Dictionary vars = new()
- {
- { "KafkaSettings__PastDueOrdersRetryTopicName", _retryTopicName },
- { "KafkaSettings__Admin__TopicList", $"[\"{_retryTopicName}\"]" }
- };
+ {
+ { "KafkaSettings__PastDueOrdersRetryTopicName", _retryTopicName },
+ { "KafkaSettings__Admin__TopicList", $"[\"{_retryTopicName}\"]" }
+ };
using PastDueOrdersRetryConsumer consumerRetryService = (PastDueOrdersRetryConsumer)ServiceUtil
.GetServices(new List() { typeof(IHostedService) }, vars)
diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/OrderRepositoryTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/OrderRepositoryTests.cs
index d9db996f..821e3a81 100644
--- a/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/OrderRepositoryTests.cs
+++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/OrderRepositoryTests.cs
@@ -102,5 +102,46 @@ public async Task GetOrderWithStatusById_ConfirmConditionEndpoint()
// Assert
Assert.Equal("https://vg.no/condition", actual?.ConditionEndpoint?.ToString());
}
+
+ [Fact]
+ public async Task SetProcessingStatus_AllStatusesSupported()
+ {
+ // Arrange
+ OrderRepository repo = (OrderRepository)ServiceUtil
+ .GetServices(new List() { typeof(IOrderRepository) })
+ .First(i => i.GetType() == typeof(OrderRepository));
+
+ NotificationOrder order = new()
+ {
+ Id = Guid.NewGuid(),
+ Created = DateTime.UtcNow,
+ Creator = new("test"),
+ Templates = new List()
+ {
+ new EmailTemplate("noreply@altinn.no", "Subject", "Body", EmailContentType.Plain),
+ new SmsTemplate("Altinn", "This is the body")
+ },
+ ConditionEndpoint = new Uri("https://vg.no/condition")
+ };
+
+ _orderIdsToDelete.Add(order.Id);
+ await repo.Create(order);
+
+ foreach (OrderProcessingStatus statusType in Enum.GetValues(typeof(OrderProcessingStatus)))
+ {
+ // Act
+ await repo.SetProcessingStatus(order.Id, statusType);
+
+ // Assert
+ string sql = $@"SELECT count(1)
+ FROM notifications.orders
+ WHERE alternateid = '{order.Id}'
+ AND processedstatus = '{statusType}'";
+
+ int orderCount = await PostgreUtil.RunSqlReturnOutput(sql);
+
+ Assert.Equal(1, orderCount);
+ }
+ }
}
}
diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/GetOrderServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/GetOrderServiceTests.cs
index 703b6172..30b5ed83 100644
--- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/GetOrderServiceTests.cs
+++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/GetOrderServiceTests.cs
@@ -135,6 +135,7 @@ await result.Match(
[InlineData(OrderProcessingStatus.Registered, "Order has been registered and is awaiting requested send time before processing.")]
[InlineData(OrderProcessingStatus.Processing, "Order processing is ongoing. Notifications are being generated.")]
[InlineData(OrderProcessingStatus.Completed, "Order processing is completed. All notifications have been generated.")]
+ [InlineData(OrderProcessingStatus.SendConditionNotMet, "Order processing was stopped due to send condition not being met.")]
public void GetStatusDescription_ExpectedDescription(OrderProcessingStatus status, string expected)
{
string actual = GetOrderService.GetStatusDescription(status);
diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs
index 7c252c3f..458c5167 100644
--- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs
+++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs
@@ -3,20 +3,21 @@
using System.Threading.Tasks;
using Altinn.Notifications.Core.Enums;
+using Altinn.Notifications.Core.Exceptions;
using Altinn.Notifications.Core.Integrations;
using Altinn.Notifications.Core.Models.Orders;
+using Altinn.Notifications.Core.Models.SendCondition;
using Altinn.Notifications.Core.Persistence;
using Altinn.Notifications.Core.Services;
using Altinn.Notifications.Core.Services.Interfaces;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
-using static Altinn.Authorization.ABAC.Constants.XacmlConstants;
-
namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices;
public class OrderProcessingServiceTests
@@ -96,6 +97,39 @@ public async Task ProcessOrder_SmsOrder_SmsServiceCalled()
emailMockService.Verify(e => e.ProcessOrder(It.IsAny()), Times.Never);
}
+ [Fact]
+ public async Task ProcessOrder_SendConditionNotMet_ProcessingStops()
+ {
+ // Arrange
+ NotificationOrder order = new()
+ {
+ Id = Guid.NewGuid(),
+ NotificationChannel = NotificationChannel.Sms,
+ ConditionEndpoint = new Uri("https://vg.no")
+ };
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.CheckSendCondition(It.IsAny())).ReturnsAsync(false);
+
+ Mock repoMock = new();
+ repoMock.Setup(r => r.SetProcessingStatus(
+ It.Is(g => g.Equals(order.Id)),
+ It.Is(ops => ops == OrderProcessingStatus.SendConditionNotMet)));
+ var orderProcessingService = GetTestService(conditionClient: clientMock.Object, repo: repoMock.Object);
+
+ // Act
+ await orderProcessingService.ProcessOrder(order);
+
+ // Assert
+ clientMock.Verify(
+ c => c.CheckSendCondition(It.IsAny()),
+ Times.Once);
+
+ repoMock.Verify(
+ r => r.SetProcessingStatus(It.Is(g => g.Equals(order.Id)), It.Is(ops => ops == OrderProcessingStatus.SendConditionNotMet)),
+ Times.Once);
+ }
+
[Fact]
public async Task ProcessOrder_SerivceThrowsException_ProcessingStatusIsNotSet()
{
@@ -148,6 +182,39 @@ public async Task ProcessOrderRetry_SmsOrder_SmsServiceCalled()
emailMockService.Verify(e => e.ProcessOrderRetry(It.IsAny()), Times.Never);
}
+ [Fact]
+ public async Task ProcessOrderRetry_SendConditionNotMet_ProcessingStops()
+ {
+ // Arrange
+ NotificationOrder order = new()
+ {
+ Id = Guid.NewGuid(),
+ NotificationChannel = NotificationChannel.Sms,
+ ConditionEndpoint = new Uri("https://vg.no")
+ };
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.CheckSendCondition(It.IsAny())).ReturnsAsync(false);
+
+ Mock repoMock = new();
+ repoMock.Setup(r => r.SetProcessingStatus(
+ It.Is(g => g.Equals(order.Id)),
+ It.Is(ops => ops == OrderProcessingStatus.SendConditionNotMet)));
+ var orderProcessingService = GetTestService(conditionClient: clientMock.Object, repo: repoMock.Object);
+
+ // Act
+ await orderProcessingService.ProcessOrderRetry(order);
+
+ // Assert
+ clientMock.Verify(
+ c => c.CheckSendCondition(It.IsAny()),
+ Times.Once);
+
+ repoMock.Verify(
+ r => r.SetProcessingStatus(It.Is(g => g.Equals(order.Id)), It.Is(ops => ops == OrderProcessingStatus.SendConditionNotMet)),
+ Times.Once);
+ }
+
[Fact]
public async Task ProcessOrderRetry_SerivceThrowsException_ProcessingStatusIsNotSet()
{
@@ -175,11 +242,78 @@ public async Task ProcessOrderRetry_SerivceThrowsException_ProcessingStatusIsNot
Times.Never);
}
+ [Fact]
+ public async Task IsSendConditionMet_NoConditionEndpoint_ReturnsTrue()
+ {
+ // Arrange
+ NotificationOrder order = new() { Id = Guid.NewGuid(), ConditionEndpoint = null };
+ var orderProcessingService = GetTestService();
+
+ // Act
+ bool actual = await orderProcessingService.IsSendConditionMet(order, isRetry: false);
+
+ // Assert
+ Assert.True(actual);
+ }
+
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(false, false)]
+ [InlineData(true, true)]
+ [InlineData(false, true)]
+ public async Task IsSendConditionMet_SuccessResultFromClient_ReturnsSameAsClient(bool expectedConditionResult, bool isRetry)
+ {
+ // Arrange
+ NotificationOrder order = new() { Id = Guid.NewGuid(), ConditionEndpoint = new Uri("https://vg.no") };
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.CheckSendCondition(It.IsAny())).ReturnsAsync(expectedConditionResult);
+ var orderProcessingService = GetTestService(conditionClient: clientMock.Object);
+
+ // Act
+ bool actual = await orderProcessingService.IsSendConditionMet(order, isRetry: isRetry);
+
+ // Assert
+ Assert.Equal(expectedConditionResult, actual);
+ }
+
+ [Fact]
+ public async Task IsSendConditionMet_ErrorResultOnFirstAttempt_ThrowsException()
+ {
+ // Arrange
+ NotificationOrder order = new() { Id = Guid.NewGuid(), ConditionEndpoint = new Uri("https://vg.no") };
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.CheckSendCondition(It.IsAny())).ReturnsAsync(new ConditionClientError());
+ var orderProcessingService = GetTestService(conditionClient: clientMock.Object);
+
+ // Act
+ await Assert.ThrowsAsync(async () => await orderProcessingService.IsSendConditionMet(order, isRetry: false));
+ }
+
+ [Fact]
+ public async Task IsSendConditionMet_ErrorResultOnRetry_ReturnsTrue()
+ {
+ // Arrange
+ NotificationOrder order = new() { Id = Guid.NewGuid(), ConditionEndpoint = new Uri("https://vg.no") };
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.CheckSendCondition(It.IsAny())).ReturnsAsync(new ConditionClientError());
+ var orderProcessingService = GetTestService(conditionClient: clientMock.Object);
+
+ // Act
+ bool actual = await orderProcessingService.IsSendConditionMet(order, isRetry: true);
+
+ // Assert
+ Assert.True(actual);
+ }
+
private static OrderProcessingService GetTestService(
IOrderRepository? repo = null,
IEmailOrderProcessingService? emailMock = null,
ISmsOrderProcessingService? smsMock = null,
- IKafkaProducer? producer = null)
+ IKafkaProducer? producer = null,
+ IConditionClient? conditionClient = null)
{
if (repo == null)
{
@@ -205,8 +339,14 @@ private static OrderProcessingService GetTestService(
producer = producerMock.Object;
}
+ if (conditionClient == null)
+ {
+ var conditionClientMock = new Mock();
+ conditionClient = conditionClientMock.Object;
+ }
+
var kafkaSettings = new Altinn.Notifications.Core.Configuration.KafkaSettings() { PastDueOrdersTopicName = _pastDueTopicName };
- return new OrderProcessingService(repo, emailMock, smsMock, producer, Options.Create(kafkaSettings));
+ return new OrderProcessingService(repo, emailMock, smsMock, conditionClient, producer, Options.Create(kafkaSettings), new LoggerFactory().CreateLogger());
}
}