feat: consume transactional email notifications

Add contract validation, SMTP delivery results, terminal failure context, neutral development seeding, and local Docker setup.

Ref: IT-1033
This commit is contained in:
2026-08-04 12:32:28 +03:00
parent b8435ac07b
commit 2757869176
37 changed files with 1465 additions and 122 deletions
@@ -1,14 +1,17 @@
namespace HrynCo.NotificationService.Worker;
using HrynCo.NotificationService.Contracts.Messages;
using Hrynco.RabbitMq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using HrynCo.NotificationService.Worker.Services.EmailProcessing;
using Hrynco.RabbitMq;
using Microsoft.Extensions.Options;
public sealed class SendEmailConsumer : RabbitMqConsumerBase<SendEmailMessage, SendEmailMessageData>
{
internal const string IncomingQueue = "notification.send-email";
internal const string SupportedMessageType = "Notification.SendEmail.v1";
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<SendEmailConsumer> _logger;
public SendEmailConsumer(
IOptionsMonitor<RabbitMqSettings> options,
@@ -17,16 +20,51 @@ public sealed class SendEmailConsumer : RabbitMqConsumerBase<SendEmailMessage, S
: base(options, logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
private const string IncomingQueue = "notification.send-email";
protected override string QueueName => IncomingQueue;
protected override async Task HandleMessageAsync(SendEmailMessage message, CancellationToken cancellationToken)
protected override bool TryValidateMessage(
SendEmailMessage message,
RabbitMqMessageContext context,
out string? validationError)
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<ISendEmailService>();
validationError = SendEmailDeliveryValidator.GetValidationError(
message,
context,
SupportedMessageType);
return validationError is null;
}
protected override async Task HandleMessageAsync(
SendEmailMessage message,
RabbitMqMessageContext context,
CancellationToken cancellationToken)
{
string payloadCorrelationId = message.CorrelationContext.CorrelationId;
if (!string.IsNullOrWhiteSpace(context.CorrelationId) &&
!string.Equals(context.CorrelationId, payloadCorrelationId, StringComparison.Ordinal))
{
_logger.LogWarning(
"Broker and payload correlation IDs differ; payload correlation ID will be used");
}
await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
ISendEmailService service = scope.ServiceProvider.GetRequiredService<ISendEmailService>();
await service.ProcessAsync(message, cancellationToken);
}
protected override async Task HandleMessageRetriesExhaustedAsync(
SendEmailMessage message,
RabbitMqMessageContext context,
Exception exception,
CancellationToken cancellationToken)
{
string deliveryError = NotificationDeliveryErrorFormatter.Format(exception);
await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
INotificationResultPublisher resultPublisher =
scope.ServiceProvider.GetRequiredService<INotificationResultPublisher>();
await resultPublisher.PublishAsync(message, deliveryError, cancellationToken);
}
}