+{
+ private const string TemplateKey = "provider_activated";
+
+ public async Task HandleAsync(
+ ProviderActivatedIntegrationEvent integrationEvent,
+ CancellationToken cancellationToken = default)
+ {
+ var correlationId = $"{TemplateKey}:{integrationEvent.ProviderId}";
+
+ if (await logRepository.ExistsByCorrelationIdAsync(correlationId, cancellationToken))
+ {
+ logger.LogInformation(
+ "Skipping provider activation email for {ProviderId} — already sent (correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, correlationId);
+ return;
+ }
+
+ var userResult = await usersModuleApi.GetUserByIdAsync(integrationEvent.UserId, cancellationToken);
+ if (!userResult.IsSuccess || userResult.Value == null || string.IsNullOrWhiteSpace(userResult.Value.Email))
+ {
+ logger.LogWarning(
+ "Could not resolve email for user {UserId}. Skipping provider activation email for provider {ProviderId}.",
+ integrationEvent.UserId, integrationEvent.ProviderId);
+ return;
+ }
+
+ var safeName = HtmlEncoder.Default.Encode(integrationEvent.Name);
+ var recipientEmail = userResult.Value.Email;
+
+ var payload = JsonSerializer.Serialize(new
+ {
+ To = recipientEmail,
+ Subject = "Seu cadastro foi aprovado!",
+ HtmlBody = $"Olá, {safeName}!
Seu cadastro foi aprovado. Você já pode receber solicitações de serviço.
",
+ TextBody = $"Olá, {integrationEvent.Name}!\nSeu cadastro foi aprovado. Você já pode receber solicitações de serviço.",
+ From = (string?)null,
+ CorrelationId = correlationId,
+ TemplateKey = TemplateKey
+ });
+
+ var message = OutboxMessage.Create(
+ channel: ECommunicationChannel.Email,
+ payload: payload,
+ priority: ECommunicationPriority.High,
+ correlationId: correlationId);
+
+ try
+ {
+ await outboxRepository.AddAsync(message, cancellationToken);
+ await outboxRepository.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Provider activation email enqueued for provider {ProviderId} (UserId: {UserId}, correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, integrationEvent.UserId, correlationId);
+ }
+ catch (Exception ex)
+ {
+ if (ex is Microsoft.EntityFrameworkCore.DbUpdateException dbEx)
+ {
+ var processedException = MeAjudaAi.Shared.Database.Exceptions.PostgreSqlExceptionProcessor.ProcessException(dbEx);
+
+ if (processedException is MeAjudaAi.Shared.Database.Exceptions.UniqueConstraintException)
+ {
+ logger.LogInformation(
+ "Skipping provider activation email for {ProviderId} — already enqueued or sent (correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, correlationId);
+ return;
+ }
+ }
+
+ throw;
+ }
+ }
+}
diff --git a/src/Modules/Communications/Application/Handlers/ProviderAwaitingVerificationIntegrationEventHandler.cs b/src/Modules/Communications/Application/Handlers/ProviderAwaitingVerificationIntegrationEventHandler.cs
new file mode 100644
index 000000000..9a2c60fd8
--- /dev/null
+++ b/src/Modules/Communications/Application/Handlers/ProviderAwaitingVerificationIntegrationEventHandler.cs
@@ -0,0 +1,71 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging.Messages.Providers;
+using MeAjudaAi.Contracts.Shared;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+
+namespace MeAjudaAi.Modules.Communications.Application.Handlers;
+
+///
+/// Handler para notificar administradores quando um prestador aguarda verificação.
+///
+public sealed class ProviderAwaitingVerificationIntegrationEventHandler(
+ IOutboxMessageRepository outboxRepository,
+ IConfiguration configuration,
+ ILogger logger)
+ : IEventHandler
+{
+ public async Task HandleAsync(ProviderAwaitingVerificationIntegrationEvent integrationEvent, CancellationToken cancellationToken = default)
+ {
+ var adminEmail = configuration["Communications:AdminEmail"];
+ if (string.IsNullOrWhiteSpace(adminEmail))
+ {
+ adminEmail = "suporte@meajudaai.com.br";
+ }
+
+ var correlationId = $"admin_verification_alert:{integrationEvent.ProviderId}";
+
+ var emailPayload = new
+ {
+ To = adminEmail,
+ Subject = "Novo prestador aguardando verificação",
+ Body = $"O prestador {integrationEvent.Name} (ID: {integrationEvent.ProviderId}) enviou documentos para análise.",
+ TemplateKey = "admin-provider-verification-alert",
+ CorrelationId = correlationId
+ };
+
+ var message = OutboxMessage.Create(
+ ECommunicationChannel.Email,
+ JsonSerializer.Serialize(emailPayload),
+ ECommunicationPriority.Normal,
+ correlationId: correlationId);
+
+ try
+ {
+ await outboxRepository.AddAsync(message, cancellationToken);
+ await outboxRepository.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Admin notification enqueued for provider {ProviderId} (correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, correlationId);
+ }
+ catch (Exception ex)
+ {
+ var processedException = MeAjudaAi.Shared.Database.Exceptions.PostgreSqlExceptionProcessor.ProcessException(
+ ex as Microsoft.EntityFrameworkCore.DbUpdateException ?? new Microsoft.EntityFrameworkCore.DbUpdateException(ex.Message, ex));
+
+ if (processedException is MeAjudaAi.Shared.Database.Exceptions.UniqueConstraintException)
+ {
+ logger.LogInformation(
+ "Skipping admin notification for provider {ProviderId} — already enqueued (correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, correlationId);
+ return;
+ }
+
+ throw;
+ }
+ }
+}
diff --git a/src/Modules/Communications/Application/Handlers/ProviderVerificationStatusUpdatedIntegrationEventHandler.cs b/src/Modules/Communications/Application/Handlers/ProviderVerificationStatusUpdatedIntegrationEventHandler.cs
new file mode 100644
index 000000000..17e5af00b
--- /dev/null
+++ b/src/Modules/Communications/Application/Handlers/ProviderVerificationStatusUpdatedIntegrationEventHandler.cs
@@ -0,0 +1,100 @@
+using MeAjudaAi.Contracts.Modules.Users;
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging.Messages.Providers;
+using MeAjudaAi.Contracts.Shared;
+using MeAjudaAi.Shared.Utilities;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+
+namespace MeAjudaAi.Modules.Communications.Application.Handlers;
+
+///
+/// Handler para notificar o prestador quando seu status de verificação é atualizado.
+///
+public sealed class ProviderVerificationStatusUpdatedIntegrationEventHandler(
+ IOutboxMessageRepository outboxRepository,
+ IUsersModuleApi usersModuleApi,
+ ILogger logger)
+ : IEventHandler
+{
+ public async Task HandleAsync(ProviderVerificationStatusUpdatedIntegrationEvent integrationEvent, CancellationToken cancellationToken = default)
+ {
+ var userResult = await usersModuleApi.GetUserByIdAsync(integrationEvent.UserId, cancellationToken);
+
+ if (!userResult.IsSuccess)
+ {
+ throw new InvalidOperationException($"Failed to fetch user {integrationEvent.UserId} for provider {integrationEvent.ProviderId} verification update: {userResult.Error.Message}");
+ }
+
+ if (userResult.Value == null || string.IsNullOrWhiteSpace(userResult.Value.Email))
+ {
+ logger.LogWarning(
+ "Could not resolve email for user {UserId}. Skipping verification status notification for provider {ProviderId}.",
+ integrationEvent.UserId, integrationEvent.ProviderId);
+ return;
+ }
+
+ var recipientEmail = userResult.Value.Email;
+ var normalizedStatus = integrationEvent.NewStatus.Trim().ToLowerInvariant();
+
+ var templateKey = normalizedStatus switch
+ {
+ "verified" or "approved" => "provider-verification-approved",
+ "rejected" or "denied" => "provider-verification-rejected",
+ "pending" or "awaiting" => "provider-verification-pending",
+ _ => "provider-verification-status-update" // default/fallback
+ };
+
+ var displayStatus = normalizedStatus switch
+ {
+ "verified" or "approved" => "aprovado",
+ "rejected" or "denied" => "rejeitado",
+ "pending" or "awaiting" => "pendente",
+ _ => normalizedStatus
+ };
+
+ var correlationId = $"verification_status_update:{integrationEvent.Id}:{integrationEvent.ProviderId}:{normalizedStatus}";
+
+ var emailPayload = new
+ {
+ To = recipientEmail,
+ Subject = $"Atualização no status de verificação: {displayStatus}",
+ Body = $"Olá {integrationEvent.Name}, seu status de verificação foi alterado para: {displayStatus}. {integrationEvent.Comments}",
+ TemplateKey = templateKey,
+ CorrelationId = correlationId
+ };
+
+ var message = OutboxMessage.Create(
+ channel: ECommunicationChannel.Email,
+ payload: JsonSerializer.Serialize(emailPayload),
+ priority: ECommunicationPriority.High,
+ correlationId: correlationId);
+
+ try
+ {
+ await outboxRepository.AddAsync(message, cancellationToken);
+ await outboxRepository.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Verification status update notification enqueued for user {UserId} ({Email}, correlationId: {CorrelationId}).",
+ integrationEvent.UserId, PiiMaskingHelper.MaskEmail(recipientEmail), correlationId);
+ }
+ catch (Exception ex)
+ {
+ var processedException = MeAjudaAi.Shared.Database.Exceptions.PostgreSqlExceptionProcessor.ProcessException(
+ ex as Microsoft.EntityFrameworkCore.DbUpdateException ?? new Microsoft.EntityFrameworkCore.DbUpdateException(ex.Message, ex));
+
+ if (processedException is MeAjudaAi.Shared.Database.Exceptions.UniqueConstraintException)
+ {
+ logger.LogInformation(
+ "Skipping verification status update for provider {ProviderId} — already enqueued (correlationId: {CorrelationId}).",
+ integrationEvent.ProviderId, correlationId);
+ return;
+ }
+
+ throw;
+ }
+ }
+}
diff --git a/src/Modules/Communications/Application/Handlers/UserRegisteredIntegrationEventHandler.cs b/src/Modules/Communications/Application/Handlers/UserRegisteredIntegrationEventHandler.cs
new file mode 100644
index 000000000..739eb99b2
--- /dev/null
+++ b/src/Modules/Communications/Application/Handlers/UserRegisteredIntegrationEventHandler.cs
@@ -0,0 +1,80 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging.Messages.Users;
+using MeAjudaAi.Contracts.Shared;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+
+namespace MeAjudaAi.Modules.Communications.Application.Handlers;
+
+///
+/// Consome o evento UserRegisteredIntegrationEvent e enfileira e-mail de boas-vindas no Outbox.
+///
+public sealed class UserRegisteredIntegrationEventHandler(
+ IOutboxMessageRepository outboxRepository,
+ ICommunicationLogRepository logRepository,
+ ILogger logger)
+ : IEventHandler
+{
+ private const string TemplateKey = "user_registered";
+
+ public async Task HandleAsync(
+ UserRegisteredIntegrationEvent integrationEvent,
+ CancellationToken cancellationToken = default)
+ {
+ var correlationId = $"{TemplateKey}:{integrationEvent.UserId}";
+
+ // Idempotência: evita re-enfileirar se já foi processado
+ if (await logRepository.ExistsByCorrelationIdAsync(correlationId, cancellationToken))
+ {
+ logger.LogInformation(
+ "Skipping welcome email for user {UserId} — already sent (correlationId: {CorrelationId}).",
+ integrationEvent.UserId, correlationId);
+ return;
+ }
+
+ var payload = JsonSerializer.Serialize(new
+ {
+ To = integrationEvent.Email,
+ Subject = "Bem-vindo ao MeAjudaAi!",
+ HtmlBody = $"Olá, {integrationEvent.FirstName}!
Seja bem-vindo(a) ao MeAjudaAi.
",
+ TextBody = $"Olá, {integrationEvent.FirstName}!\nSeja bem-vindo(a) ao MeAjudaAi.",
+ From = (string?)null,
+ CorrelationId = correlationId,
+ TemplateKey = TemplateKey
+ });
+
+ var message = OutboxMessage.Create(
+ channel: ECommunicationChannel.Email,
+ payload: payload,
+ priority: ECommunicationPriority.Normal,
+ correlationId: correlationId);
+
+ try
+ {
+ await outboxRepository.AddAsync(message, cancellationToken);
+ await outboxRepository.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation(
+ "Welcome email enqueued for user {UserId} (outboxId: {OutboxId}, correlationId: {CorrelationId}).",
+ integrationEvent.UserId, message.Id, correlationId);
+ }
+ catch (Exception ex)
+ {
+ var processedException = MeAjudaAi.Shared.Database.Exceptions.PostgreSqlExceptionProcessor.ProcessException(
+ ex as Microsoft.EntityFrameworkCore.DbUpdateException ?? new Microsoft.EntityFrameworkCore.DbUpdateException(ex.Message, ex));
+
+ if (processedException is MeAjudaAi.Shared.Database.Exceptions.UniqueConstraintException)
+ {
+ logger.LogInformation(
+ "Skipping welcome email for user {UserId} — already enqueued or sent (correlationId: {CorrelationId}).",
+ integrationEvent.UserId, correlationId);
+ return;
+ }
+
+ throw;
+ }
+ }
+}
diff --git a/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj b/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj
new file mode 100644
index 000000000..3fb799ddd
--- /dev/null
+++ b/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+ <_Parameter1>MeAjudaAi.Modules.Communications.Tests
+
+
+ <_Parameter1>MeAjudaAi.Integration.Tests
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Modules/Communications/Application/ModuleApi/CommunicationsModuleApi.cs b/src/Modules/Communications/Application/ModuleApi/CommunicationsModuleApi.cs
new file mode 100644
index 000000000..5173bafc4
--- /dev/null
+++ b/src/Modules/Communications/Application/ModuleApi/CommunicationsModuleApi.cs
@@ -0,0 +1,150 @@
+using MeAjudaAi.Contracts.Functional;
+using MeAjudaAi.Contracts.Models;
+using MeAjudaAi.Contracts.Modules.Communications;
+using MeAjudaAi.Contracts.Modules.Communications.DTOs;
+using MeAjudaAi.Contracts.Modules.Communications.Queries;
+using MeAjudaAi.Contracts.Shared;
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Shared.Utilities.Constants;
+using System.Text.Json;
+
+namespace MeAjudaAi.Modules.Communications.Application.ModuleApi;
+
+///
+/// Implementação da API pública do módulo de comunicações.
+///
+[MeAjudaAi.Contracts.Modules.ModuleApi(ModuleNames.Communications)]
+public sealed class CommunicationsModuleApi(
+ IOutboxMessageRepository outboxRepository,
+ IEmailTemplateRepository templateRepository,
+ ICommunicationLogRepository logRepository)
+ : ICommunicationsModuleApi
+{
+ private readonly IEmailTemplateRepository _templateRepository = templateRepository;
+
+ public string ModuleName => ModuleNames.Communications;
+ public string ApiVersion => "1.0";
+
+ public Task IsAvailableAsync(CancellationToken cancellationToken = default)
+ => Task.FromResult(true);
+
+ public async Task> SendEmailAsync(
+ EmailMessageDto email,
+ ECommunicationPriority priority = ECommunicationPriority.Normal,
+ CancellationToken ct = default)
+ {
+ if (email == null) return Result.Failure(Error.BadRequest("A mensagem de e-mail não pode ser nula."));
+ if (string.IsNullOrWhiteSpace(email.To)) return Result.Failure(Error.BadRequest("O e-mail do destinatário é obrigatório."));
+ if (string.IsNullOrWhiteSpace(email.Subject)) return Result.Failure(Error.BadRequest("O assunto do e-mail é obrigatório."));
+ if (string.IsNullOrWhiteSpace(email.Body)) return Result.Failure(Error.BadRequest("O corpo do e-mail é obrigatório."));
+
+ if (!Enum.IsDefined(typeof(ECommunicationPriority), priority))
+ return Result.Failure(Error.BadRequest("Prioridade de comunicação inválida."));
+
+ return await EnqueueOutboxAsync(ECommunicationChannel.Email, email, priority, ct);
+ }
+
+ public async Task>> GetTemplatesAsync(CancellationToken ct = default)
+ {
+ var templates = await _templateRepository.GetAllAsync(ct);
+
+ var dtos = templates.Select(x => new EmailTemplateDto(
+ x.Id,
+ x.TemplateKey,
+ x.Subject,
+ x.HtmlBody,
+ x.TextBody,
+ x.IsSystemTemplate,
+ x.Language)).ToList();
+
+ return Result>.Success(dtos);
+ }
+
+ public async Task> SendSmsAsync(
+ SmsMessageDto sms,
+ ECommunicationPriority priority = ECommunicationPriority.Normal,
+ CancellationToken ct = default)
+ {
+ if (sms == null) return Result.Failure(Error.BadRequest("A mensagem SMS não pode ser nula."));
+ if (string.IsNullOrWhiteSpace(sms.PhoneNumber)) return Result.Failure(Error.BadRequest("O número de telefone é obrigatório."));
+ if (string.IsNullOrWhiteSpace(sms.Message)) return Result.Failure(Error.BadRequest("O corpo da mensagem SMS é obrigatório."));
+
+ if (!Enum.IsDefined(typeof(ECommunicationPriority), priority))
+ return Result.Failure(Error.BadRequest("Prioridade de comunicação inválida."));
+
+ return await EnqueueOutboxAsync(ECommunicationChannel.Sms, sms, priority, ct);
+ }
+
+ public async Task> SendPushAsync(
+ PushMessageDto push,
+ ECommunicationPriority priority = ECommunicationPriority.Normal,
+ CancellationToken ct = default)
+ {
+ if (push == null) return Result.Failure(Error.BadRequest("A notificação push não pode ser nula."));
+ if (string.IsNullOrWhiteSpace(push.DeviceToken)) return Result.Failure(Error.BadRequest("O token do dispositivo é obrigatório."));
+ if (string.IsNullOrWhiteSpace(push.Title)) return Result.Failure(Error.BadRequest("O título do push é obrigatório."));
+ if (string.IsNullOrWhiteSpace(push.Body)) return Result.Failure(Error.BadRequest("O corpo do push é obrigatório."));
+
+ if (!Enum.IsDefined(typeof(ECommunicationPriority), priority))
+ return Result.Failure(Error.BadRequest("Prioridade de comunicação inválida."));
+
+ return await EnqueueOutboxAsync(ECommunicationChannel.Push, push, priority, ct);
+ }
+
+ public async Task>> GetLogsAsync(
+ CommunicationLogQuery query,
+ CancellationToken ct = default)
+ {
+ if (query == null) return Result>.Failure(Error.BadRequest("A consulta não pode ser nula."));
+ if (query.PageNumber < 1) return Result>.Failure(Error.BadRequest("O número da página deve ser pelo menos 1."));
+ if (query.PageSize < 1 || query.PageSize > 100) return Result>.Failure(Error.BadRequest("O tamanho da página deve estar entre 1 e 100."));
+
+ var (items, totalCount) = await logRepository.SearchAsync(
+ query.CorrelationId,
+ query.Channel,
+ query.Recipient,
+ query.IsSuccess,
+ query.PageNumber,
+ query.PageSize,
+ ct);
+
+ var dtos = items.Select(x => new CommunicationLogDto(
+ x.Id,
+ x.CorrelationId,
+ x.Channel.ToString(),
+ x.Recipient,
+ x.TemplateKey,
+ x.IsSuccess,
+ x.ErrorMessage,
+ x.AttemptCount,
+ x.CreatedAt)).ToList();
+
+ return Result>.Success(new PagedResult
+ {
+ Items = dtos,
+ PageNumber = query.PageNumber,
+ PageSize = query.PageSize,
+ TotalItems = totalCount
+ });
+ }
+
+ private async Task> EnqueueOutboxAsync(
+ ECommunicationChannel channel,
+ TPayload payload,
+ ECommunicationPriority priority,
+ CancellationToken ct)
+ {
+ var serializedPayload = JsonSerializer.Serialize(payload);
+ var message = OutboxMessage.Create(
+ channel,
+ serializedPayload,
+ priority);
+
+ await outboxRepository.AddAsync(message, ct);
+ await outboxRepository.SaveChangesAsync(ct);
+
+ return Result.Success(message.Id);
+ }
+}
diff --git a/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs b/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs
new file mode 100644
index 000000000..79ca395a3
--- /dev/null
+++ b/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs
@@ -0,0 +1,61 @@
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Communications.Application.Services;
+
+///
+/// Worker de background para processamento periódico do Outbox de comunicações.
+///
+internal sealed class CommunicationsOutboxWorker(
+ IServiceScopeFactory scopeFactory,
+ ILogger logger) : BackgroundService
+{
+ private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10);
+ private readonly TimeSpan _stuckTimeout = TimeSpan.FromMinutes(5);
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ logger.LogInformation("Communications Outbox Worker started.");
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ using var scope = scopeFactory.CreateScope();
+
+ // 1. Recupera mensagens travadas em processamento
+ var repository = scope.ServiceProvider.GetRequiredService();
+ var resetCount = await repository.ResetStaleProcessingMessagesAsync(DateTime.UtcNow.Subtract(_stuckTimeout), stoppingToken);
+
+
+ if (resetCount > 0)
+ {
+ logger.LogWarning("Reset {Count} stuck outbox messages back to Pending.", resetCount);
+ }
+
+ // 2. Processa mensagens pendentes
+ var processor = scope.ServiceProvider.GetRequiredService();
+ int processedCount = await processor.ProcessPendingMessagesAsync(50, stoppingToken);
+
+ if (processedCount > 0)
+ {
+ logger.LogInformation("Processed {Count} outbox messages.", processedCount);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Normal shutdown
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error occurred while processing communications outbox.");
+ }
+
+ await Task.Delay(_checkInterval, stoppingToken);
+ }
+
+ logger.LogInformation("Communications Outbox Worker stopped.");
+ }
+}
diff --git a/src/Modules/Communications/Application/Services/Email/IEmailService.cs b/src/Modules/Communications/Application/Services/Email/IEmailService.cs
new file mode 100644
index 000000000..7bba33e22
--- /dev/null
+++ b/src/Modules/Communications/Application/Services/Email/IEmailService.cs
@@ -0,0 +1,18 @@
+using MeAjudaAi.Contracts.Functional;
+using MeAjudaAi.Contracts.Modules.Communications.DTOs;
+
+namespace MeAjudaAi.Modules.Communications.Application.Services.Email;
+
+///
+/// Serviço interno para envio de e-mails, abstraindo o canal real.
+///
+public interface IEmailService
+{
+ ///
+ /// Envia uma mensagem de e-mail de forma imediata.
+ ///
+ /// DTO com dados do e-mail
+ /// Token de cancelamento
+ /// ID da mensagem no provedor
+ Task> SendAsync(EmailMessageDto message, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Application/Services/Email/StubEmailService.cs b/src/Modules/Communications/Application/Services/Email/StubEmailService.cs
new file mode 100644
index 000000000..0328e8f37
--- /dev/null
+++ b/src/Modules/Communications/Application/Services/Email/StubEmailService.cs
@@ -0,0 +1,25 @@
+using MeAjudaAi.Contracts.Functional;
+using MeAjudaAi.Contracts.Modules.Communications.DTOs;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Communications.Application.Services.Email;
+
+///
+/// Stub para envio de e-mails em desenvolvimento/MVP.
+/// Apenas loga no console as informações do e-mail.
+///
+public sealed class StubEmailService(ILogger logger) : IEmailService
+{
+ public async Task> SendAsync(EmailMessageDto message, CancellationToken cancellationToken = default)
+ {
+ // Simulação de delay de rede
+ await Task.Delay(100, cancellationToken);
+
+ logger.LogInformation(
+ "[STUB EMAIL] Email dispatched (recipient and subject masked) | Body length: {BodyLength} bytes",
+ message.Body.Length);
+
+ // Retorna um ID falso do provedor
+ return Result.Success($"stub_{Guid.NewGuid():N}");
+ }
+}
diff --git a/src/Modules/Communications/Application/Services/OutboxProcessorService.cs b/src/Modules/Communications/Application/Services/OutboxProcessorService.cs
new file mode 100644
index 000000000..2f6d7d1eb
--- /dev/null
+++ b/src/Modules/Communications/Application/Services/OutboxProcessorService.cs
@@ -0,0 +1,206 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Modules.Communications.Domain.Services;
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Shared.Database.Outbox;
+using MeAjudaAi.Shared.Utilities;
+using MeAjudaAi.Contracts.Shared;
+using MeAjudaAi.Contracts.Modules.Communications.DTOs;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+using OutboxMessage = MeAjudaAi.Modules.Communications.Domain.Entities.OutboxMessage;
+
+namespace MeAjudaAi.Modules.Communications.Application.Services;
+
+///
+/// Serviço de processamento das mensagens do Outbox.
+///
+public interface IOutboxProcessorService
+{
+ ///
+ /// Processa mensagens pendentes no Outbox.
+ ///
+ Task ProcessPendingMessagesAsync(
+ int batchSize = 50,
+ CancellationToken cancellationToken = default);
+}
+
+///
+/// Implementação do processador de Outbox específica para comunicações.
+/// Estende a base genérica para aproveitar lógica de polling e retries.
+///
+public sealed class OutboxProcessorService(
+ IOutboxMessageRepository outboxRepository,
+ ICommunicationLogRepository logRepository,
+ IEmailSender emailSender,
+ ISmsSender smsSender,
+ IPushSender pushSender,
+ ILogger logger)
+ : OutboxProcessorBase(outboxRepository, logger), IOutboxProcessorService
+{
+ protected override async Task DispatchAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var success = await DispatchInternalAsync(message, cancellationToken);
+ return success
+ ? DispatchResult.Success()
+ : DispatchResult.Failure("Dispatch service returned false.");
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ return DispatchResult.Canceled();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error dispatching outbox message {Id} ({Channel}).", message.Id, message.Channel);
+ return DispatchResult.Failure(ex.Message);
+ }
+ }
+
+ protected override async Task OnSuccessAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ var recipientRaw = ExtractRecipient(message);
+ var recipientMasked = MaskRecipientForChannel(recipientRaw, message.Channel);
+
+ try
+ {
+ var log = CommunicationLog.CreateSuccess(
+ correlationId: message.CorrelationId ?? $"outbox:{message.Id}",
+ channel: message.Channel,
+ recipient: recipientRaw,
+ attemptCount: message.RetryCount + 1,
+ outboxMessageId: message.Id,
+ templateKey: ExtractTemplateKey(message));
+
+ await logRepository.AddAsync(log, cancellationToken);
+ // SaveChanges handled by OutboxProcessorBase
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to create success log for outbox message {Id} (CorrelationId: {CorrelationId}).",
+ message.Id, message.CorrelationId);
+ }
+
+ logger.LogInformation("Outbox message {Id} ({Channel}) sent to {Recipient}.",
+ message.Id, message.Channel, recipientMasked);
+ }
+
+ protected override async Task OnFailureAsync(OutboxMessage message, string? error, CancellationToken cancellationToken)
+ {
+ var recipientRaw = ExtractRecipient(message);
+ var recipientMasked = MaskRecipientForChannel(recipientRaw, message.Channel);
+
+ if (!message.HasRetriesLeft)
+ {
+ try
+ {
+ var log = CommunicationLog.CreateFailure(
+ correlationId: message.CorrelationId ?? $"outbox:{message.Id}",
+ channel: message.Channel,
+ recipient: recipientRaw,
+ errorMessage: error ?? "Max retries reached.",
+ attemptCount: message.RetryCount,
+ outboxMessageId: message.Id,
+ templateKey: ExtractTemplateKey(message));
+
+ await logRepository.AddAsync(log, cancellationToken);
+ // SaveChanges handled by OutboxProcessorBase
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to create failure log for outbox message {Id} (CorrelationId: {CorrelationId}).",
+ message.Id, message.CorrelationId);
+ }
+ }
+
+ logger.LogWarning("Outbox message {Id} dispatch to {Channel} for {Recipient} failed. Attempt {Retry}/{Max}.",
+ message.Id, message.Channel, recipientMasked, message.RetryCount, message.MaxRetries);
+ }
+
+ private async Task DispatchInternalAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ return message.Channel switch
+ {
+ ECommunicationChannel.Email => await DispatchEmailAsync(message, cancellationToken),
+ ECommunicationChannel.Sms => await DispatchSmsAsync(message, cancellationToken),
+ ECommunicationChannel.Push => await DispatchPushAsync(message, cancellationToken),
+ _ => throw new InvalidOperationException($"Unknown channel: {message.Channel}")
+ };
+ }
+
+ private async Task DispatchEmailAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ var email = JsonSerializer.Deserialize(message.Payload)
+ ?? throw new InvalidOperationException("Invalid email payload.");
+
+ var htmlBody = email.HtmlBody ?? (email.Body != null ? System.Net.WebUtility.HtmlEncode(email.Body) : string.Empty);
+ var textBody = email.TextBody ?? email.Body ?? string.Empty;
+
+ return await emailSender.SendAsync(
+ new Domain.Services.EmailMessage(email.To, email.Subject, htmlBody, textBody, email.From),
+ cancellationToken);
+ }
+
+ private async Task DispatchSmsAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ var sms = JsonSerializer.Deserialize(message.Payload)
+ ?? throw new InvalidOperationException("Invalid SMS payload.");
+
+ return await smsSender.SendAsync(
+ new Domain.Services.SmsMessage(sms.PhoneNumber, sms.Body),
+ cancellationToken);
+ }
+
+ private async Task DispatchPushAsync(OutboxMessage message, CancellationToken cancellationToken)
+ {
+ var push = JsonSerializer.Deserialize(message.Payload)
+ ?? throw new InvalidOperationException("Invalid push payload.");
+
+ return await pushSender.SendAsync(
+ new Domain.Services.PushNotification(push.DeviceToken, push.Title, push.Body, push.Data),
+ cancellationToken);
+ }
+
+ private string MaskRecipientForChannel(string recipient, ECommunicationChannel channel)
+ {
+ return channel switch
+ {
+ ECommunicationChannel.Email => PiiMaskingHelper.MaskEmail(recipient),
+ ECommunicationChannel.Sms => PiiMaskingHelper.MaskPhoneNumber(recipient),
+ ECommunicationChannel.Push => PiiMaskingHelper.MaskSensitiveData(recipient),
+ _ => PiiMaskingHelper.MaskSensitiveData(recipient)
+ };
+ }
+
+ private string? ExtractTemplateKey(OutboxMessage message)
+ {
+ if (message.Channel != ECommunicationChannel.Email) return null;
+ try
+ {
+ return JsonSerializer.Deserialize(message.Payload)?.TemplateKey;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private string ExtractRecipient(OutboxMessage message)
+ {
+ try
+ {
+ return message.Channel switch
+ {
+ ECommunicationChannel.Email => JsonSerializer.Deserialize(message.Payload)?.To ?? "unknown",
+ ECommunicationChannel.Sms => JsonSerializer.Deserialize(message.Payload)?.PhoneNumber ?? "unknown",
+ ECommunicationChannel.Push => JsonSerializer.Deserialize(message.Payload)?.DeviceToken ?? "unknown",
+ _ => "unknown"
+ };
+ }
+ catch
+ {
+ return "error-extracting";
+ }
+ }
+}
diff --git a/src/Modules/Communications/Application/packages.lock.json b/src/Modules/Communications/Application/packages.lock.json
new file mode 100644
index 000000000..7c49998de
--- /dev/null
+++ b/src/Modules/Communications/Application/packages.lock.json
@@ -0,0 +1,827 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "SonarAnalyzer.CSharp": {
+ "type": "Direct",
+ "requested": "[10.22.0.136894, )",
+ "resolved": "10.22.0.136894",
+ "contentHash": "6fI0XUWHvFIa/cvo1HuopV1Gh1hnKJq+XlTMJ2q71+6D3uVkl6Vxza3fFKQ9C4Bc7KFUFtukzRPmiH1be0JxOA=="
+ },
+ "Asp.Versioning.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.1.0",
+ "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Dapper.AOT": {
+ "type": "Transitive",
+ "resolved": "1.0.48",
+ "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA=="
+ },
+ "Hangfire.NetCore": {
+ "type": "Transitive",
+ "resolved": "1.8.23",
+ "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==",
+ "dependencies": {
+ "Hangfire.Core": "[1.8.23]",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
+ "Microsoft.Extensions.Hosting.Abstractions": "3.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "3.0.0"
+ }
+ },
+ "Humanizer.Core": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
+ },
+ "Microsoft.Bcl.TimeProvider": {
+ "type": "Transitive",
+ "resolved": "8.0.1",
+ "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA=="
+ },
+ "Microsoft.CodeAnalysis.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.11.0",
+ "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg=="
+ },
+ "Microsoft.CodeAnalysis.Common": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp.Workspaces": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.CSharp": "[5.0.0]",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]",
+ "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.Workspaces.Common": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.Workspaces.MSBuild": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.Build.Framework": "17.11.31",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]",
+ "Microsoft.Extensions.DependencyInjection": "9.0.0",
+ "Microsoft.Extensions.Logging": "9.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0",
+ "Microsoft.Extensions.Primitives": "9.0.0",
+ "Microsoft.VisualStudio.SolutionPersistence": "1.0.52",
+ "Newtonsoft.Json": "13.0.3",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11",
+ "Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Options": "8.0.2"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ=="
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "Microsoft.FeatureManagement": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==",
+ "dependencies": {
+ "Microsoft.Bcl.TimeProvider": "8.0.1",
+ "Microsoft.Extensions.Caching.Memory": "8.0.1",
+ "Microsoft.Extensions.Configuration": "8.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "8.0.2",
+ "Microsoft.Extensions.Logging": "8.0.1"
+ }
+ },
+ "Microsoft.VisualStudio.SolutionPersistence": {
+ "type": "Transitive",
+ "resolved": "1.0.52",
+ "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w=="
+ },
+ "Mono.TextTemplating": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==",
+ "dependencies": {
+ "System.CodeDom": "6.0.0"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.4",
+ "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
+ },
+ "Pipelines.Sockets.Unofficial": {
+ "type": "Transitive",
+ "resolved": "2.2.8",
+ "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
+ },
+ "Serilog.Extensions.Hosting": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0",
+ "Serilog": "4.3.0",
+ "Serilog.Extensions.Logging": "10.0.0"
+ }
+ },
+ "Serilog.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging": "10.0.0",
+ "Serilog": "4.2.0"
+ }
+ },
+ "Serilog.Formatting.Compact": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.Debug": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.File": {
+ "type": "Transitive",
+ "resolved": "7.0.0",
+ "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==",
+ "dependencies": {
+ "Serilog": "4.2.0"
+ }
+ },
+ "StackExchange.Redis": {
+ "type": "Transitive",
+ "resolved": "2.7.27",
+ "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
+ "Pipelines.Sockets.Unofficial": "2.2.8"
+ }
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
+ },
+ "System.Composition": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0",
+ "System.Composition.Convention": "9.0.0",
+ "System.Composition.Hosting": "9.0.0",
+ "System.Composition.Runtime": "9.0.0",
+ "System.Composition.TypedParts": "9.0.0"
+ }
+ },
+ "System.Composition.AttributedModel": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA=="
+ },
+ "System.Composition.Convention": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0"
+ }
+ },
+ "System.Composition.Hosting": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==",
+ "dependencies": {
+ "System.Composition.Runtime": "9.0.0"
+ }
+ },
+ "System.Composition.Runtime": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA=="
+ },
+ "System.Composition.TypedParts": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0",
+ "System.Composition.Hosting": "9.0.0",
+ "System.Composition.Runtime": "9.0.0"
+ }
+ },
+ "System.Threading.RateLimiting": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
+ },
+ "meajudaai.contracts": {
+ "type": "Project",
+ "dependencies": {
+ "FluentValidation": "[12.1.1, )"
+ }
+ },
+ "meajudaai.modules.communications.domain": {
+ "type": "Project",
+ "dependencies": {
+ "MeAjudaAi.Shared": "[1.0.0, )"
+ }
+ },
+ "meajudaai.shared": {
+ "type": "Project",
+ "dependencies": {
+ "Asp.Versioning.Mvc": "[8.1.1, )",
+ "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )",
+ "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )",
+ "AspNetCore.HealthChecks.Redis": "[9.0.0, )",
+ "Dapper": "[2.1.72, )",
+ "EFCore.NamingConventions": "[10.0.1, )",
+ "FluentValidation": "[12.1.1, )",
+ "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )",
+ "Hangfire.AspNetCore": "[1.8.23, )",
+ "Hangfire.Core": "[1.8.23, )",
+ "Hangfire.PostgreSql": "[1.21.1, )",
+ "MeAjudaAi.Contracts": "[1.0.0, )",
+ "Microsoft.AspNetCore.OpenApi": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )",
+ "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )",
+ "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )",
+ "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )",
+ "RabbitMQ.Client": "[7.2.1, )",
+ "Rebus": "[8.9.0, )",
+ "Rebus.RabbitMq": "[10.1.1, )",
+ "Rebus.ServiceProvider": "[10.7.2, )",
+ "Scrutor": "[7.0.0, )",
+ "Serilog": "[4.3.1, )",
+ "Serilog.AspNetCore": "[10.0.0, )",
+ "Serilog.Enrichers.Environment": "[3.0.1, )",
+ "Serilog.Enrichers.Process": "[3.0.0, )",
+ "Serilog.Enrichers.Thread": "[4.0.0, )",
+ "Serilog.Settings.Configuration": "[10.0.0, )",
+ "Serilog.Sinks.Console": "[6.1.1, )",
+ "Serilog.Sinks.Seq": "[9.0.0, )"
+ }
+ },
+ "Asp.Versioning.Http": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==",
+ "dependencies": {
+ "Asp.Versioning.Abstractions": "8.1.0"
+ }
+ },
+ "Asp.Versioning.Mvc": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==",
+ "dependencies": {
+ "Asp.Versioning.Http": "8.1.1"
+ }
+ },
+ "Asp.Versioning.Mvc.ApiExplorer": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==",
+ "dependencies": {
+ "Asp.Versioning.Mvc": "8.1.1"
+ }
+ },
+ "AspNetCore.HealthChecks.NpgSql": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "Npgsql": "8.0.3"
+ }
+ },
+ "AspNetCore.HealthChecks.Redis": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "StackExchange.Redis": "2.7.4"
+ }
+ },
+ "Dapper": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.72, )",
+ "resolved": "2.1.72",
+ "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw=="
+ },
+ "EFCore.NamingConventions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
+ }
+ },
+ "FluentValidation": {
+ "type": "CentralTransitive",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw=="
+ },
+ "FluentValidation.DependencyInjectionExtensions": {
+ "type": "CentralTransitive",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==",
+ "dependencies": {
+ "FluentValidation": "12.1.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0"
+ }
+ },
+ "Hangfire.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[1.8.23, )",
+ "resolved": "1.8.23",
+ "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==",
+ "dependencies": {
+ "Hangfire.NetCore": "[1.8.23]"
+ }
+ },
+ "Hangfire.Core": {
+ "type": "CentralTransitive",
+ "requested": "[1.8.23, )",
+ "resolved": "1.8.23",
+ "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==",
+ "dependencies": {
+ "Newtonsoft.Json": "11.0.1"
+ }
+ },
+ "Hangfire.PostgreSql": {
+ "type": "CentralTransitive",
+ "requested": "[1.21.1, )",
+ "resolved": "1.21.1",
+ "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==",
+ "dependencies": {
+ "Dapper": "2.0.123",
+ "Dapper.AOT": "1.0.48",
+ "Hangfire.Core": "1.8.0",
+ "Npgsql": "6.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.OpenApi": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==",
+ "dependencies": {
+ "Microsoft.OpenApi": "2.0.0"
+ }
+ },
+ "Microsoft.Build.Framework": {
+ "type": "CentralTransitive",
+ "requested": "[18.0.2, )",
+ "resolved": "18.0.2",
+ "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA=="
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Design": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.Build.Framework": "18.0.2",
+ "Microsoft.CodeAnalysis.CSharp": "5.0.0",
+ "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0",
+ "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Mono.TextTemplating": "3.0.0",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Hybrid": {
+ "type": "CentralTransitive",
+ "requested": "[10.4.0, )",
+ "resolved": "10.4.0",
+ "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Caching.Memory": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Caching.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "StackExchange.Redis": "2.7.27"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.FeatureManagement.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[4.4.0, )",
+ "resolved": "4.4.0",
+ "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==",
+ "dependencies": {
+ "Microsoft.FeatureManagement": "4.4.0"
+ }
+ },
+ "Microsoft.OpenApi": {
+ "type": "CentralTransitive",
+ "requested": "[2.7.0, )",
+ "resolved": "2.7.0",
+ "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA=="
+ },
+ "Npgsql": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.2, )",
+ "resolved": "10.0.2",
+ "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
+ }
+ },
+ "Npgsql.EntityFrameworkCore.PostgreSQL": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.2"
+ }
+ },
+ "RabbitMQ.Client": {
+ "type": "CentralTransitive",
+ "requested": "[7.2.1, )",
+ "resolved": "7.2.1",
+ "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==",
+ "dependencies": {
+ "System.Threading.RateLimiting": "8.0.0"
+ }
+ },
+ "Rebus": {
+ "type": "CentralTransitive",
+ "requested": "[8.9.0, )",
+ "resolved": "8.9.0",
+ "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==",
+ "dependencies": {
+ "Newtonsoft.Json": "13.0.4"
+ }
+ },
+ "Rebus.RabbitMq": {
+ "type": "CentralTransitive",
+ "requested": "[10.1.1, )",
+ "resolved": "10.1.1",
+ "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==",
+ "dependencies": {
+ "RabbitMq.Client": "7.1.2",
+ "rebus": "8.9.0"
+ }
+ },
+ "Rebus.ServiceProvider": {
+ "type": "CentralTransitive",
+ "requested": "[10.7.2, )",
+ "resolved": "10.7.2",
+ "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)",
+ "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)",
+ "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)",
+ "Rebus": "8.9.0"
+ }
+ },
+ "Scrutor": {
+ "type": "CentralTransitive",
+ "requested": "[7.0.0, )",
+ "resolved": "7.0.0",
+ "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.DependencyModel": "10.0.0"
+ }
+ },
+ "Serilog": {
+ "type": "CentralTransitive",
+ "requested": "[4.3.1, )",
+ "resolved": "4.3.1",
+ "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
+ },
+ "Serilog.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.0, )",
+ "resolved": "10.0.0",
+ "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==",
+ "dependencies": {
+ "Serilog": "4.3.0",
+ "Serilog.Extensions.Hosting": "10.0.0",
+ "Serilog.Formatting.Compact": "3.0.0",
+ "Serilog.Settings.Configuration": "10.0.0",
+ "Serilog.Sinks.Console": "6.1.1",
+ "Serilog.Sinks.Debug": "3.0.0",
+ "Serilog.Sinks.File": "7.0.0"
+ }
+ },
+ "Serilog.Enrichers.Environment": {
+ "type": "CentralTransitive",
+ "requested": "[3.0.1, )",
+ "resolved": "3.0.1",
+ "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Enrichers.Process": {
+ "type": "CentralTransitive",
+ "requested": "[3.0.0, )",
+ "resolved": "3.0.0",
+ "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Enrichers.Thread": {
+ "type": "CentralTransitive",
+ "requested": "[4.0.0, )",
+ "resolved": "4.0.0",
+ "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Settings.Configuration": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.0, )",
+ "resolved": "10.0.0",
+ "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyModel": "10.0.0",
+ "Serilog": "4.3.0"
+ }
+ },
+ "Serilog.Sinks.Console": {
+ "type": "CentralTransitive",
+ "requested": "[6.1.1, )",
+ "resolved": "6.1.1",
+ "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.Seq": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==",
+ "dependencies": {
+ "Serilog": "4.2.0",
+ "Serilog.Sinks.File": "6.0.0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Communications/Domain/Entities/CommunicationLog.cs b/src/Modules/Communications/Domain/Entities/CommunicationLog.cs
new file mode 100644
index 000000000..b4bae52fe
--- /dev/null
+++ b/src/Modules/Communications/Domain/Entities/CommunicationLog.cs
@@ -0,0 +1,114 @@
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Shared.Domain;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Entities;
+
+///
+/// Registro de auditoria de uma comunicação entregue (ou com falha definitiva).
+///
+///
+/// Utilizado para rastrear o histórico de comunicações enviadas, com suporte
+/// a idempotência via . Antes de criar um novo registro,
+/// o sistema verifica se já existe um com o mesmo CorrelationId para evitar duplicatas.
+///
+public sealed class CommunicationLog : BaseEntity
+{
+ private CommunicationLog() { }
+
+ ///
+ /// ID de correlação para garantir idempotência. Ex: "user_registered:{userId}".
+ ///
+ public string CorrelationId { get; private set; } = string.Empty;
+
+ ///
+ /// Canal pelo qual a comunicação foi enviada.
+ ///
+ public ECommunicationChannel Channel { get; private set; }
+
+ ///
+ /// Destinatário da comunicação (e-mail, número de telefone, etc.).
+ ///
+ public string Recipient { get; private set; } = string.Empty;
+
+ ///
+ /// Template key utilizado (quando aplicável).
+ ///
+ public string? TemplateKey { get; private set; }
+
+ ///
+ /// Indica se foi entregue com sucesso.
+ ///
+ public bool IsSuccess { get; private set; }
+
+ ///
+ /// Mensagem de erro (quando IsSuccess = false).
+ ///
+ public string? ErrorMessage { get; private set; }
+
+ ///
+ /// Número de tentativas realizadas até a entrega (ou falha definitiva).
+ ///
+ public int AttemptCount { get; private set; }
+
+ ///
+ /// ID da mensagem no Outbox (rastreabilidade).
+ ///
+ public Guid? OutboxMessageId { get; private set; }
+
+ ///
+ /// Cria um log de comunicação bem-sucedida.
+ ///
+ public static CommunicationLog CreateSuccess(
+ string correlationId,
+ ECommunicationChannel channel,
+ string recipient,
+ int attemptCount,
+ Guid? outboxMessageId = null,
+ string? templateKey = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(recipient);
+ ArgumentOutOfRangeException.ThrowIfNegative(attemptCount);
+
+ return new CommunicationLog
+ {
+ CorrelationId = correlationId,
+ Channel = channel,
+ Recipient = recipient,
+ TemplateKey = templateKey,
+ IsSuccess = true,
+ AttemptCount = attemptCount,
+ OutboxMessageId = outboxMessageId
+ };
+ }
+
+ ///
+ /// Cria um log de comunicação com falha.
+ ///
+ public static CommunicationLog CreateFailure(
+ string correlationId,
+ ECommunicationChannel channel,
+ string recipient,
+ string errorMessage,
+ int attemptCount,
+ Guid? outboxMessageId = null,
+ string? templateKey = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(recipient);
+ ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage);
+ ArgumentOutOfRangeException.ThrowIfNegative(attemptCount);
+
+ return new CommunicationLog
+ {
+ CorrelationId = correlationId,
+ Channel = channel,
+ Recipient = recipient,
+ TemplateKey = templateKey,
+ IsSuccess = false,
+ ErrorMessage = errorMessage,
+ AttemptCount = attemptCount,
+ OutboxMessageId = outboxMessageId
+ };
+ }
+}
diff --git a/src/Modules/Communications/Domain/Entities/EmailTemplate.cs b/src/Modules/Communications/Domain/Entities/EmailTemplate.cs
new file mode 100644
index 000000000..6b803bcc6
--- /dev/null
+++ b/src/Modules/Communications/Domain/Entities/EmailTemplate.cs
@@ -0,0 +1,138 @@
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Shared.Domain;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Entities;
+
+///
+/// Template de e-mail com suporte a override por contexto.
+///
+///
+/// Permite que administradores substituam templates padrão por versões customizadas
+/// sem alterar o código-fonte. O sistema verifica templates com
+/// antes de aplicar o template padrão.
+///
+public sealed class EmailTemplate : BaseEntity
+{
+ private EmailTemplate() { }
+
+ ///
+ /// Identificador único do template (snake_case). Ex: "user_registered", "provider_approved".
+ ///
+ public string TemplateKey { get; private set; } = string.Empty;
+
+ ///
+ /// Chave de override opcional. Permite substituição customizada sem alterar o padrão.
+ ///
+ public string? OverrideKey { get; private set; }
+
+ ///
+ /// Assunto do e-mail. Suporta tokens como {{FirstName}}.
+ ///
+ public string Subject { get; private set; } = string.Empty;
+
+ ///
+ /// Corpo HTML do e-mail. Suporta tokens como {{FirstName}}.
+ ///
+ public string HtmlBody { get; private set; } = string.Empty;
+
+ ///
+ /// Corpo em texto puro (fallback para clientes sem suporte a HTML).
+ ///
+ public string TextBody { get; private set; } = string.Empty;
+
+ ///
+ /// Indica se este template está ativo.
+ ///
+ public bool IsActive { get; private set; }
+
+ ///
+ /// Indica se este é um template de sistema (protegido contra deleção).
+ ///
+ public bool IsSystemTemplate { get; private set; }
+
+ ///
+ /// Idioma do template. Ex: "pt-BR", "en-US".
+ ///
+ public string Language { get; private set; } = "pt-BR";
+
+ ///
+ /// Versão do template (incrementada a cada update).
+ ///
+ public int Version { get; private set; }
+
+ ///
+ /// Cria um novo template de e-mail.
+ ///
+ public static EmailTemplate Create(
+ string templateKey,
+ string subject,
+ string htmlBody,
+ string textBody,
+ string language = "pt-BR",
+ string? overrideKey = null,
+ bool isSystemTemplate = false)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(templateKey);
+ ArgumentException.ThrowIfNullOrWhiteSpace(subject);
+ ArgumentException.ThrowIfNullOrWhiteSpace(htmlBody);
+ ArgumentException.ThrowIfNullOrWhiteSpace(textBody);
+ ArgumentException.ThrowIfNullOrWhiteSpace(language);
+
+ return new EmailTemplate
+ {
+ TemplateKey = templateKey.ToLowerInvariant().Trim(),
+ OverrideKey = string.IsNullOrWhiteSpace(overrideKey) ? null : overrideKey.ToLowerInvariant().Trim(),
+ Subject = subject,
+ HtmlBody = htmlBody,
+ TextBody = textBody,
+ Language = language.ToLowerInvariant().Trim(),
+ IsActive = true,
+ Version = 1,
+ IsSystemTemplate = isSystemTemplate
+ };
+ }
+
+ ///
+ /// Atualiza o conteúdo do template e incrementa a versão.
+ ///
+ public void UpdateContent(string subject, string htmlBody, string textBody)
+ {
+ if (IsSystemTemplate)
+ {
+ throw new InvalidOperationException("Não é possível alterar o conteúdo de um template do sistema.");
+ }
+
+ ArgumentException.ThrowIfNullOrWhiteSpace(subject);
+ ArgumentException.ThrowIfNullOrWhiteSpace(htmlBody);
+ ArgumentException.ThrowIfNullOrWhiteSpace(textBody);
+
+ Subject = subject;
+ HtmlBody = htmlBody;
+ TextBody = textBody;
+ Version++;
+ MarkAsUpdated();
+ }
+
+ ///
+ /// Desativa o template.
+ ///
+ public void Deactivate()
+ {
+ if (IsSystemTemplate)
+ {
+ throw new InvalidOperationException("Não é possível desativar um template do sistema.");
+ }
+
+ IsActive = false;
+ MarkAsUpdated();
+ }
+
+ ///
+ /// Ativa o template.
+ ///
+ public void Activate()
+ {
+ IsActive = true;
+ MarkAsUpdated();
+ }
+}
diff --git a/src/Modules/Communications/Domain/Entities/OutboxMessage.cs b/src/Modules/Communications/Domain/Entities/OutboxMessage.cs
new file mode 100644
index 000000000..4f5034a0b
--- /dev/null
+++ b/src/Modules/Communications/Domain/Entities/OutboxMessage.cs
@@ -0,0 +1,49 @@
+using MeAjudaAi.Modules.Communications.Domain.Enums;
+using MeAjudaAi.Shared.Database.Outbox;
+using MeAjudaAi.Contracts.Shared;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Entities;
+
+///
+/// Representa uma mensagem no Outbox específica do módulo de comunicações.
+/// Herda da base genérica para aproveitar lógica de processamento e retries.
+///
+public sealed class OutboxMessage : MeAjudaAi.Shared.Database.Outbox.OutboxMessage
+{
+ private OutboxMessage() { }
+
+ ///
+ /// Canal de comunicação: Email, Sms ou Push.
+ /// No banco, este campo é específico deste módulo.
+ ///
+ public ECommunicationChannel Channel { get; private set; }
+
+ ///
+ /// Cria uma nova mensagem no Outbox de comunicações.
+ ///
+ public static OutboxMessage Create(
+ ECommunicationChannel channel,
+ string payload,
+ ECommunicationPriority priority = ECommunicationPriority.Normal,
+ DateTime? scheduledAt = null,
+ int maxRetries = 3,
+ string? correlationId = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(payload);
+ if (maxRetries < 1)
+ throw new ArgumentOutOfRangeException(nameof(maxRetries), "MaxRetries must be at least 1.");
+
+ return new OutboxMessage
+ {
+ Channel = channel,
+ Type = channel.ToString(), // Mapeia o canal para o campo Type da base
+ Payload = payload,
+ Status = EOutboxMessageStatus.Pending,
+ Priority = priority,
+ RetryCount = 0,
+ MaxRetries = maxRetries,
+ ScheduledAt = scheduledAt,
+ CorrelationId = correlationId
+ };
+ }
+}
diff --git a/src/Modules/Communications/Domain/Enums/ECommunicationChannel.cs b/src/Modules/Communications/Domain/Enums/ECommunicationChannel.cs
new file mode 100644
index 000000000..94caafc7d
--- /dev/null
+++ b/src/Modules/Communications/Domain/Enums/ECommunicationChannel.cs
@@ -0,0 +1,11 @@
+namespace MeAjudaAi.Modules.Communications.Domain.Enums;
+
+///
+/// Canal de comunicação.
+///
+public enum ECommunicationChannel
+{
+ Email = 1,
+ Sms = 2,
+ Push = 3
+}
diff --git a/src/Modules/Communications/Domain/MeAjudaAi.Modules.Communications.Domain.csproj b/src/Modules/Communications/Domain/MeAjudaAi.Modules.Communications.Domain.csproj
new file mode 100644
index 000000000..4af60a596
--- /dev/null
+++ b/src/Modules/Communications/Domain/MeAjudaAi.Modules.Communications.Domain.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+ <_Parameter1>MeAjudaAi.Modules.Communications.Tests
+
+
+ <_Parameter1>MeAjudaAi.Integration.Tests
+
+
+
+
+
+
+
+
diff --git a/src/Modules/Communications/Domain/Repositories/ICommunicationLogRepository.cs b/src/Modules/Communications/Domain/Repositories/ICommunicationLogRepository.cs
new file mode 100644
index 000000000..31c8b7f73
--- /dev/null
+++ b/src/Modules/Communications/Domain/Repositories/ICommunicationLogRepository.cs
@@ -0,0 +1,45 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Repositories;
+
+///
+/// Repositório de logs de comunicação.
+///
+public interface ICommunicationLogRepository
+{
+ ///
+ /// Adiciona um novo log.
+ ///
+ Task AddAsync(CommunicationLog log, CancellationToken cancellationToken = default);
+
+ ///
+ /// Verifica se já existe um log com o CorrelationId especificado.
+ /// Usado para garantir idempotência.
+ ///
+ Task ExistsByCorrelationIdAsync(string correlationId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retorna o histórico de logs de um determinado destinatário.
+ ///
+ Task> GetByRecipientAsync(
+ string recipient,
+ int maxResults = 50,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Busca logs de comunicação de forma paginada.
+ ///
+ Task<(IReadOnlyList Items, int TotalCount)> SearchAsync(
+ string? correlationId = null,
+ string? channel = null,
+ string? recipient = null,
+ bool? isSuccess = null,
+ int pageNumber = 1,
+ int pageSize = 20,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Persiste as alterações no banco de dados.
+ ///
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/Repositories/IEmailTemplateRepository.cs b/src/Modules/Communications/Domain/Repositories/IEmailTemplateRepository.cs
new file mode 100644
index 000000000..48d204beb
--- /dev/null
+++ b/src/Modules/Communications/Domain/Repositories/IEmailTemplateRepository.cs
@@ -0,0 +1,40 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Repositories;
+
+///
+/// Repositório de templates de e-mail.
+///
+public interface IEmailTemplateRepository
+{
+ ///
+ /// Adiciona um novo template.
+ ///
+ Task AddAsync(EmailTemplate template, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retorna um template ativo pelo TemplateKey e Language.
+ /// Prefere OverrideKey quando disponível.
+ ///
+ Task GetActiveByKeyAsync(
+ string templateKey,
+ string language = "pt-BR",
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Retorna todos os templates de uma determinada chave.
+ ///
+ Task> GetAllByKeyAsync(
+ string templateKey,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Retorna todos os templates registrados.
+ ///
+ Task> GetAllAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Remove um template pelo ID (protegido se for template de sistema).
+ ///
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/Repositories/IOutboxMessageRepository.cs b/src/Modules/Communications/Domain/Repositories/IOutboxMessageRepository.cs
new file mode 100644
index 000000000..17ecf8c61
--- /dev/null
+++ b/src/Modules/Communications/Domain/Repositories/IOutboxMessageRepository.cs
@@ -0,0 +1,27 @@
+using MeAjudaAi.Shared.Database.Outbox;
+using MeAjudaAi.Contracts.Shared;
+using OutboxMessage = MeAjudaAi.Modules.Communications.Domain.Entities.OutboxMessage;
+
+namespace MeAjudaAi.Modules.Communications.Domain.Repositories;
+
+///
+/// Repositório para gestão de mensagens Outbox de comunicação.
+/// Herda da interface genérica para garantir consistência no processamento.
+///
+public interface IOutboxMessageRepository : IOutboxRepository
+{
+ ///
+ /// Retorna o total de mensagens por status (usado para monitoramento/health checks).
+ ///
+ Task CountByStatusAsync(EOutboxMessageStatus status, CancellationToken cancellationToken = default);
+
+ ///
+ /// Limpa mensagens muito antigas já enviadas para economizar espaço em disco.
+ ///
+ Task CleanupOldMessagesAsync(DateTime threshold, CancellationToken cancellationToken = default);
+
+ ///
+ /// Reseta mensagens travadas no status 'Processing' por muito tempo.
+ ///
+ Task ResetStaleProcessingMessagesAsync(DateTime threshold, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/Services/IEmailSender.cs b/src/Modules/Communications/Domain/Services/IEmailSender.cs
new file mode 100644
index 000000000..cd51731c3
--- /dev/null
+++ b/src/Modules/Communications/Domain/Services/IEmailSender.cs
@@ -0,0 +1,31 @@
+namespace MeAjudaAi.Modules.Communications.Domain.Services;
+
+///
+/// DTO que representa uma mensagem de e-mail a ser enviada.
+///
+/// Endereço de destino.
+/// Assunto do e-mail.
+/// Corpo HTML.
+/// Corpo em texto puro.
+/// Endereço de origem (null usa padrão configurado).
+public sealed record EmailMessage(
+ string To,
+ string Subject,
+ string HtmlBody,
+ string TextBody,
+ string? From = null
+);
+
+///
+/// Abstração do canal de envio de e-mail.
+///
+public interface IEmailSender
+{
+ ///
+ /// Envia um e-mail.
+ ///
+ /// Dados do e-mail a enviar.
+ /// Token de cancelamento.
+ /// True se enviado com sucesso.
+ Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/Services/IPushSender.cs b/src/Modules/Communications/Domain/Services/IPushSender.cs
new file mode 100644
index 000000000..026404dbe
--- /dev/null
+++ b/src/Modules/Communications/Domain/Services/IPushSender.cs
@@ -0,0 +1,29 @@
+namespace MeAjudaAi.Modules.Communications.Domain.Services;
+
+///
+/// DTO que representa uma notificação push a ser enviada.
+///
+/// Token do dispositivo destino.
+/// Título da notificação.
+/// Corpo da notificação.
+/// Dados adicionais opcionais (key-value pairs).
+public sealed record PushNotification(
+ string DeviceToken,
+ string Title,
+ string Body,
+ IDictionary? Data = null
+);
+
+///
+/// Abstração do canal de envio de notificações push.
+///
+public interface IPushSender
+{
+ ///
+ /// Envia uma notificação push.
+ ///
+ /// Dados da notificação a enviar.
+ /// Token de cancelamento.
+ /// True se enviada com sucesso.
+ Task SendAsync(PushNotification notification, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/Services/ISmsSender.cs b/src/Modules/Communications/Domain/Services/ISmsSender.cs
new file mode 100644
index 000000000..d47963c5a
--- /dev/null
+++ b/src/Modules/Communications/Domain/Services/ISmsSender.cs
@@ -0,0 +1,25 @@
+namespace MeAjudaAi.Modules.Communications.Domain.Services;
+
+///
+/// DTO que representa uma mensagem SMS a ser enviada.
+///
+/// Número de telefone no formato E.164 (+5511999999999).
+/// Texto da mensagem.
+public sealed record SmsMessage(
+ string PhoneNumber,
+ string Body
+);
+
+///
+/// Abstração do canal de envio de SMS.
+///
+public interface ISmsSender
+{
+ ///
+ /// Envia uma mensagem SMS.
+ ///
+ /// Dados do SMS a enviar.
+ /// Token de cancelamento.
+ /// True se enviado com sucesso.
+ Task SendAsync(SmsMessage message, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Communications/Domain/packages.lock.json b/src/Modules/Communications/Domain/packages.lock.json
new file mode 100644
index 000000000..09f7dbed4
--- /dev/null
+++ b/src/Modules/Communications/Domain/packages.lock.json
@@ -0,0 +1,821 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "SonarAnalyzer.CSharp": {
+ "type": "Direct",
+ "requested": "[10.22.0.136894, )",
+ "resolved": "10.22.0.136894",
+ "contentHash": "6fI0XUWHvFIa/cvo1HuopV1Gh1hnKJq+XlTMJ2q71+6D3uVkl6Vxza3fFKQ9C4Bc7KFUFtukzRPmiH1be0JxOA=="
+ },
+ "Asp.Versioning.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.1.0",
+ "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Dapper.AOT": {
+ "type": "Transitive",
+ "resolved": "1.0.48",
+ "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA=="
+ },
+ "Hangfire.NetCore": {
+ "type": "Transitive",
+ "resolved": "1.8.23",
+ "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==",
+ "dependencies": {
+ "Hangfire.Core": "[1.8.23]",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
+ "Microsoft.Extensions.Hosting.Abstractions": "3.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "3.0.0"
+ }
+ },
+ "Humanizer.Core": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
+ },
+ "Microsoft.Bcl.TimeProvider": {
+ "type": "Transitive",
+ "resolved": "8.0.1",
+ "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA=="
+ },
+ "Microsoft.CodeAnalysis.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.11.0",
+ "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg=="
+ },
+ "Microsoft.CodeAnalysis.Common": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp.Workspaces": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.CSharp": "[5.0.0]",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]",
+ "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.Workspaces.Common": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.CodeAnalysis.Workspaces.MSBuild": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.Build.Framework": "17.11.31",
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
+ "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]",
+ "Microsoft.Extensions.DependencyInjection": "9.0.0",
+ "Microsoft.Extensions.Logging": "9.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0",
+ "Microsoft.Extensions.Primitives": "9.0.0",
+ "Microsoft.VisualStudio.SolutionPersistence": "1.0.52",
+ "Newtonsoft.Json": "13.0.3",
+ "System.Composition": "9.0.0"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11",
+ "Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Options": "8.0.2"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ=="
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "Microsoft.FeatureManagement": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==",
+ "dependencies": {
+ "Microsoft.Bcl.TimeProvider": "8.0.1",
+ "Microsoft.Extensions.Caching.Memory": "8.0.1",
+ "Microsoft.Extensions.Configuration": "8.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "8.0.2",
+ "Microsoft.Extensions.Logging": "8.0.1"
+ }
+ },
+ "Microsoft.VisualStudio.SolutionPersistence": {
+ "type": "Transitive",
+ "resolved": "1.0.52",
+ "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w=="
+ },
+ "Mono.TextTemplating": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==",
+ "dependencies": {
+ "System.CodeDom": "6.0.0"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.4",
+ "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
+ },
+ "Pipelines.Sockets.Unofficial": {
+ "type": "Transitive",
+ "resolved": "2.2.8",
+ "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
+ },
+ "Serilog.Extensions.Hosting": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0",
+ "Serilog": "4.3.0",
+ "Serilog.Extensions.Logging": "10.0.0"
+ }
+ },
+ "Serilog.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging": "10.0.0",
+ "Serilog": "4.2.0"
+ }
+ },
+ "Serilog.Formatting.Compact": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.Debug": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.File": {
+ "type": "Transitive",
+ "resolved": "7.0.0",
+ "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==",
+ "dependencies": {
+ "Serilog": "4.2.0"
+ }
+ },
+ "StackExchange.Redis": {
+ "type": "Transitive",
+ "resolved": "2.7.27",
+ "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
+ "Pipelines.Sockets.Unofficial": "2.2.8"
+ }
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
+ },
+ "System.Composition": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0",
+ "System.Composition.Convention": "9.0.0",
+ "System.Composition.Hosting": "9.0.0",
+ "System.Composition.Runtime": "9.0.0",
+ "System.Composition.TypedParts": "9.0.0"
+ }
+ },
+ "System.Composition.AttributedModel": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA=="
+ },
+ "System.Composition.Convention": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0"
+ }
+ },
+ "System.Composition.Hosting": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==",
+ "dependencies": {
+ "System.Composition.Runtime": "9.0.0"
+ }
+ },
+ "System.Composition.Runtime": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA=="
+ },
+ "System.Composition.TypedParts": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==",
+ "dependencies": {
+ "System.Composition.AttributedModel": "9.0.0",
+ "System.Composition.Hosting": "9.0.0",
+ "System.Composition.Runtime": "9.0.0"
+ }
+ },
+ "System.Threading.RateLimiting": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
+ },
+ "meajudaai.contracts": {
+ "type": "Project",
+ "dependencies": {
+ "FluentValidation": "[12.1.1, )"
+ }
+ },
+ "meajudaai.shared": {
+ "type": "Project",
+ "dependencies": {
+ "Asp.Versioning.Mvc": "[8.1.1, )",
+ "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )",
+ "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )",
+ "AspNetCore.HealthChecks.Redis": "[9.0.0, )",
+ "Dapper": "[2.1.72, )",
+ "EFCore.NamingConventions": "[10.0.1, )",
+ "FluentValidation": "[12.1.1, )",
+ "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )",
+ "Hangfire.AspNetCore": "[1.8.23, )",
+ "Hangfire.Core": "[1.8.23, )",
+ "Hangfire.PostgreSql": "[1.21.1, )",
+ "MeAjudaAi.Contracts": "[1.0.0, )",
+ "Microsoft.AspNetCore.OpenApi": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )",
+ "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )",
+ "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )",
+ "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )",
+ "RabbitMQ.Client": "[7.2.1, )",
+ "Rebus": "[8.9.0, )",
+ "Rebus.RabbitMq": "[10.1.1, )",
+ "Rebus.ServiceProvider": "[10.7.2, )",
+ "Scrutor": "[7.0.0, )",
+ "Serilog": "[4.3.1, )",
+ "Serilog.AspNetCore": "[10.0.0, )",
+ "Serilog.Enrichers.Environment": "[3.0.1, )",
+ "Serilog.Enrichers.Process": "[3.0.0, )",
+ "Serilog.Enrichers.Thread": "[4.0.0, )",
+ "Serilog.Settings.Configuration": "[10.0.0, )",
+ "Serilog.Sinks.Console": "[6.1.1, )",
+ "Serilog.Sinks.Seq": "[9.0.0, )"
+ }
+ },
+ "Asp.Versioning.Http": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==",
+ "dependencies": {
+ "Asp.Versioning.Abstractions": "8.1.0"
+ }
+ },
+ "Asp.Versioning.Mvc": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==",
+ "dependencies": {
+ "Asp.Versioning.Http": "8.1.1"
+ }
+ },
+ "Asp.Versioning.Mvc.ApiExplorer": {
+ "type": "CentralTransitive",
+ "requested": "[8.1.1, )",
+ "resolved": "8.1.1",
+ "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==",
+ "dependencies": {
+ "Asp.Versioning.Mvc": "8.1.1"
+ }
+ },
+ "AspNetCore.HealthChecks.NpgSql": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "Npgsql": "8.0.3"
+ }
+ },
+ "AspNetCore.HealthChecks.Redis": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "StackExchange.Redis": "2.7.4"
+ }
+ },
+ "Dapper": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.72, )",
+ "resolved": "2.1.72",
+ "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw=="
+ },
+ "EFCore.NamingConventions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
+ }
+ },
+ "FluentValidation": {
+ "type": "CentralTransitive",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw=="
+ },
+ "FluentValidation.DependencyInjectionExtensions": {
+ "type": "CentralTransitive",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==",
+ "dependencies": {
+ "FluentValidation": "12.1.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0"
+ }
+ },
+ "Hangfire.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[1.8.23, )",
+ "resolved": "1.8.23",
+ "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==",
+ "dependencies": {
+ "Hangfire.NetCore": "[1.8.23]"
+ }
+ },
+ "Hangfire.Core": {
+ "type": "CentralTransitive",
+ "requested": "[1.8.23, )",
+ "resolved": "1.8.23",
+ "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==",
+ "dependencies": {
+ "Newtonsoft.Json": "11.0.1"
+ }
+ },
+ "Hangfire.PostgreSql": {
+ "type": "CentralTransitive",
+ "requested": "[1.21.1, )",
+ "resolved": "1.21.1",
+ "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==",
+ "dependencies": {
+ "Dapper": "2.0.123",
+ "Dapper.AOT": "1.0.48",
+ "Hangfire.Core": "1.8.0",
+ "Npgsql": "6.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.OpenApi": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==",
+ "dependencies": {
+ "Microsoft.OpenApi": "2.0.0"
+ }
+ },
+ "Microsoft.Build.Framework": {
+ "type": "CentralTransitive",
+ "requested": "[18.0.2, )",
+ "resolved": "18.0.2",
+ "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA=="
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Design": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==",
+ "dependencies": {
+ "Humanizer.Core": "2.14.1",
+ "Microsoft.Build.Framework": "18.0.2",
+ "Microsoft.CodeAnalysis.CSharp": "5.0.0",
+ "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0",
+ "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Mono.TextTemplating": "3.0.0",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Hybrid": {
+ "type": "CentralTransitive",
+ "requested": "[10.4.0, )",
+ "resolved": "10.4.0",
+ "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Caching.Memory": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Caching.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "StackExchange.Redis": "2.7.27"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.FeatureManagement.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[4.4.0, )",
+ "resolved": "4.4.0",
+ "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==",
+ "dependencies": {
+ "Microsoft.FeatureManagement": "4.4.0"
+ }
+ },
+ "Microsoft.OpenApi": {
+ "type": "CentralTransitive",
+ "requested": "[2.7.0, )",
+ "resolved": "2.7.0",
+ "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA=="
+ },
+ "Npgsql": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.2, )",
+ "resolved": "10.0.2",
+ "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
+ }
+ },
+ "Npgsql.EntityFrameworkCore.PostgreSQL": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.2"
+ }
+ },
+ "RabbitMQ.Client": {
+ "type": "CentralTransitive",
+ "requested": "[7.2.1, )",
+ "resolved": "7.2.1",
+ "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==",
+ "dependencies": {
+ "System.Threading.RateLimiting": "8.0.0"
+ }
+ },
+ "Rebus": {
+ "type": "CentralTransitive",
+ "requested": "[8.9.0, )",
+ "resolved": "8.9.0",
+ "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==",
+ "dependencies": {
+ "Newtonsoft.Json": "13.0.4"
+ }
+ },
+ "Rebus.RabbitMq": {
+ "type": "CentralTransitive",
+ "requested": "[10.1.1, )",
+ "resolved": "10.1.1",
+ "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==",
+ "dependencies": {
+ "RabbitMq.Client": "7.1.2",
+ "rebus": "8.9.0"
+ }
+ },
+ "Rebus.ServiceProvider": {
+ "type": "CentralTransitive",
+ "requested": "[10.7.2, )",
+ "resolved": "10.7.2",
+ "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)",
+ "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)",
+ "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)",
+ "Rebus": "8.9.0"
+ }
+ },
+ "Scrutor": {
+ "type": "CentralTransitive",
+ "requested": "[7.0.0, )",
+ "resolved": "7.0.0",
+ "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.DependencyModel": "10.0.0"
+ }
+ },
+ "Serilog": {
+ "type": "CentralTransitive",
+ "requested": "[4.3.1, )",
+ "resolved": "4.3.1",
+ "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ=="
+ },
+ "Serilog.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.0, )",
+ "resolved": "10.0.0",
+ "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==",
+ "dependencies": {
+ "Serilog": "4.3.0",
+ "Serilog.Extensions.Hosting": "10.0.0",
+ "Serilog.Formatting.Compact": "3.0.0",
+ "Serilog.Settings.Configuration": "10.0.0",
+ "Serilog.Sinks.Console": "6.1.1",
+ "Serilog.Sinks.Debug": "3.0.0",
+ "Serilog.Sinks.File": "7.0.0"
+ }
+ },
+ "Serilog.Enrichers.Environment": {
+ "type": "CentralTransitive",
+ "requested": "[3.0.1, )",
+ "resolved": "3.0.1",
+ "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Enrichers.Process": {
+ "type": "CentralTransitive",
+ "requested": "[3.0.0, )",
+ "resolved": "3.0.0",
+ "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Enrichers.Thread": {
+ "type": "CentralTransitive",
+ "requested": "[4.0.0, )",
+ "resolved": "4.0.0",
+ "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Settings.Configuration": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.0, )",
+ "resolved": "10.0.0",
+ "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyModel": "10.0.0",
+ "Serilog": "4.3.0"
+ }
+ },
+ "Serilog.Sinks.Console": {
+ "type": "CentralTransitive",
+ "requested": "[6.1.1, )",
+ "resolved": "6.1.1",
+ "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==",
+ "dependencies": {
+ "Serilog": "4.0.0"
+ }
+ },
+ "Serilog.Sinks.Seq": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==",
+ "dependencies": {
+ "Serilog": "4.2.0",
+ "Serilog.Sinks.File": "6.0.0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Communications/Infrastructure/Extensions.cs b/src/Modules/Communications/Infrastructure/Extensions.cs
new file mode 100644
index 000000000..dd687ca15
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Extensions.cs
@@ -0,0 +1,40 @@
+using MeAjudaAi.Modules.Communications.Domain.Repositories;
+using MeAjudaAi.Modules.Communications.Domain.Services;
+using MeAjudaAi.Modules.Communications.Infrastructure.Persistence;
+using MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Repositories;
+using MeAjudaAi.Modules.Communications.Infrastructure.Services;
+using MeAjudaAi.Shared.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure;
+
+public static class Extensions
+{
+ public static IServiceCollection AddCommunicationsInfrastructure(this IServiceCollection services, IConfiguration configuration)
+ {
+ // Persistência
+ services.AddPostgresContext(builder =>
+ {
+ builder.UseSnakeCaseNamingConvention();
+ });
+
+ // Repositórios
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ // Stubs de remetentes (ativados via feature flag para dev/testes)
+ // Por padrão, ativa se não houver configuração para evitar crash na injeção de dependência do OutboxProcessorService
+ if (configuration.GetValue("Communications:EnableStubs", true))
+ {
+ services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
+ }
+
+ return services;
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/MeAjudaAi.Modules.Communications.Infrastructure.csproj b/src/Modules/Communications/Infrastructure/MeAjudaAi.Modules.Communications.Infrastructure.csproj
new file mode 100644
index 000000000..b49e9996a
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/MeAjudaAi.Modules.Communications.Infrastructure.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+ <_Parameter1>MeAjudaAi.Modules.Communications.Tests
+
+
+ <_Parameter1>MeAjudaAi.Integration.Tests
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContext.cs b/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContext.cs
new file mode 100644
index 000000000..18c100bf2
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContext.cs
@@ -0,0 +1,55 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using MeAjudaAi.Shared.Database;
+using MeAjudaAi.Shared.Domain;
+using MeAjudaAi.Shared.Events;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence;
+
+///
+/// DbContext para o módulo de comunicações.
+///
+public sealed class CommunicationsDbContext : BaseDbContext
+{
+ public DbSet OutboxMessages => Set();
+ public DbSet EmailTemplates => Set();
+ public DbSet CommunicationLogs => Set();
+
+ public CommunicationsDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ public CommunicationsDbContext(DbContextOptions options, IDomainEventProcessor domainEventProcessor)
+ : base(options, domainEventProcessor)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema("communications");
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(CommunicationsDbContext).Assembly);
+ base.OnModelCreating(modelBuilder);
+ }
+
+ protected override Task> GetDomainEventsAsync(CancellationToken cancellationToken = default)
+ {
+ var domainEvents = ChangeTracker.Entries()
+ .Select(x => x.Entity)
+ .SelectMany(x => x.DomainEvents)
+ .ToList();
+
+ return Task.FromResult(domainEvents);
+ }
+
+ protected override void ClearDomainEvents()
+ {
+ var entities = ChangeTracker.Entries()
+ .Select(x => x.Entity)
+ .ToList();
+
+ foreach (var entity in entities)
+ {
+ entity.ClearDomainEvents();
+ }
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContextFactory.cs b/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContextFactory.cs
new file mode 100644
index 000000000..12a4a47af
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/CommunicationsDbContextFactory.cs
@@ -0,0 +1,15 @@
+using MeAjudaAi.Shared.Database;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence;
+
+///
+/// Factory para criação do DbContext em tempo de design (usado por dotnet ef migrations).
+///
+public class CommunicationsDbContextFactory : BaseDesignTimeDbContextFactory
+{
+ protected override CommunicationsDbContext CreateDbContextInstance(DbContextOptions options)
+ {
+ return new CommunicationsDbContext(options);
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Configurations/CommunicationLogConfiguration.cs b/src/Modules/Communications/Infrastructure/Persistence/Configurations/CommunicationLogConfiguration.cs
new file mode 100644
index 000000000..e2e7cbbcb
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Configurations/CommunicationLogConfiguration.cs
@@ -0,0 +1,38 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Configurations;
+
+internal sealed class CommunicationLogConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("communication_logs");
+
+ builder.HasKey(x => x.Id);
+
+ builder.Property(x => x.CorrelationId)
+ .HasMaxLength(200)
+ .IsRequired();
+
+ builder.Property(x => x.Channel)
+ .HasConversion()
+ .HasMaxLength(20)
+ .IsRequired();
+
+ builder.Property(x => x.Recipient)
+ .HasMaxLength(255)
+ .IsRequired();
+
+ builder.Property(x => x.TemplateKey)
+ .HasMaxLength(100);
+
+ builder.Property(x => x.ErrorMessage)
+ .HasMaxLength(2000);
+
+ builder.HasIndex(x => x.CorrelationId).IsUnique();
+ builder.HasIndex(x => x.Recipient);
+ builder.HasIndex(x => x.CreatedAt);
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Configurations/EmailTemplateConfiguration.cs b/src/Modules/Communications/Infrastructure/Persistence/Configurations/EmailTemplateConfiguration.cs
new file mode 100644
index 000000000..83c72a1bf
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Configurations/EmailTemplateConfiguration.cs
@@ -0,0 +1,41 @@
+using MeAjudaAi.Modules.Communications.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Configurations;
+
+internal sealed class EmailTemplateConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("email_templates");
+
+ builder.HasKey(x => x.Id);
+
+ builder.Property(x => x.TemplateKey)
+ .HasMaxLength(100)
+ .IsRequired();
+
+ builder.Property(x => x.OverrideKey)
+ .HasMaxLength(100);
+
+ builder.Property(x => x.Subject)
+ .HasMaxLength(255)
+ .IsRequired();
+
+ builder.Property(x => x.HtmlBody)
+ .IsRequired();
+
+ builder.Property(x => x.TextBody)
+ .IsRequired();
+
+ builder.Property(x => x.Language)
+ .HasMaxLength(10)
+ .IsRequired()
+ .HasDefaultValue("pt-BR");
+
+ builder.HasIndex(x => new { x.TemplateKey, x.Language, x.OverrideKey })
+ .IsUnique()
+ .AreNullsDistinct(false);
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Configurations/OutboxMessageConfiguration.cs b/src/Modules/Communications/Infrastructure/Persistence/Configurations/OutboxMessageConfiguration.cs
new file mode 100644
index 000000000..7482a1123
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Configurations/OutboxMessageConfiguration.cs
@@ -0,0 +1,52 @@
+using OutboxMessage = MeAjudaAi.Modules.Communications.Domain.Entities.OutboxMessage;
+using MeAjudaAi.Shared.Database.Outbox;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Configurations;
+
+internal sealed class OutboxMessageConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("outbox_messages");
+
+ builder.HasKey(x => x.Id);
+
+ builder.Property(x => x.Channel)
+ .HasConversion()
+ .HasMaxLength(20)
+ .IsRequired();
+
+ builder.Property(x => x.Type)
+ .HasMaxLength(100)
+ .IsRequired();
+
+ builder.Property(x => x.CorrelationId)
+ .HasMaxLength(200);
+
+ builder.Property(x => x.Payload)
+ .HasColumnType("jsonb")
+ .IsRequired();
+
+ builder.Property(x => x.Status)
+ .HasConversion()
+ .HasMaxLength(20)
+ .IsRequired();
+
+ builder.Property(x => x.Priority)
+ .HasConversion()
+ .HasMaxLength(20)
+ .IsRequired();
+
+ builder.Property(x => x.ErrorMessage)
+ .HasMaxLength(2000);
+
+ builder.HasIndex(x => x.CorrelationId)
+ .HasDatabaseName(OutboxMessageConstraints.CorrelationIdIndexName)
+ .IsUnique()
+ .HasFilter("\"correlation_id\" IS NOT Null");
+
+ builder.HasIndex(x => new { x.Status, x.ScheduledAt, x.Priority, x.CreatedAt });
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.Designer.cs b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.Designer.cs
new file mode 100644
index 000000000..5a1e00ccd
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.Designer.cs
@@ -0,0 +1,201 @@
+//
+using System;
+using MeAjudaAi.Modules.Communications.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(CommunicationsDbContext))]
+ [Migration("20260409000512_InitialCommunications")]
+ partial class InitialCommunications
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("communications")
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.CommunicationLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AttemptCount")
+ .HasColumnType("integer");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("IsSuccess")
+ .HasColumnType("boolean");
+
+ b.Property("OutboxMessageId")
+ .HasColumnType("uuid");
+
+ b.Property("Recipient")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("TemplateKey")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CorrelationId")
+ .IsUnique();
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("Recipient");
+
+ b.ToTable("communication_logs", "communications");
+ });
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.EmailTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("HtmlBody")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("IsSystemTemplate")
+ .HasColumnType("boolean");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("pt-BR");
+
+ b.Property("OverrideKey")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("TemplateKey")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TextBody")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TemplateKey", "Language", "OverrideKey")
+ .IsUnique();
+
+ b.ToTable("email_templates", "communications");
+ });
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.OutboxMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("MaxRetries")
+ .HasColumnType("integer");
+
+ b.Property("Payload")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("Priority")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RetryCount")
+ .HasColumnType("integer");
+
+ b.Property("ScheduledAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("SentAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ScheduledAt");
+
+ b.HasIndex("Status", "Priority", "CreatedAt");
+
+ b.ToTable("outbox_messages", "communications");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.cs b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.cs
new file mode 100644
index 000000000..f29c046c7
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409000512_InitialCommunications.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Migrations
+{
+ ///
+ public partial class InitialCommunications : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "communications");
+
+ migrationBuilder.CreateTable(
+ name: "communication_logs",
+ schema: "communications",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ CorrelationId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Channel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Recipient = table.Column(type: "character varying(255)", maxLength: 255, nullable: false),
+ TemplateKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ IsSuccess = table.Column(type: "boolean", nullable: false),
+ ErrorMessage = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ AttemptCount = table.Column(type: "integer", nullable: false),
+ OutboxMessageId = table.Column(type: "uuid", nullable: true),
+ CreatedAt = table.Column(type: "timestamp without time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_communication_logs", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "email_templates",
+ schema: "communications",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ TemplateKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ OverrideKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ Subject = table.Column(type: "character varying(255)", maxLength: 255, nullable: false),
+ HtmlBody = table.Column(type: "text", nullable: false),
+ TextBody = table.Column(type: "text", nullable: false),
+ IsActive = table.Column(type: "boolean", nullable: false),
+ IsSystemTemplate = table.Column(type: "boolean", nullable: false),
+ Language = table.Column(type: "character varying(10)", maxLength: 10, nullable: false, defaultValue: "pt-BR"),
+ Version = table.Column(type: "integer", nullable: false),
+ CreatedAt = table.Column(type: "timestamp without time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_email_templates", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "outbox_messages",
+ schema: "communications",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Channel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Payload = table.Column(type: "jsonb", nullable: false),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Priority = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ RetryCount = table.Column(type: "integer", nullable: false),
+ MaxRetries = table.Column(type: "integer", nullable: false),
+ ScheduledAt = table.Column(type: "timestamp without time zone", nullable: true),
+ SentAt = table.Column(type: "timestamp without time zone", nullable: true),
+ ErrorMessage = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ CreatedAt = table.Column(type: "timestamp without time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_outbox_messages", x => x.Id);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_communication_logs_CorrelationId",
+ schema: "communications",
+ table: "communication_logs",
+ column: "CorrelationId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_communication_logs_CreatedAt",
+ schema: "communications",
+ table: "communication_logs",
+ column: "CreatedAt");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_communication_logs_Recipient",
+ schema: "communications",
+ table: "communication_logs",
+ column: "Recipient");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_email_templates_TemplateKey_Language_OverrideKey",
+ schema: "communications",
+ table: "email_templates",
+ columns: new[] { "TemplateKey", "Language", "OverrideKey" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_outbox_messages_ScheduledAt",
+ schema: "communications",
+ table: "outbox_messages",
+ column: "ScheduledAt");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_outbox_messages_Status_Priority_CreatedAt",
+ schema: "communications",
+ table: "outbox_messages",
+ columns: new[] { "Status", "Priority", "CreatedAt" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "communication_logs",
+ schema: "communications");
+
+ migrationBuilder.DropTable(
+ name: "email_templates",
+ schema: "communications");
+
+ migrationBuilder.DropTable(
+ name: "outbox_messages",
+ schema: "communications");
+ }
+ }
+}
diff --git a/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409004631_AddCorrelationIdToOutbox.Designer.cs b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409004631_AddCorrelationIdToOutbox.Designer.cs
new file mode 100644
index 000000000..f6e6eefca
--- /dev/null
+++ b/src/Modules/Communications/Infrastructure/Persistence/Migrations/20260409004631_AddCorrelationIdToOutbox.Designer.cs
@@ -0,0 +1,209 @@
+//
+using System;
+using MeAjudaAi.Modules.Communications.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Communications.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(CommunicationsDbContext))]
+ [Migration("20260409004631_AddCorrelationIdToOutbox")]
+ partial class AddCorrelationIdToOutbox
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("communications")
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.CommunicationLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AttemptCount")
+ .HasColumnType("integer");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("IsSuccess")
+ .HasColumnType("boolean");
+
+ b.Property("OutboxMessageId")
+ .HasColumnType("uuid");
+
+ b.Property("Recipient")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("TemplateKey")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CorrelationId")
+ .IsUnique();
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("Recipient");
+
+ b.ToTable("communication_logs", "communications");
+ });
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.EmailTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("HtmlBody")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("IsSystemTemplate")
+ .HasColumnType("boolean");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("pt-BR");
+
+ b.Property("OverrideKey")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("TemplateKey")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TextBody")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TemplateKey", "Language", "OverrideKey")
+ .IsUnique();
+
+ NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TemplateKey", "Language", "OverrideKey"), false);
+
+ b.ToTable("email_templates", "communications");
+ });
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Communications.Domain.Entities.OutboxMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CorrelationId")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property