feat: add RabbitMQ worker, contracts, usage UI in channels screen

- Add HrynCo.NotificationService.Contracts project with SendEmailMessage and NotificationResultMessage
- Add SendEmailConsumer (RabbitMQ worker) with reply-to pattern via CorrelationContext.ReplyTo
- Add SendEmailHandler owning SMTP send + usage increment as business logic
- Add GetChannelUsageSummaryHandler with single DB query via navigation property
- Merge usage stats inline into channels list (daily/monthly with progress bars)
- Refactor AdminChannelsController.Index to use GetChannelUsageSummaryQuery
- Add RabbitMQ service to docker-compose files
- Remove dead AdminChannelUsageController, ChannelUsageViewModel, ChannelUsageSummary

Ref: IT-628

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Anatolii Grynchuk
2026-05-02 14:00:58 +03:00
parent 395f5573a1
commit b0996833bc
29 changed files with 569 additions and 78 deletions
@@ -35,5 +35,9 @@ internal class EmailChannelEntityConfiguration : IEntityTypeConfiguration<EmailC
builder.Property(x => x.IsActive).HasColumnName("is_active");
builder.Property(x => x.Created).HasColumnName("created");
builder.Property(x => x.Updated).HasColumnName("updated");
builder.HasMany(x => x.UsageRecords)
.WithOne()
.HasForeignKey(u => u.ProviderId);
}
}
@@ -19,4 +19,6 @@ internal class EmailChannelEntity : Entity
public int? MonthlyLimit { get; set; }
public int WarnThresholdPercent { get; set; }
public bool IsActive { get; set; }
public ICollection<EmailChannelUsageEntity> UsageRecords { get; set; } = [];
}
@@ -35,6 +35,30 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
return entities.Select(MapToDomain).ToList();
}
public async Task<IReadOnlyList<ChannelWithUsage>> GetAllWithUsageSummaryAsync(
DateOnly today, CancellationToken ct = default)
{
var rows = await DbSet
.AsNoTracking()
.OrderBy(c => c.ServiceName)
.ThenBy(c => c.Priority)
.Select(c => new
{
Channel = c,
DailySent = c.UsageRecords
.Where(u => u.Date == today)
.Sum(u => (int?)u.SentCount) ?? 0,
MonthlySent = c.UsageRecords
.Where(u => u.Date.Year == today.Year && u.Date.Month == today.Month)
.Sum(u => (int?)u.SentCount) ?? 0
})
.ToListAsync(ct);
return rows
.Select(r => new ChannelWithUsage(MapToDomain(r.Channel), r.DailySent, r.MonthlySent))
.ToList();
}
public async Task<EmailChannel?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
EmailChannelEntity? entity = await DbSet.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);