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);
}
}