refactor: rename domain types and introduce TransactionBehavior pattern
- Rename Template -> EmailTemplate, Provider -> EmailChannel, ProviderSettings -> EmailChannelSettings, ProviderType -> EmailChannelType, ProviderUsage -> EmailChannelUsage throughout all layers - Add Undefined = 0 to EmailChannelType enum for safe default handling - Remove SaveChangesAsync from EfRepository methods — repositories now only stage changes - Add SaveChangesAsync to IUnitOfWork and EfUnitOfWork - Add TransactionBehavior MediatR pipeline: wraps every handler in a transaction, saves and commits on success, rolls back on exception - Add MediatR package reference to Services project Ref: IT-628 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ namespace HrynCo.NotificationService.DAL.Abstract;
|
|||||||
|
|
||||||
public interface IUnitOfWork
|
public interface IUnitOfWork
|
||||||
{
|
{
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
|
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
|
||||||
ITransaction? GetCurrentTransaction();
|
ITransaction? GetCurrentTransaction();
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,12 +2,12 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
|
||||||
public class Provider : Entity
|
public class EmailChannel : Entity
|
||||||
{
|
{
|
||||||
public required string ServiceName { get; set; }
|
public required string ServiceName { get; set; }
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
public ProviderType ProviderType { get; set; }
|
public EmailChannelType EmailChannelType { get; set; }
|
||||||
public required ProviderSettings Settings { get; set; }
|
public required EmailChannelSettings Settings { get; set; }
|
||||||
public int? DailyLimit { get; set; }
|
public int? DailyLimit { get; set; }
|
||||||
public int? MonthlyLimit { get; set; }
|
public int? MonthlyLimit { get; set; }
|
||||||
public int WarnThresholdPercent { get; set; } = 90;
|
public int WarnThresholdPercent { get; set; } = 90;
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
|
||||||
public abstract class ProviderSettings
|
public abstract class EmailChannelSettings
|
||||||
{
|
{
|
||||||
public abstract ProviderType ProviderType { get; }
|
public abstract EmailChannelType EmailChannelType { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SmtpProviderSettings : ProviderSettings
|
public class SmtpChannelSettings : EmailChannelSettings
|
||||||
{
|
{
|
||||||
public override ProviderType ProviderType => ProviderType.Smtp;
|
public override EmailChannelType EmailChannelType => EmailChannelType.Smtp;
|
||||||
|
|
||||||
public required string Host { get; set; }
|
public required string Host { get; set; }
|
||||||
public int Port { get; set; } = 587;
|
public int Port { get; set; } = 587;
|
||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
|
||||||
public enum ProviderType
|
public enum EmailChannelType
|
||||||
{
|
{
|
||||||
|
Undefined = 0,
|
||||||
Smtp = 1
|
Smtp = 1
|
||||||
}
|
}
|
||||||
+2
-2
@@ -3,10 +3,10 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
|
|||||||
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
namespace HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks email send counts per provider per calendar day.
|
/// Tracks email send counts per EmailChannel per calendar day.
|
||||||
/// Monthly counts are derived by summing daily records within a month.
|
/// Monthly counts are derived by summing daily records within a month.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProviderUsage : Entity
|
public class EmailChannelUsage : Entity
|
||||||
{
|
{
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||||
|
|
||||||
|
public interface IEmailChannelRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
|
||||||
|
Task<EmailChannel?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||||
|
Task AddAsync(EmailChannel channel, CancellationToken ct = default);
|
||||||
|
Task UpdateAsync(EmailChannel channel, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(EmailChannel channel, CancellationToken ct = default);
|
||||||
|
}
|
||||||
+2
-2
@@ -2,9 +2,9 @@ using HrynCo.NotificationService.DAL.Abstract.Providers;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||||
|
|
||||||
public interface IProviderUsageRepository
|
public interface IEmailChannelUsageRepository
|
||||||
{
|
{
|
||||||
Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default);
|
Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default);
|
||||||
Task<int> GetMonthlyCountAsync(Guid providerId, int year, int month, CancellationToken ct = default);
|
Task<int> GetMonthlyCountAsync(Guid providerId, int year, int month, CancellationToken ct = default);
|
||||||
Task IncrementAsync(Guid providerId, DateOnly date, CancellationToken ct = default);
|
Task IncrementUsageAsync(Guid providerId, DateOnly date, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||||
|
|
||||||
|
public interface IEmailEmailTemplateRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<EmailTemplate>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
|
||||||
|
Task<EmailTemplate?> GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default);
|
||||||
|
Task AddAsync(EmailTemplate EmailTemplate, CancellationToken ct = default);
|
||||||
|
Task UpdateAsync(EmailTemplate EmailTemplate, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(EmailTemplate EmailTemplate, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
|
||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
||||||
|
|
||||||
public interface IProviderRepository
|
|
||||||
{
|
|
||||||
Task<IReadOnlyList<Provider>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
|
|
||||||
Task<Provider?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
||||||
Task AddAsync(Provider provider, CancellationToken ct = default);
|
|
||||||
Task UpdateAsync(Provider provider, CancellationToken ct = default);
|
|
||||||
Task DeleteAsync(Provider provider, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
||||||
|
|
||||||
public interface ITemplateRepository
|
|
||||||
{
|
|
||||||
Task<IReadOnlyList<Template>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
|
|
||||||
Task<Template?> GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default);
|
|
||||||
Task AddAsync(Template template, CancellationToken ct = default);
|
|
||||||
Task UpdateAsync(Template template, CancellationToken ct = default);
|
|
||||||
Task DeleteAsync(Template template, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
+2
-2
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.Abstract.Templates;
|
namespace HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||||
|
|
||||||
public class Template : Entity
|
public class EmailTemplate : Entity
|
||||||
{
|
{
|
||||||
public required string ServiceName { get; set; }
|
public required string ServiceName { get; set; }
|
||||||
public required string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
@@ -10,5 +10,5 @@ public class Template : Entity
|
|||||||
public required string Subject { get; set; }
|
public required string Subject { get; set; }
|
||||||
public required string HtmlBody { get; set; }
|
public required string HtmlBody { get; set; }
|
||||||
public required string TextBody { get; set; }
|
public required string TextBody { get; set; }
|
||||||
public IReadOnlyList<TemplateVariable> Variables { get; set; } = [];
|
public IReadOnlyList<EmailTemplateVariable> Variables { get; set; } = [];
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace HrynCo.NotificationService.DAL.Abstract.Templates;
|
namespace HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||||
|
|
||||||
public record TemplateVariable
|
public record EmailTemplateVariable
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public bool Required { get; init; }
|
public bool Required { get; init; }
|
||||||
|
|||||||
+3
-3
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
||||||
|
|
||||||
internal class ProviderEntityConfiguration : IEntityTypeConfiguration<ProviderEntity>
|
internal class EmailChannelEntityConfiguration : IEntityTypeConfiguration<EmailChannelEntity>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<ProviderEntity> builder)
|
public void Configure(EntityTypeBuilder<EmailChannelEntity> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("providers");
|
builder.ToTable("providers");
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ internal class ProviderEntityConfiguration : IEntityTypeConfiguration<ProviderEn
|
|||||||
|
|
||||||
builder.Property(x => x.Priority).HasColumnName("priority");
|
builder.Property(x => x.Priority).HasColumnName("priority");
|
||||||
|
|
||||||
builder.Property(x => x.ProviderType).HasColumnName("provider_type");
|
builder.Property(x => x.EmailChannelType).HasColumnName("provider_type");
|
||||||
|
|
||||||
builder.Property(x => x.SettingsJson)
|
builder.Property(x => x.SettingsJson)
|
||||||
.HasColumnName("settings")
|
.HasColumnName("settings")
|
||||||
+2
-2
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
||||||
|
|
||||||
internal class ProviderUsageEntityConfiguration : IEntityTypeConfiguration<ProviderUsageEntity>
|
internal class EmailChannelUsageEntityConfiguration : IEntityTypeConfiguration<EmailChannelUsageEntity>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<ProviderUsageEntity> builder)
|
public void Configure(EntityTypeBuilder<EmailChannelUsageEntity> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("provider_usage");
|
builder.ToTable("provider_usage");
|
||||||
|
|
||||||
+2
-2
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
namespace HrynCo.NotificationService.DAL.EF.Configurations;
|
||||||
|
|
||||||
internal class TemplateEntityConfiguration : IEntityTypeConfiguration<TemplateEntity>
|
internal class EmailEmailTemplateEntityConfiguration : IEntityTypeConfiguration<EmailTemplateEntity>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<TemplateEntity> builder)
|
public void Configure(EntityTypeBuilder<EmailTemplateEntity> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("templates");
|
builder.ToTable("templates");
|
||||||
|
|
||||||
@@ -18,40 +18,28 @@ internal abstract class EfRepository<TEntity>
|
|||||||
protected async Task AddAsync(TEntity entity, CancellationToken ct = default)
|
protected async Task AddAsync(TEntity entity, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await DbSet.AddAsync(entity, ct);
|
await DbSet.AddAsync(entity, ct);
|
||||||
await DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await DbSet.AddRangeAsync(entities, ct);
|
await DbSet.AddRangeAsync(entities, ct);
|
||||||
await DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task UpdateAsync(TEntity entity, CancellationToken ct = default)
|
protected void Update(TEntity entity)
|
||||||
{
|
{
|
||||||
DbSet.Update(entity);
|
DbSet.Update(entity);
|
||||||
await DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task DeleteAsync(TEntity entity, CancellationToken ct = default)
|
protected void Delete(TEntity entity)
|
||||||
{
|
{
|
||||||
DbSet.Remove(entity);
|
DbSet.Remove(entity);
|
||||||
await DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
protected void DeleteRange(IEnumerable<TEntity> entities)
|
||||||
{
|
{
|
||||||
DbSet.RemoveRange(entities);
|
DbSet.RemoveRange(entities);
|
||||||
await DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default)
|
protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) =>
|
||||||
{
|
DbSet.AnyAsync(predicate, ct);
|
||||||
return DbSet.AnyAsync(predicate, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Task SaveAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
return DbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,11 @@ internal abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
|
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_currentTransaction != null)
|
if (_currentTransaction != null)
|
||||||
|
|||||||
+4
-4
@@ -3,15 +3,15 @@ using HrynCo.NotificationService.DAL.Abstract.Providers;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
||||||
|
|
||||||
internal class ProviderEntity : Entity
|
internal class EmailChannelEntity : Entity
|
||||||
{
|
{
|
||||||
public required string ServiceName { get; set; }
|
public required string ServiceName { get; set; }
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
public ProviderType ProviderType { get; set; }
|
public EmailChannelType EmailChannelType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provider-specific credentials and settings stored as JSONB.
|
/// EmailChannel-specific credentials and settings stored as JSONB.
|
||||||
/// Deserialized based on <see cref="ProviderType"/> in the repository.
|
/// Deserialized based on <see cref="EmailChannelType"/> in the repository.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string SettingsJson { get; set; }
|
public required string SettingsJson { get; set; }
|
||||||
|
|
||||||
+1
-1
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
||||||
|
|
||||||
internal class ProviderUsageEntity : Entity
|
internal class EmailChannelUsageEntity : Entity
|
||||||
{
|
{
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
+3
-3
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
namespace HrynCo.NotificationService.DAL.EF.Entities;
|
||||||
|
|
||||||
internal class TemplateEntity : Entity
|
internal class EmailTemplateEntity : Entity
|
||||||
{
|
{
|
||||||
public required string ServiceName { get; set; }
|
public required string ServiceName { get; set; }
|
||||||
public required string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
@@ -10,10 +10,10 @@ internal class TemplateEntity : Entity
|
|||||||
public required string Subject { get; set; }
|
public required string Subject { get; set; }
|
||||||
public required string HtmlBody { get; set; }
|
public required string HtmlBody { get; set; }
|
||||||
public required string TextBody { get; set; }
|
public required string TextBody { get; set; }
|
||||||
public List<TemplateVariableData> Variables { get; set; } = [];
|
public List<EmailTemplateVariableData> Variables { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class TemplateVariableData
|
internal class EmailTemplateVariableData
|
||||||
{
|
{
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
@@ -10,9 +10,9 @@ public class NotificationDbContext : DbContext
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal DbSet<TemplateEntity> Templates => Set<TemplateEntity>();
|
internal DbSet<EmailTemplateEntity> Templates => Set<EmailTemplateEntity>();
|
||||||
internal DbSet<ProviderEntity> Providers => Set<ProviderEntity>();
|
internal DbSet<EmailChannelEntity> Providers => Set<EmailChannelEntity>();
|
||||||
internal DbSet<ProviderUsageEntity> ProviderUsage => Set<ProviderUsageEntity>();
|
internal DbSet<EmailChannelUsageEntity> EmailChannelUsage => Set<EmailChannelUsageEntity>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||||
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||||
|
using HrynCo.NotificationService.DAL.EF.Core;
|
||||||
|
using HrynCo.NotificationService.DAL.EF.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
||||||
|
|
||||||
|
internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>, IEmailChannelRepository
|
||||||
|
{
|
||||||
|
public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var entities = await DbSet
|
||||||
|
.Where(x => x.ServiceName == serviceName)
|
||||||
|
.OrderBy(x => x.Priority)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return entities.Select(MapToDomain).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EmailChannel?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailChannelEntity? entity = await DbSet.FindAsync([id], ct);
|
||||||
|
return entity is null ? null : MapToDomain(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task AddAsync(EmailChannel channel, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return base.AddAsync(MapToEntity(channel), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateAsync(EmailChannel channel, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailChannelEntity entity = MapToEntity(channel);
|
||||||
|
entity.Updated = DateTimeOffset.UtcNow;
|
||||||
|
Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(EmailChannel channel, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailChannelEntity? entity = await DbSet.FindAsync([channel.Id], ct);
|
||||||
|
if (entity is not null)
|
||||||
|
{
|
||||||
|
Delete(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailChannel MapToDomain(EmailChannelEntity e)
|
||||||
|
{
|
||||||
|
return new EmailChannel
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
ServiceName = e.ServiceName,
|
||||||
|
Priority = e.Priority,
|
||||||
|
EmailChannelType = e.EmailChannelType,
|
||||||
|
Settings = DeserializeSettings(e.EmailChannelType, e.SettingsJson),
|
||||||
|
DailyLimit = e.DailyLimit,
|
||||||
|
MonthlyLimit = e.MonthlyLimit,
|
||||||
|
WarnThresholdPercent = e.WarnThresholdPercent,
|
||||||
|
IsActive = e.IsActive,
|
||||||
|
Created = e.Created,
|
||||||
|
Updated = e.Updated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailChannelEntity MapToEntity(EmailChannel p)
|
||||||
|
{
|
||||||
|
return new EmailChannelEntity
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
ServiceName = p.ServiceName,
|
||||||
|
Priority = p.Priority,
|
||||||
|
EmailChannelType = p.EmailChannelType,
|
||||||
|
SettingsJson = JsonSerializer.Serialize(p.Settings),
|
||||||
|
DailyLimit = p.DailyLimit,
|
||||||
|
MonthlyLimit = p.MonthlyLimit,
|
||||||
|
WarnThresholdPercent = p.WarnThresholdPercent,
|
||||||
|
IsActive = p.IsActive,
|
||||||
|
Created = p.Created,
|
||||||
|
Updated = p.Updated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailChannelSettings DeserializeSettings(EmailChannelType type, string json)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
EmailChannelType.Smtp => JsonSerializer.Deserialize<SmtpChannelSettings>(json)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Failed to deserialize SMTP EmailChannel settings."),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown or undefined email channel type: {type}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-14
@@ -5,15 +5,15 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
||||||
|
|
||||||
internal sealed class ProviderUsageRepository : EfRepository<ProviderUsageEntity>, IProviderUsageRepository
|
internal sealed class EmailChannelUsageRepository : EfRepository<EmailChannelUsageEntity>, IEmailChannelUsageRepository
|
||||||
{
|
{
|
||||||
public ProviderUsageRepository(NotificationDbContext dbContext) : base(dbContext)
|
public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default)
|
public async Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ProviderUsageEntity? entity = await DbSet
|
EmailChannelUsageEntity? entity = await DbSet
|
||||||
.FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct);
|
.FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct);
|
||||||
|
|
||||||
return entity?.SentCount ?? 0;
|
return entity?.SentCount ?? 0;
|
||||||
@@ -28,18 +28,17 @@ internal sealed class ProviderUsageRepository : EfRepository<ProviderUsageEntity
|
|||||||
.SumAsync(x => x.SentCount, ct);
|
.SumAsync(x => x.SentCount, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task IncrementAsync(Guid providerId, DateOnly date, CancellationToken ct = default)
|
public async Task IncrementUsageAsync(Guid providerId, DateOnly date, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
EmailChannelUsageEntity? entity = await DbSet
|
||||||
|
.FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct);
|
||||||
|
|
||||||
// Atomic upsert: insert with count=1 or increment existing count.
|
if (entity is null)
|
||||||
await DbContext.Database.ExecuteSqlAsync(
|
await AddAsync(new EmailChannelUsageEntity { ProviderId = providerId, Date = date, SentCount = 1 }, ct);
|
||||||
$"""
|
else
|
||||||
INSERT INTO provider_usage (id, provider_id, date, sent_count, created)
|
{
|
||||||
VALUES ({Guid.NewGuid()}, {providerId}, {date}, 1, {now})
|
entity.SentCount++;
|
||||||
ON CONFLICT (provider_id, date) DO UPDATE SET
|
Update(entity);
|
||||||
sent_count = provider_usage.sent_count + 1,
|
}
|
||||||
updated = {now}
|
|
||||||
""", ct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||||
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||||
|
using HrynCo.NotificationService.DAL.EF.Core;
|
||||||
|
using HrynCo.NotificationService.DAL.EF.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
||||||
|
|
||||||
|
internal sealed class EmailTemplateRepository : EfRepository<EmailTemplateEntity>, IEmailEmailTemplateRepository
|
||||||
|
{
|
||||||
|
public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<EmailTemplate>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
List<EmailTemplateEntity> entities = await DbSet
|
||||||
|
.Where(x => x.ServiceName == serviceName)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return entities.Select(MapToDomain).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EmailTemplate?> GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailTemplateEntity? entity = await DbSet.FirstOrDefaultAsync(
|
||||||
|
x => x.ServiceName == serviceName && x.Key == key && x.LanguageCode == languageCode, ct);
|
||||||
|
|
||||||
|
return entity is null ? null : MapToDomain(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task AddAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) =>
|
||||||
|
base.AddAsync(MapToEntity(EmailTemplate), ct);
|
||||||
|
|
||||||
|
public Task UpdateAsync(EmailTemplate EmailTemplate, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailTemplateEntity entity = MapToEntity(EmailTemplate);
|
||||||
|
entity.Updated = DateTimeOffset.UtcNow;
|
||||||
|
Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(EmailTemplate EmailTemplate, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EmailTemplateEntity? entity = await DbSet.FindAsync([EmailTemplate.Id], ct);
|
||||||
|
if (entity is not null)
|
||||||
|
Delete(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailTemplate MapToDomain(EmailTemplateEntity e) => new()
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
ServiceName = e.ServiceName,
|
||||||
|
Key = e.Key,
|
||||||
|
LanguageCode = e.LanguageCode,
|
||||||
|
Subject = e.Subject,
|
||||||
|
HtmlBody = e.HtmlBody,
|
||||||
|
TextBody = e.TextBody,
|
||||||
|
Variables = e.Variables.Select(v => new EmailTemplateVariable { Name = v.Name, Required = v.Required }).ToList(),
|
||||||
|
Created = e.Created,
|
||||||
|
Updated = e.Updated
|
||||||
|
};
|
||||||
|
|
||||||
|
private static EmailTemplateEntity MapToEntity(EmailTemplate t) => new()
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
ServiceName = t.ServiceName,
|
||||||
|
Key = t.Key,
|
||||||
|
LanguageCode = t.LanguageCode,
|
||||||
|
Subject = t.Subject,
|
||||||
|
HtmlBody = t.HtmlBody,
|
||||||
|
TextBody = t.TextBody,
|
||||||
|
Variables = t.Variables.Select(v => new EmailTemplateVariableData { Name = v.Name, Required = v.Required }).ToList(),
|
||||||
|
Created = t.Created,
|
||||||
|
Updated = t.Updated
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
|
||||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
||||||
using HrynCo.NotificationService.DAL.EF.Core;
|
|
||||||
using HrynCo.NotificationService.DAL.EF.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
|
||||||
|
|
||||||
internal sealed class ProviderRepository : EfRepository<ProviderEntity>, IProviderRepository
|
|
||||||
{
|
|
||||||
public ProviderRepository(NotificationDbContext dbContext) : base(dbContext)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Provider>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
List<ProviderEntity> entities = await DbSet
|
|
||||||
.Where(x => x.ServiceName == serviceName)
|
|
||||||
.OrderBy(x => x.Priority)
|
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
return entities.Select(MapToDomain).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Provider?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ProviderEntity? entity = await DbSet.FindAsync([id], ct);
|
|
||||||
return entity is null ? null : MapToDomain(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task AddAsync(Provider provider, CancellationToken ct = default) =>
|
|
||||||
base.AddAsync(MapToEntity(provider), ct);
|
|
||||||
|
|
||||||
public Task UpdateAsync(Provider provider, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ProviderEntity entity = MapToEntity(provider);
|
|
||||||
entity.Updated = DateTimeOffset.UtcNow;
|
|
||||||
return base.UpdateAsync(entity, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(Provider provider, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ProviderEntity? entity = await DbSet.FindAsync([provider.Id], ct);
|
|
||||||
if (entity is not null)
|
|
||||||
await base.DeleteAsync(entity, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Provider MapToDomain(ProviderEntity e) => new()
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
ServiceName = e.ServiceName,
|
|
||||||
Priority = e.Priority,
|
|
||||||
ProviderType = e.ProviderType,
|
|
||||||
Settings = DeserializeSettings(e.ProviderType, e.SettingsJson),
|
|
||||||
DailyLimit = e.DailyLimit,
|
|
||||||
MonthlyLimit = e.MonthlyLimit,
|
|
||||||
WarnThresholdPercent = e.WarnThresholdPercent,
|
|
||||||
IsActive = e.IsActive,
|
|
||||||
Created = e.Created,
|
|
||||||
Updated = e.Updated
|
|
||||||
};
|
|
||||||
|
|
||||||
private static ProviderEntity MapToEntity(Provider p) => new()
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
ServiceName = p.ServiceName,
|
|
||||||
Priority = p.Priority,
|
|
||||||
ProviderType = p.ProviderType,
|
|
||||||
SettingsJson = JsonSerializer.Serialize(p.Settings),
|
|
||||||
DailyLimit = p.DailyLimit,
|
|
||||||
MonthlyLimit = p.MonthlyLimit,
|
|
||||||
WarnThresholdPercent = p.WarnThresholdPercent,
|
|
||||||
IsActive = p.IsActive,
|
|
||||||
Created = p.Created,
|
|
||||||
Updated = p.Updated
|
|
||||||
};
|
|
||||||
|
|
||||||
private static ProviderSettings DeserializeSettings(ProviderType type, string json) => type switch
|
|
||||||
{
|
|
||||||
ProviderType.Smtp => JsonSerializer.Deserialize<SmtpProviderSettings>(json)
|
|
||||||
?? throw new InvalidOperationException("Failed to deserialize SMTP provider settings."),
|
|
||||||
_ => throw new InvalidOperationException($"Unknown provider type: {type}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
||||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
||||||
using HrynCo.NotificationService.DAL.EF.Core;
|
|
||||||
using HrynCo.NotificationService.DAL.EF.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
|
||||||
|
|
||||||
internal sealed class TemplateRepository : EfRepository<TemplateEntity>, ITemplateRepository
|
|
||||||
{
|
|
||||||
public TemplateRepository(NotificationDbContext dbContext) : base(dbContext)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Template>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
List<TemplateEntity> entities = await DbSet
|
|
||||||
.Where(x => x.ServiceName == serviceName)
|
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
return entities.Select(MapToDomain).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Template?> GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
TemplateEntity? entity = await DbSet.FirstOrDefaultAsync(
|
|
||||||
x => x.ServiceName == serviceName && x.Key == key && x.LanguageCode == languageCode, ct);
|
|
||||||
|
|
||||||
return entity is null ? null : MapToDomain(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task AddAsync(Template template, CancellationToken ct = default) =>
|
|
||||||
base.AddAsync(MapToEntity(template), ct);
|
|
||||||
|
|
||||||
public Task UpdateAsync(Template template, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
TemplateEntity entity = MapToEntity(template);
|
|
||||||
entity.Updated = DateTimeOffset.UtcNow;
|
|
||||||
return base.UpdateAsync(entity, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(Template template, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
TemplateEntity? entity = await DbSet.FindAsync([template.Id], ct);
|
|
||||||
if (entity is not null)
|
|
||||||
await base.DeleteAsync(entity, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Template MapToDomain(TemplateEntity e) => new()
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
ServiceName = e.ServiceName,
|
|
||||||
Key = e.Key,
|
|
||||||
LanguageCode = e.LanguageCode,
|
|
||||||
Subject = e.Subject,
|
|
||||||
HtmlBody = e.HtmlBody,
|
|
||||||
TextBody = e.TextBody,
|
|
||||||
Variables = e.Variables.Select(v => new TemplateVariable { Name = v.Name, Required = v.Required }).ToList(),
|
|
||||||
Created = e.Created,
|
|
||||||
Updated = e.Updated
|
|
||||||
};
|
|
||||||
|
|
||||||
private static TemplateEntity MapToEntity(Template t) => new()
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
ServiceName = t.ServiceName,
|
|
||||||
Key = t.Key,
|
|
||||||
LanguageCode = t.LanguageCode,
|
|
||||||
Subject = t.Subject,
|
|
||||||
HtmlBody = t.HtmlBody,
|
|
||||||
TextBody = t.TextBody,
|
|
||||||
Variables = t.Variables.Select(v => new TemplateVariableData { Name = v.Name, Required = v.Required }).ToList(),
|
|
||||||
Created = t.Created,
|
|
||||||
Updated = t.Updated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using HrynCo.NotificationService.DAL.Abstract;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.Services.Behaviors;
|
||||||
|
|
||||||
|
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : notnull
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public TransactionBehavior(IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _unitOfWork.ExecuteInTransactionAsync(async () =>
|
||||||
|
{
|
||||||
|
TResponse result = await next();
|
||||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MediatR" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\HrynCo.NotificationService.DAL.Abstract\HrynCo.NotificationService.DAL.Abstract.csproj" />
|
<ProjectReference Include="..\HrynCo.NotificationService.DAL.Abstract\HrynCo.NotificationService.DAL.Abstract.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user