using Zentral.Domain.Entities; using Zentral.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; namespace Zentral.API.Services; /// /// Service per la gestione dei moduli applicativi e delle relative subscription /// public class ModuleService { private readonly ZentralDbContext _context; private readonly IMemoryCache _cache; private readonly ILogger _logger; private const string MODULES_CACHE_KEY = "modules_all"; private const string ACTIVE_MODULES_CACHE_KEY = "modules_active"; private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); public ModuleService( ZentralDbContext context, IMemoryCache cache, ILogger logger) { _context = context; _cache = cache; _logger = logger; } /// /// Ottiene tutti i moduli con lo stato della subscription /// public async Task> GetAllModulesAsync() { return await _cache.GetOrCreateAsync(MODULES_CACHE_KEY, async entry => { entry.AbsoluteExpirationRelativeToNow = CacheDuration; return await _context.AppModules .Include(m => m.Subscription) .Where(m => m.IsAvailable) .OrderBy(m => m.SortOrder) .ThenBy(m => m.Name) .ToListAsync(); }) ?? new List(); } /// /// Ottiene solo i moduli attivi (per la costruzione del menu) /// public async Task> GetActiveModulesAsync() { return await _cache.GetOrCreateAsync(ACTIVE_MODULES_CACHE_KEY, async entry => { entry.AbsoluteExpirationRelativeToNow = CacheDuration; var modules = await _context.AppModules .Include(m => m.Subscription) .Where(m => m.IsAvailable) .OrderBy(m => m.SortOrder) .ThenBy(m => m.Name) .ToListAsync(); return modules.Where(m => m.IsCore || (m.Subscription?.IsValid() ?? false)).ToList(); }) ?? new List(); } /// /// Ottiene un modulo specifico per codice /// public async Task GetModuleByCodeAsync(string code) { return await _context.AppModules .Include(m => m.Subscription) .FirstOrDefaultAsync(m => m.Code == code); } /// /// Verifica se un modulo è attualmente abilitato /// public async Task IsModuleEnabledAsync(string code) { var module = await GetModuleByCodeAsync(code); if (module == null) return false; // I moduli core sono sempre abilitati if (module.IsCore) return true; return module.Subscription?.IsValid() ?? false; } /// /// Verifica se un modulo ha una subscription valida (non scaduta) /// public async Task HasValidSubscriptionAsync(string code) { var module = await GetModuleByCodeAsync(code); return module?.Subscription?.IsValid() ?? false; } /// /// Attiva un modulo creando o aggiornando la subscription /// public async Task EnableModuleAsync( string code, SubscriptionType subscriptionType, DateTime? startDate = null, DateTime? endDate = null, bool autoRenew = false, decimal? paidPrice = null, string? notes = null) { var module = await _context.AppModules .Include(m => m.Subscription) .FirstOrDefaultAsync(m => m.Code == code); if (module == null) throw new ArgumentException($"Modulo con codice '{code}' non trovato"); if (module.IsCore) throw new InvalidOperationException("I moduli core non possono essere attivati/disattivati manualmente"); // Verifica dipendenze var missingDeps = await CheckDependenciesAsync(module); if (missingDeps.Any()) throw new InvalidOperationException( $"Il modulo richiede i seguenti moduli attivi: {string.Join(", ", missingDeps)}"); var now = DateTime.UtcNow; var effectiveStartDate = startDate ?? now; // Calcola data fine se non specificata DateTime? effectiveEndDate = endDate; if (!effectiveEndDate.HasValue && subscriptionType != SubscriptionType.None) { effectiveEndDate = subscriptionType switch { SubscriptionType.Monthly => effectiveStartDate.AddMonths(1), SubscriptionType.Annual => effectiveStartDate.AddYears(1), _ => null }; } if (module.Subscription == null) { // Crea nuova subscription module.Subscription = new ModuleSubscription { ModuleId = module.Id, IsEnabled = true, SubscriptionType = subscriptionType, StartDate = effectiveStartDate, EndDate = effectiveEndDate, AutoRenew = autoRenew, PaidPrice = paidPrice ?? module.BasePrice, Notes = notes, CreatedAt = now, UpdatedAt = now }; _context.ModuleSubscriptions.Add(module.Subscription); } else { // Aggiorna subscription esistente module.Subscription.IsEnabled = true; module.Subscription.SubscriptionType = subscriptionType; module.Subscription.StartDate = effectiveStartDate; module.Subscription.EndDate = effectiveEndDate; module.Subscription.AutoRenew = autoRenew; module.Subscription.PaidPrice = paidPrice ?? module.Subscription.PaidPrice ?? module.BasePrice; if (notes != null) module.Subscription.Notes = notes; module.Subscription.UpdatedAt = now; } await _context.SaveChangesAsync(); InvalidateCache(); _logger.LogInformation( "Modulo {ModuleCode} attivato con subscription {Type} fino a {EndDate}", code, subscriptionType, effectiveEndDate); return module.Subscription; } /// /// Disattiva un modulo (mantiene i dati ma rimuove l'accesso) /// public async Task DisableModuleAsync(string code) { var module = await _context.AppModules .Include(m => m.Subscription) .FirstOrDefaultAsync(m => m.Code == code); if (module == null) throw new ArgumentException($"Modulo con codice '{code}' non trovato"); if (module.IsCore) throw new InvalidOperationException("I moduli core non possono essere disattivati"); // Verifica se altri moduli dipendono da questo var dependentModules = await GetDependentModulesAsync(code); var activeDependents = dependentModules.Where(m => m.Subscription?.IsValid() ?? false).ToList(); if (activeDependents.Any()) throw new InvalidOperationException( $"I seguenti moduli attivi dipendono da questo modulo: {string.Join(", ", activeDependents.Select(m => m.Name))}"); if (module.Subscription != null) { module.Subscription.IsEnabled = false; module.Subscription.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } InvalidateCache(); _logger.LogInformation("Modulo {ModuleCode} disattivato", code); } /// /// Aggiorna i dettagli della subscription /// public async Task UpdateSubscriptionAsync( string code, SubscriptionType? subscriptionType = null, DateTime? endDate = null, bool? autoRenew = null, string? notes = null) { var module = await _context.AppModules .Include(m => m.Subscription) .FirstOrDefaultAsync(m => m.Code == code); if (module == null) throw new ArgumentException($"Modulo con codice '{code}' non trovato"); if (module.Subscription == null) throw new InvalidOperationException($"Il modulo '{code}' non ha una subscription attiva"); if (subscriptionType.HasValue) module.Subscription.SubscriptionType = subscriptionType.Value; if (endDate.HasValue) module.Subscription.EndDate = endDate.Value; if (autoRenew.HasValue) module.Subscription.AutoRenew = autoRenew.Value; if (notes != null) module.Subscription.Notes = notes; module.Subscription.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); InvalidateCache(); return module.Subscription; } /// /// Rinnova una subscription esistente /// public async Task RenewSubscriptionAsync(string code, decimal? paidPrice = null) { var module = await _context.AppModules .Include(m => m.Subscription) .FirstOrDefaultAsync(m => m.Code == code); if (module == null) throw new ArgumentException($"Modulo con codice '{code}' non trovato"); if (module.Subscription == null) throw new InvalidOperationException($"Il modulo '{code}' non ha una subscription da rinnovare"); var now = DateTime.UtcNow; var currentEnd = module.Subscription.EndDate ?? now; var newStart = currentEnd > now ? currentEnd : now; var newEnd = module.Subscription.SubscriptionType switch { SubscriptionType.Monthly => newStart.AddMonths(1), SubscriptionType.Annual => newStart.AddYears(1), _ => newStart.AddYears(1) // Default to annual }; module.Subscription.StartDate = newStart; module.Subscription.EndDate = newEnd; module.Subscription.LastRenewalDate = now; module.Subscription.IsEnabled = true; module.Subscription.PaidPrice = paidPrice ?? module.Subscription.PaidPrice; module.Subscription.UpdatedAt = now; await _context.SaveChangesAsync(); InvalidateCache(); _logger.LogInformation( "Modulo {ModuleCode} rinnovato fino a {EndDate}", code, newEnd); return module.Subscription; } /// /// Ottiene tutte le subscription /// public async Task> GetAllSubscriptionsAsync() { return await _context.ModuleSubscriptions .Include(s => s.Module) .OrderBy(s => s.Module.SortOrder) .ToListAsync(); } /// /// Verifica e disattiva i moduli con subscription scaduta (per job schedulato) /// public async Task CheckExpiredSubscriptionsAsync() { var expiredSubscriptions = await _context.ModuleSubscriptions .Include(s => s.Module) .Where(s => s.IsEnabled && s.EndDate.HasValue && s.EndDate.Value < DateTime.UtcNow && !s.AutoRenew) .ToListAsync(); foreach (var subscription in expiredSubscriptions) { subscription.IsEnabled = false; subscription.UpdatedAt = DateTime.UtcNow; _logger.LogWarning( "Modulo {ModuleCode} disattivato per scadenza subscription", subscription.Module.Code); } if (expiredSubscriptions.Any()) { await _context.SaveChangesAsync(); InvalidateCache(); } return expiredSubscriptions.Count; } /// /// Ottiene i moduli in scadenza entro N giorni /// public async Task> GetExpiringModulesAsync(int daysThreshold = 30) { var thresholdDate = DateTime.UtcNow.AddDays(daysThreshold); return await _context.AppModules .Include(m => m.Subscription) .Where(m => m.Subscription != null && m.Subscription.IsEnabled && m.Subscription.EndDate.HasValue && m.Subscription.EndDate.Value <= thresholdDate && m.Subscription.EndDate.Value > DateTime.UtcNow) .OrderBy(m => m.Subscription!.EndDate) .ToListAsync(); } /// /// Verifica le dipendenze mancanti per un modulo /// private async Task> CheckDependenciesAsync(AppModule module) { var dependencies = module.GetDependencies().ToList(); if (!dependencies.Any()) return new List(); var missingDeps = new List(); foreach (var depCode in dependencies) { if (!await IsModuleEnabledAsync(depCode)) { var depModule = await GetModuleByCodeAsync(depCode); missingDeps.Add(depModule?.Name ?? depCode); } } return missingDeps; } /// /// Ottiene i moduli che dipendono da un determinato modulo /// private async Task> GetDependentModulesAsync(string code) { var allModules = await GetAllModulesAsync(); return allModules .Where(m => m.GetDependencies().Contains(code)) .ToList(); } /// /// Invalida la cache dei moduli /// public void InvalidateCache() { _cache.Remove(MODULES_CACHE_KEY); _cache.Remove(ACTIVE_MODULES_CACHE_KEY); _logger.LogDebug("Cache moduli invalidata"); } /// /// Inizializza i moduli di default se non esistono /// public async Task SeedDefaultModulesAsync() { if (await _context.AppModules.AnyAsync()) return; var defaultModules = new List { new AppModule { Code = "warehouse", Name = "Magazzino", Description = "Gestione inventario, movimenti di magazzino, giacenze e valorizzazione scorte", Icon = "Warehouse", BasePrice = 1200m, MonthlyMultiplier = 1.2m, SortOrder = 10, IsCore = false, RoutePath = "/warehouse", IsAvailable = true, CreatedAt = DateTime.UtcNow }, new AppModule { Code = "purchases", Name = "Acquisti", Description = "Gestione ordini fornitori, DDT in entrata, fatture passive e analisi acquisti", Icon = "ShoppingCart", BasePrice = 1500m, MonthlyMultiplier = 1.2m, SortOrder = 20, IsCore = false, Dependencies = "warehouse", RoutePath = "/purchases", IsAvailable = true, CreatedAt = DateTime.UtcNow }, new AppModule { Code = "sales", Name = "Vendite", Description = "Gestione ordini clienti, DDT in uscita, fatture attive e analisi vendite", Icon = "PointOfSale", BasePrice = 1500m, MonthlyMultiplier = 1.2m, SortOrder = 30, IsCore = false, Dependencies = "warehouse", RoutePath = "/sales", IsAvailable = true, CreatedAt = DateTime.UtcNow }, new AppModule { Code = "production", Name = "Produzione", Description = "Cicli produttivi, distinte base, pianificazione MRP e controllo avanzamento", Icon = "Precision Manufacturing", BasePrice = 2500m, MonthlyMultiplier = 1.2m, SortOrder = 40, IsCore = false, Dependencies = "warehouse", RoutePath = "/production", IsAvailable = true, CreatedAt = DateTime.UtcNow }, new AppModule { Code = "quality", Name = "Qualità", Description = "Controlli qualità, gestione non conformità, certificazioni e audit", Icon = "VerifiedUser", BasePrice = 1800m, MonthlyMultiplier = 1.2m, SortOrder = 50, IsCore = false, RoutePath = "/quality", IsAvailable = true, CreatedAt = DateTime.UtcNow } }; _context.AppModules.AddRange(defaultModules); await _context.SaveChangesAsync(); _logger.LogInformation("Seed {Count} moduli di default completato", defaultModules.Count); } }