using Indotalent.ConfigBackEnd.Extensions; using Indotalent.ConfigBackEnd.Interfaces; using Indotalent.Data.Abstracts; using Indotalent.Data.Entities; using Indotalent.Data.Interfaces; using Indotalent.Infrastructure.AutoNumberGenerator; using Indotalent.Shared.Consts; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Indotalent.Infrastructure.Database; public class AppDbContext : IdentityDbContext { private readonly IServiceScopeFactory _scopeFactory; private readonly ICurrentUserService _currentUserService; public AppDbContext( DbContextOptions options, IServiceScopeFactory scopeFactory, ICurrentUserService currentUserService) : base(options) { _scopeFactory = scopeFactory; _currentUserService = currentUserService; } public string CurrentTenantId => _currentUserService?.TenantId ?? "NO_TENANT_SELECTED"; public DbSet AutoNumberSequence { get; set; } = default!; public DbSet Currency { get; set; } = default!; public DbSet Company { get; set; } = default!; public DbSet Tax { get; set; } = default!; public DbSet Branch { get; set; } = default!; public DbSet Department { get; set; } = default!; public DbSet Designation { get; set; } = default!; public DbSet Employee { get; set; } = default!; public DbSet LeaveCategory { get; set; } = default!; public DbSet LeaveRequest { get; set; } = default!; public DbSet LeaveBalance { get; set; } = default!; public DbSet Evaluation { get; set; } = default!; public DbSet Appraisal { get; set; } = default!; public DbSet Promotion { get; set; } = default!; public DbSet Transfer { get; set; } = default!; public DbSet Income { get; set; } = default!; public DbSet Deduction { get; set; } = default!; public DbSet Grade { get; set; } = default!; public DbSet EmployeeIncome { get; set; } = default!; public DbSet EmployeeDeduction { get; set; } = default!; public DbSet PayrollProcess { get; set; } = default!; public DbSet PayrollDetail { get; set; } = default!; public DbSet PayrollComponent { get; set; } = default!; public DbSet Tenant { get; set; } = default!; public DbSet TenantUser { get; set; } = default!; public override int SaveChanges() { ApplySoftDelete(); ApplyAudit(); ApplyMultiTenant(); return base.SaveChanges(); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { ChangeTracker.DetectChanges(); ApplySoftDelete(); ApplyAudit(); ApplyMultiTenant(); await ApplyAutoNumberAsync(cancellationToken); return await base.SaveChangesAsync(cancellationToken); } private void ApplySoftDelete() { var entries = ChangeTracker.Entries() .Where(e => e.State == EntityState.Deleted) .ToList(); foreach (var entry in entries) { entry.State = EntityState.Modified; entry.Entity.IsDeleted = true; } } private void ApplyAudit() { var entries = ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified) .ToList(); if (!entries.Any()) return; var userId = _currentUserService.UserId ?? "SYSTEM"; if (string.IsNullOrEmpty(userId)) { throw new InvalidOperationException("UserId not found in the current user session context."); } foreach (var entry in entries) { var now = DateTimeOffset.UtcNow; var auditEntity = entry.Entity; if (entry.State == EntityState.Added) { auditEntity.CreatedAt = now; auditEntity.CreatedBy = userId; } else { entry.Property(nameof(IHasAudit.CreatedAt)).IsModified = false; entry.Property(nameof(IHasAudit.CreatedBy)).IsModified = false; } auditEntity.UpdatedAt = now; auditEntity.UpdatedBy = userId; } } private async Task ApplyAutoNumberAsync(CancellationToken ct) { var entries = ChangeTracker .Entries() .Where(e => e.State == EntityState.Added && string.IsNullOrEmpty(e.Entity.AutoNumber)) .ToList(); if (!entries.Any()) return; var currentTenantId = _currentUserService.TenantId ?? "SYSTEM_GLOBAL_TENANT"; if (string.IsNullOrWhiteSpace(currentTenantId)) { throw new InvalidOperationException("TenantId cannot be null or empty for multi-tenant auto-number sequence generation. Ensure the user session context is valid."); } using var scope = _scopeFactory.CreateScope(); var generator = scope.ServiceProvider.GetRequiredService(); foreach (var entry in entries) { var entityName = entry.Entity.GetType().Name; var shortName = entityName.ToShortNameConsonant(3); entry.Entity.AutoNumber = await generator.GenerateNextNumberAsync( tenantId: currentTenantId ?? string.Empty, entityName: entityName, prefixTemplate: $"{shortName}/{{Year}}/", paddingLength: 4, cancellationToken: ct ); } } private void ApplyMultiTenant() { var entries = ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified) .ToList(); if (!entries.Any()) return; var currentTenantId = _currentUserService.TenantId; if (string.IsNullOrEmpty(currentTenantId)) { throw new InvalidOperationException("TenantId not found in the current user session context."); } foreach (var entry in entries) { if (entry.State == EntityState.Added) { entry.Entity.TenantId = currentTenantId; } else if (entry.State == EntityState.Modified) { var dbTenantId = entry.Property(nameof(IMultiTenant.TenantId)).OriginalValue; if (dbTenantId != null) { if (entry.Entity.TenantId != dbTenantId.ToString() || entry.Entity.TenantId != currentTenantId) { throw new InvalidOperationException("Cross-tenant operation detected or TenantId data corruption attempted."); } } } } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); if (Database.ProviderName?.Contains("MySql") == true) { foreach (var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.GetTableName()!.StartsWith("AspNet")) { foreach (var property in entity.GetProperties().Where(p => p.ClrType == typeof(string))) { property.SetMaxLength(191); } } } } foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var type = entityType.ClrType; if (typeof(BaseEntity).IsAssignableFrom(type)) { modelBuilder.Entity(type) .Property("Id") .HasMaxLength(GlobalConsts.StringLengthId) .IsFixedLength(); } if (typeof(IHasAudit).IsAssignableFrom(type)) { modelBuilder.Entity(type) .Property(nameof(IHasAudit.CreatedBy)) .HasMaxLength(GlobalConsts.StringLengthShort); modelBuilder.Entity(type) .Property(nameof(IHasAudit.UpdatedBy)) .HasMaxLength(GlobalConsts.StringLengthShort); } if (typeof(IMultiTenant).IsAssignableFrom(type)) { modelBuilder.Entity(type) .Property(nameof(IMultiTenant.TenantId)) .HasMaxLength(GlobalConsts.StringLengthShort); } var hasDeleted = typeof(IHasIsDeleted).IsAssignableFrom(type); var isMultiTenant = typeof(IMultiTenant).IsAssignableFrom(type); if (hasDeleted || isMultiTenant) { modelBuilder.Entity(type).HasQueryFilter(GetGlobalFilters(type, hasDeleted, isMultiTenant)); } } modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); } private System.Linq.Expressions.LambdaExpression GetGlobalFilters(Type type, bool hasDeleted, bool isMultiTenant) { var parameter = System.Linq.Expressions.Expression.Parameter(type, "e"); System.Linq.Expressions.Expression? combinedBody = null; if (hasDeleted) { var property = System.Linq.Expressions.Expression.Property(parameter, nameof(IHasIsDeleted.IsDeleted)); var falseConstant = System.Linq.Expressions.Expression.Constant(false); combinedBody = System.Linq.Expressions.Expression.Equal(property, falseConstant); } if (isMultiTenant) { var tenantProperty = System.Linq.Expressions.Expression.Property(parameter, nameof(IMultiTenant.TenantId)); var dbContextInstance = System.Linq.Expressions.Expression.Constant(this); var currentTenantIdProperty = System.Linq.Expressions.Expression.Property(dbContextInstance, nameof(CurrentTenantId)); var tenantComparison = System.Linq.Expressions.Expression.Equal(tenantProperty, currentTenantIdProperty); combinedBody = combinedBody == null ? tenantComparison : System.Linq.Expressions.Expression.AndAlso(combinedBody, tenantComparison); } return System.Linq.Expressions.Expression.Lambda(combinedBody!, parameter); } }