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; 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) { // ====== Global Tenancy Config (meta schema) — ไม่สืบทอด BaseEntity ====== model.Entity(b => { b.ToTable("tenants", schema: "meta"); b.HasKey(x => x.TenantKey); 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); 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); 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); b.HasMany(x => x.Identities) .WithOne(i => i.User) .HasForeignKey(i => i.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.MfaFactors) .WithOne(i => i.User) .HasForeignKey(i => i.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Sessions) .WithOne(s => s.User) .HasForeignKey(s => s.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.PasswordHistories) .WithOne(ph => ph.User) .HasForeignKey(ph => ph.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.ExternalAccounts) .WithOne(ea => ea.User) .HasForeignKey(ea => ea.UserId) .OnDelete(DeleteBehavior.Cascade); }); 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"); }); 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 }); }); 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 }); }); 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 }); }); 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(); }); model.Entity(b => { b.ToTable("roles"); b.HasKey(x => x.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.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(); }); model.Entity(b => { b.ToTable("role_permissions"); b.HasKey(x => x.Id); b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); }); // ====== HR ====== model.Entity(b => { b.ToTable("user_profiles"); b.HasKey(x => x.Id); b.Property(x => x.FirstName).IsRequired().HasMaxLength(128); b.Property(x => x.LastName).IsRequired().HasMaxLength(128); b.HasOne(x => x.User) .WithOne() .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique(); }); model.Entity(b => { b.ToTable("departments"); b.HasKey(x => x.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 => x.ParentDepartmentId) .OnDelete(DeleteBehavior.Restrict); }); model.Entity(b => { b.ToTable("positions"); b.HasKey(x => x.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 => x.UserProfileId) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.Department) .WithMany() .HasForeignKey(x => x.DepartmentId) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.Position) .WithMany() .HasForeignKey(x => x.PositionId) .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 => x.UserProfileId) .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 => x.UserProfileId) .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 => x.UserProfileId) .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() .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); b.HasIndex(nameof(BaseEntity.TenantId)); b.HasCheckConstraint($"ck_{et.GetTableName()}_tenant_not_null", "tenant_id is not null"); // 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); } } }