namespace HrynCo.NotificationService.Worker.Services.EmailProcessing; using HrynCo.NotificationService.Contracts.Messages; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using Microsoft.Extensions.Logging; internal sealed class SendEmailService : ISendEmailService { private readonly IEmailChannelRepository _channelRepository; private readonly IEmailChannelUsageRepository _usageRepository; private readonly IEmailTemplateService _templateService; private readonly IEmailTemplateRenderingService _templateRenderingService; private readonly ISmtpEmailSender _smtpEmailSender; private readonly INotificationResultPublisher _resultPublisher; private readonly ILogger _logger; public SendEmailService( IEmailChannelRepository channelRepository, IEmailChannelUsageRepository usageRepository, IEmailTemplateService templateService, IEmailTemplateRenderingService templateRenderingService, ISmtpEmailSender smtpEmailSender, INotificationResultPublisher resultPublisher, ILogger logger) { _channelRepository = channelRepository; _usageRepository = usageRepository; _templateService = templateService; _templateRenderingService = templateRenderingService; _smtpEmailSender = smtpEmailSender; _resultPublisher = resultPublisher; _logger = logger; } public async Task ProcessAsync(SendEmailMessage message, CancellationToken cancellationToken) { SendEmailMessageValidator.Validate(message); SendEmailMessageData data = message.Data; string redactedRecipient = RecipientAddressRedactor.Redact(data.RecipientEmail); _logger.LogInformation( "Processing SendEmail for service={Service} template={Template} recipient={Recipient} [CorrelationId={CorrelationId}]", data.ServiceName, data.TemplateKey, redactedRecipient, message.CorrelationContext?.CorrelationId); EmailChannel channel = await ResolveChannelAsync(data.ServiceName, cancellationToken); EmailTemplate template = await GetTemplateAsync(data, cancellationToken); await EnforceLimitsAsync(channel, cancellationToken); RenderedEmail rendered = _templateRenderingService.Render(template, data); SmtpChannelSettings smtpChannel = channel.Settings as SmtpChannelSettings ?? throw new InvalidOperationException( $"Channel type '{channel.EmailChannelType}' is not supported for sending."); try { await _smtpEmailSender.SendAsync( smtpChannel, rendered, data.RecipientEmail, data.RecipientName, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "SMTP send failed for channel {ChannelId}", channel.Id); throw; } await _usageRepository.IncrementUsageAsync( channel.Id, DateOnly.FromDateTime(DateTime.UtcNow), cancellationToken); _logger.LogInformation( "Email sent successfully service={Service} template={Template} recipient={Recipient}", data.ServiceName, data.TemplateKey, redactedRecipient); await _resultPublisher.PublishAsync(message, null, cancellationToken); } private async Task GetTemplateAsync(SendEmailMessageData data, CancellationToken cancellationToken) { return await _templateService.GetAsync( data.ServiceName, data.TemplateKey, data.LanguageCode, cancellationToken); } private async Task ResolveChannelAsync(string serviceName, CancellationToken ct) { var channels = await _channelRepository.GetByServiceAsync(serviceName, ct); return channels .Where(c => c.IsActive) .OrderBy(c => c.Priority) .FirstOrDefault() ?? throw new InvalidOperationException( $"No active email channel found for service '{serviceName}'."); } private async Task EnforceLimitsAsync(EmailChannel channel, CancellationToken ct) { DateOnly today = DateOnly.FromDateTime(DateTime.UtcNow); if (channel.DailyLimit.HasValue) { int daily = await _usageRepository.GetDailyCountAsync(channel.Id, today, ct); if (daily >= channel.DailyLimit.Value) { throw new InvalidOperationException( $"Channel '{channel.Id}' daily limit of {channel.DailyLimit.Value} reached ({daily} sent today)."); } } if (channel.MonthlyLimit.HasValue) { int monthly = await _usageRepository.GetMonthlyCountAsync(channel.Id, today.Year, today.Month, ct); if (monthly >= channel.MonthlyLimit.Value) { throw new InvalidOperationException( $"Channel '{channel.Id}' monthly limit of {channel.MonthlyLimit.Value} reached ({monthly} sent this month)."); } } } }