494 lines
17 KiB
C#
494 lines
17 KiB
C#
using Zentral.Domain.Entities;
|
|
using Zentral.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
namespace Zentral.API.Services;
|
|
|
|
/// <summary>
|
|
/// Service per la gestione dei moduli applicativi e delle relative subscription
|
|
/// </summary>
|
|
public class ModuleService
|
|
{
|
|
private readonly ZentralDbContext _context;
|
|
private readonly IMemoryCache _cache;
|
|
private readonly ILogger<ModuleService> _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<ModuleService> logger)
|
|
{
|
|
_context = context;
|
|
_cache = cache;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene tutti i moduli con lo stato della subscription
|
|
/// </summary>
|
|
public async Task<List<AppModule>> 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<AppModule>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene solo i moduli attivi (per la costruzione del menu)
|
|
/// </summary>
|
|
public async Task<List<AppModule>> 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?.IsEnabled ?? false) && (m.Subscription?.IsValid() ?? false))).ToList();
|
|
}) ?? new List<AppModule>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene un modulo specifico per codice
|
|
/// </summary>
|
|
public async Task<AppModule?> GetModuleByCodeAsync(string code)
|
|
{
|
|
return await _context.AppModules
|
|
.Include(m => m.Subscription)
|
|
.FirstOrDefaultAsync(m => m.Code == code);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se un modulo è attualmente abilitato
|
|
/// </summary>
|
|
public async Task<bool> 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?.IsEnabled ?? false) && (module.Subscription?.IsValid() ?? false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se un modulo ha una subscription valida (non scaduta)
|
|
/// </summary>
|
|
public async Task<bool> HasValidSubscriptionAsync(string code)
|
|
{
|
|
var module = await GetModuleByCodeAsync(code);
|
|
return module?.Subscription?.IsValid() ?? false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attiva un modulo creando o aggiornando la subscription
|
|
/// </summary>
|
|
public async Task<ModuleSubscription> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disattiva un modulo (mantiene i dati ma rimuove l'accesso)
|
|
/// </summary>
|
|
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?.IsEnabled ?? false) && (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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggiorna i dettagli della subscription
|
|
/// </summary>
|
|
public async Task<ModuleSubscription> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rinnova una subscription esistente
|
|
/// </summary>
|
|
public async Task<ModuleSubscription> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene tutte le subscription
|
|
/// </summary>
|
|
public async Task<List<ModuleSubscription>> GetAllSubscriptionsAsync()
|
|
{
|
|
return await _context.ModuleSubscriptions
|
|
.Include(s => s.Module)
|
|
.OrderBy(s => s.Module.SortOrder)
|
|
.ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica e disattiva i moduli con subscription scaduta (per job schedulato)
|
|
/// </summary>
|
|
public async Task<int> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene i moduli in scadenza entro N giorni
|
|
/// </summary>
|
|
public async Task<List<AppModule>> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica le dipendenze mancanti per un modulo
|
|
/// </summary>
|
|
private async Task<List<string>> CheckDependenciesAsync(AppModule module)
|
|
{
|
|
var dependencies = module.GetDependencies().ToList();
|
|
if (!dependencies.Any())
|
|
return new List<string>();
|
|
|
|
var missingDeps = new List<string>();
|
|
|
|
foreach (var depCode in dependencies)
|
|
{
|
|
if (!await IsModuleEnabledAsync(depCode))
|
|
{
|
|
var depModule = await GetModuleByCodeAsync(depCode);
|
|
missingDeps.Add(depModule?.Name ?? depCode);
|
|
}
|
|
}
|
|
|
|
return missingDeps;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene i moduli che dipendono da un determinato modulo
|
|
/// </summary>
|
|
private async Task<List<AppModule>> GetDependentModulesAsync(string code)
|
|
{
|
|
var allModules = await GetAllModulesAsync();
|
|
return allModules
|
|
.Where(m => m.GetDependencies().Contains(code))
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalida la cache dei moduli
|
|
/// </summary>
|
|
public void InvalidateCache()
|
|
{
|
|
_cache.Remove(MODULES_CACHE_KEY);
|
|
_cache.Remove(ACTIVE_MODULES_CACHE_KEY);
|
|
_logger.LogDebug("Cache moduli invalidata");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inizializza i moduli di default se non esistono
|
|
/// </summary>
|
|
public async Task SeedDefaultModulesAsync()
|
|
{
|
|
if (await _context.AppModules.AnyAsync())
|
|
return;
|
|
|
|
var defaultModules = new List<AppModule>
|
|
{
|
|
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);
|
|
}
|
|
}
|