using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.EF.Entities; using Microsoft.EntityFrameworkCore; namespace HrynCo.NotificationService.DAL.EF.Repositories; internal sealed class PushTemplateDefinitionRepository(NotificationDbContext db, TemplateMutationRunner mutations) : IPushTemplateDefinitionRepository { private IQueryable Definitions => db.PushTemplateDefinitions .Include(d => d.Languages).ThenInclude(l => l.ServiceLanguage); public async Task> GetDefinitionsAsync(Guid serviceId, CancellationToken ct = default) => (await Definitions.AsNoTracking().Where(d => d.ServiceId == serviceId).OrderBy(d => d.Key).ToListAsync(ct)).Select(Map).ToList(); public async Task GetAsync(Guid serviceId, string key, CancellationToken ct = default) { var entity = await Definitions.AsNoTracking().SingleOrDefaultAsync(d => d.ServiceId == serviceId && d.Key == key, ct); return entity is null ? null : Map(entity); } public Task SaveAsync(PushTemplateDefinition definition, bool isNew, CancellationToken ct = default) => mutations.ExecuteAsync(async () => { PushTemplateValidation.Validate(definition); var service = await db.Services.Include(s => s.Languages).SingleOrDefaultAsync(s => s.Id == definition.ServiceId, ct) ?? throw new EmailTemplateWriteException("Service no longer exists. Reload services."); if (definition.Languages.Any(l => !service.Languages.Any(s => s.Code == l.LanguageCode))) throw new EmailTemplateWriteException("Every language must exactly match this service's catalog.", false); var entity = await LoadForSaveAsync(definition, service, isNew, ct); entity.Variables = definition.Variables.Select(v => new PushTemplateVariableData { Name = v.Name, Required = v.Required }).ToList(); SynchronizeLanguages(entity, service, definition.Languages); entity.Revision = Guid.NewGuid(); entity.Updated = DateTimeOffset.UtcNow; definition.Id = entity.Id; definition.Revision = entity.Revision; }, ct); private async Task LoadForSaveAsync(PushTemplateDefinition model, ServiceEntity service, bool isNew, CancellationToken ct) { var entity = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == service.Id && d.Key == model.Key, ct); if (!isNew) { if (entity is null || entity.Id != model.Id || entity.Revision != model.Revision) throw new EmailTemplateWriteException("This push template changed after you opened it. Reload before saving; your entered values are retained."); return entity; } if (entity is not null) throw new EmailTemplateWriteException("A push template with this key already exists."); var template = await db.TemplateCatalog.SingleOrDefaultAsync(t => t.ServiceId == service.Id && t.Key == model.Key, ct) ?? new TemplateEntity { Service = service, ServiceId = service.Id, Key = model.Key, Created = DateTimeOffset.UtcNow }; entity = new PushTemplateDefinitionEntity { Template = template, ServiceId = service.Id, Key = model.Key, Created = DateTimeOffset.UtcNow }; db.PushTemplateDefinitions.Add(entity); return entity; } private void SynchronizeLanguages(PushTemplateDefinitionEntity definition, ServiceEntity service, IReadOnlyList translations) { var codes = translations.Select(l => l.LanguageCode).ToHashSet(StringComparer.Ordinal); db.PushTemplates.RemoveRange(definition.Languages.Where(l => !codes.Contains(l.ServiceLanguage.Code))); foreach (var translation in translations) { var entity = definition.Languages.SingleOrDefault(l => l.ServiceLanguage.Code == translation.LanguageCode); if (entity is null) { entity = new PushTemplateEntity { Definition = definition, ServiceId = service.Id, ServiceLanguage = service.Languages.Single(l => l.Code == translation.LanguageCode), Title = translation.Title, Body = translation.Body, Created = DateTimeOffset.UtcNow }; definition.Languages.Add(entity); } entity.Title = translation.Title; entity.Body = translation.Body; entity.ActionsJson = translation.ActionsJson; entity.Updated = DateTimeOffset.UtcNow; } } public async Task DeleteAsync(Guid serviceId, string key, Guid revision, CancellationToken ct = default) { var removed = false; await mutations.ExecuteAsync(async () => { var entity = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == serviceId && d.Key == key, ct); if (entity is null) return; if (revision == Guid.Empty || entity.Revision != revision) throw new EmailTemplateWriteException("The push template revision changed. Reload before deleting."); db.PushTemplateDefinitions.Remove(entity); if (!await db.TemplateDefinitions.AnyAsync(d => d.ServiceId == serviceId && d.Key == key, ct)) db.TemplateCatalog.Remove(await db.TemplateCatalog.SingleAsync(t => t.ServiceId == serviceId && t.Key == key, ct)); removed = true; }, ct); return removed; } private static PushTemplateDefinition Map(PushTemplateDefinitionEntity entity) => new() { Id = entity.Id, ServiceId = entity.ServiceId, Key = entity.Key, Revision = entity.Revision, Variables = entity.Variables.Select(v => new EmailTemplateVariable { Name = v.Name, Required = v.Required }).ToList(), Languages = entity.Languages.OrderBy(l => l.ServiceLanguage.Code).Select(l => new PushTemplateTranslation { LanguageCode = l.ServiceLanguage.Code, Title = l.Title, Body = l.Body, ActionsJson = l.ActionsJson }).ToList() }; }