using AMREZ.EOP.Domain.Entities.Authentications; using AMREZ.EOP.Domain.Entities.Common; using AMREZ.EOP.Domain.Entities.HumanResources; using AMREZ.EOP.Domain.Entities.Tenancy; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; namespace AMREZ.EOP.Infrastructures.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } // ===== Auth ===== public DbSet Users => Set(); public DbSet UserIdentities => Set(); public DbSet UserMfaFactors => Set(); public DbSet UserSessions => Set(); public DbSet UserPasswordHistories => Set(); public DbSet UserExternalAccounts => Set(); public DbSet Roles => Set(); public DbSet Permissions => Set(); public DbSet UserRoles => Set(); public DbSet RolePermissions => Set(); // ===== HR ===== public DbSet UserProfiles => Set(); public DbSet Departments => Set(); public DbSet Positions => Set(); public DbSet Employments => Set(); public DbSet EmployeeAddresses => Set(); public DbSet EmergencyContacts => Set(); public DbSet EmployeeBankAccounts => Set(); // ===== Tenancy (meta) ===== public DbSet Tenants => Set(); public DbSet TenantDomains => Set(); protected override void OnModelCreating(ModelBuilder model) { // ====== Tenancy (meta) ====== model.Entity(b => { b.ToTable("tenants", schema: "meta"); b.HasKey(x => x.TenantKey); // PK = key (slug) b.HasAlternateKey(x => x.TenantId); // AK = GUID b.HasIndex(x => x.TenantId).IsUnique(); b.Property(x => x.TenantKey).HasMaxLength(128).IsRequired(); b.Property(x => x.Schema).HasMaxLength(128); b.Property(x => x.ConnectionString); b.Property(x => x.Mode).IsRequired(); b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.UpdatedAtUtc) .HasColumnName("updated_at_utc") .HasDefaultValueSql("now() at time zone 'utc'"); b.HasIndex(x => x.IsActive); }); model.Entity(b => { b.ToTable("tenant_domains", schema: "meta"); b.HasKey(x => x.Domain); b.Property(x => x.Domain).HasMaxLength(253).IsRequired(); b.Property(x => x.TenantKey).HasMaxLength(128); // optional b.Property(x => x.IsPlatformBaseDomain).HasDefaultValue(false); b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.UpdatedAtUtc) .HasColumnName("updated_at_utc") .HasDefaultValueSql("now() at time zone 'utc'"); b.HasIndex(x => x.TenantKey); b.HasIndex(x => x.IsPlatformBaseDomain); b.HasIndex(x => x.IsActive); b.HasOne() .WithMany() .HasForeignKey(x => x.TenantKey) .OnDelete(DeleteBehavior.Cascade); }); // ====== Auth ====== model.Entity(b => { b.ToTable("users"); b.HasKey(x => x.Id); // principal key สำหรับ composite FK จากลูก ๆ b.HasAlternateKey(u => new { u.TenantId, u.Id }); b.Property(x => x.PasswordHash).IsRequired(); b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.AccessFailedCount).HasDefaultValue(0); b.Property(x => x.MfaEnabled).HasDefaultValue(false); }); model.Entity(b => { b.ToTable("user_identities"); b.HasKey(x => x.Id); b.Property(x => x.Type).IsRequired(); b.Property(x => x.Identifier).IsRequired().HasMaxLength(256); b.Property(x => x.IsPrimary).HasDefaultValue(false); b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }).IsUnique(); b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary }) .HasDatabaseName("ix_user_identity_primary_per_type"); // (TenantId, UserId) -> User.(TenantId, Id) b.HasOne(i => i.User) .WithMany(u => u.Identities) .HasForeignKey(i => new { i.TenantId, i.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("user_mfa_factors"); b.HasKey(x => x.Id); b.Property(x => x.Type).IsRequired(); b.Property(x => x.Enabled).HasDefaultValue(true); b.HasIndex(x => new { x.TenantId, x.UserId }); b.HasOne(x => x.User) .WithMany(u => u.MfaFactors) .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("user_sessions"); b.HasKey(x => x.Id); b.Property(x => x.RefreshTokenHash).IsRequired(); b.HasIndex(x => new { x.TenantId, x.UserId }); b.HasIndex(x => new { x.TenantId, x.DeviceId }); b.HasOne(x => x.User) .WithMany(u => u.Sessions) .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("user_password_histories"); b.HasKey(x => x.Id); b.Property(x => x.PasswordHash).IsRequired(); b.HasIndex(x => new { x.TenantId, x.UserId, x.ChangedAt }); b.HasOne(x => x.User) .WithMany(u => u.PasswordHistories) .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("user_external_accounts"); b.HasKey(x => x.Id); b.Property(x => x.Provider).IsRequired(); b.Property(x => x.Subject).IsRequired(); b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject }).IsUnique(); b.HasOne(x => x.User) .WithMany(u => u.ExternalAccounts) .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("roles"); b.HasKey(x => x.Id); b.HasAlternateKey(r => new { r.TenantId, r.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(256); b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); }); model.Entity(b => { b.ToTable("permissions"); b.HasKey(x => x.Id); b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(256); b.Property(x => x.Name).IsRequired().HasMaxLength(256); b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); }); model.Entity(b => { b.ToTable("user_roles"); b.HasKey(x => x.Id); b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); b.HasOne() .WithMany() .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasOne() .WithMany() .HasForeignKey(x => new { x.TenantId, x.RoleId }) .HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id)) .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("role_permissions"); b.HasKey(x => x.Id); b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); b.HasOne() .WithMany() .HasForeignKey(x => new { x.TenantId, x.RoleId }) .HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasOne() .WithMany() .HasForeignKey(x => new { x.TenantId, x.PermissionId }) .HasPrincipalKey(nameof(Permission.TenantId), nameof(Permission.Id)) .OnDelete(DeleteBehavior.Cascade); }); // ====== HR ====== model.Entity(b => { b.ToTable("user_profiles"); b.HasKey(x => x.Id); b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.FirstName).IsRequired().HasMaxLength(128); b.Property(x => x.LastName).IsRequired().HasMaxLength(128); b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique(); b.HasOne(x => x.User) .WithOne() .HasForeignKey(x => new { x.TenantId, x.UserId }) .HasPrincipalKey(u => new { u.TenantId, u.Id }) // <-- เปลี่ยนตรงนี้ .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("departments"); b.HasKey(x => x.Id); b.HasAlternateKey(d => new { d.TenantId, d.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(64); b.Property(x => x.Name).IsRequired().HasMaxLength(256); b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); b.HasOne(x => x.Parent) .WithMany(x => x.Children) .HasForeignKey(x => new { x.TenantId, x.ParentDepartmentId }) .HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id)) .OnDelete(DeleteBehavior.Restrict); }); model.Entity(b => { b.ToTable("positions"); b.HasKey(x => x.Id); b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(64); b.Property(x => x.Title).IsRequired().HasMaxLength(256); b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); }); model.Entity(b => { b.ToTable("employments"); b.HasKey(x => x.Id); b.Property(x => x.EmploymentType).IsRequired(); b.Property(x => x.StartDate).IsRequired(); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.StartDate }); b.HasOne(x => x.UserProfile) .WithMany(p => p.Employments) .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.Department) .WithMany() .HasForeignKey(x => new { x.TenantId, x.DepartmentId }) .HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id)) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.Position) .WithMany() .HasForeignKey(x => new { x.TenantId, x.PositionId }) .HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id)) .OnDelete(DeleteBehavior.Restrict); }); model.Entity(b => { b.ToTable("employee_addresses"); b.HasKey(x => x.Id); b.Property(x => x.Line1).IsRequired().HasMaxLength(256); b.Property(x => x.City).IsRequired().HasMaxLength(128); b.Property(x => x.PostalCode).IsRequired().HasMaxLength(32); b.Property(x => x.Country).IsRequired().HasMaxLength(64); b.HasOne(x => x.UserProfile) .WithMany(p => p.Addresses) .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); }); model.Entity(b => { b.ToTable("emergency_contacts"); b.HasKey(x => x.Id); b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.Property(x => x.Relationship).IsRequired().HasMaxLength(64); b.HasOne(x => x.UserProfile) .WithMany(p => p.EmergencyContacts) .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); }); model.Entity(b => { b.ToTable("employee_bank_accounts"); b.HasKey(x => x.Id); b.Property(x => x.BankName).IsRequired().HasMaxLength(128); b.Property(x => x.AccountNumber).IsRequired().HasMaxLength(64); b.Property(x => x.AccountHolder).IsRequired().HasMaxLength(128); b.HasOne(x => x.UserProfile) .WithMany(p => p.BankAccounts) .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); }); // ====== Enums as ints ====== model.Entity().Property(x => x.Type).HasConversion(); model.Entity().Property(x => x.Type).HasConversion(); model.Entity().Property(x => x.Provider).HasConversion(); model.Entity().Property(x => x.EmploymentType).HasConversion(); model.Entity().Property(x => x.Gender).HasConversion(); // ====== BaseEntity common mapping ====== foreach (var et in model.Model.GetEntityTypes() .Where(t => typeof(BaseEntity).IsAssignableFrom(t.ClrType))) { var b = model.Entity(et.ClrType); // Tenant b.Property(nameof(BaseEntity.TenantId)) .HasColumnName("tenant_id") .HasColumnType("uuid") .IsRequired() .ValueGeneratedNever(); b.HasIndex(nameof(BaseEntity.TenantId)); // ชื่อ constraint สร้างแบบ concat แทน string interpolation กัน ambiguous handler var tn = et.GetTableName(); if (!string.IsNullOrEmpty(tn)) { b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_null"), "tenant_id is not null"); b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_zero"), "tenant_id <> '00000000-0000-0000-0000-000000000000'"); } // Audit b.Property("CreatedAt") .HasColumnName("created_at") .HasDefaultValueSql("now() at time zone 'utc'"); b.Property("UpdatedAt").HasColumnName("updated_at"); b.Property("CreatedBy").HasColumnName("created_by"); b.Property("UpdatedBy").HasColumnName("updated_by"); b.Property("IsDeleted").HasColumnName("is_deleted").HasDefaultValue(false); } } }