using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Tiku.Domain.Catalog; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Persistence.Configurations; internal sealed class TenantAuthProviderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_auth_providers"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.DisplayName).HasMaxLength(200); builder.Property(entity => entity.ConfigPublic).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.Provider }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.Status }); } } internal sealed class TenantSecretConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_secrets"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Purpose).HasMaxLength(80); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.SecretKey).HasMaxLength(120); builder.Property(entity => entity.SecretRef).HasMaxLength(300); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.EncryptionKeyId).HasMaxLength(100); builder.HasIndex(entity => new { entity.TenantId, entity.SecretRef }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Purpose, entity.Provider, entity.SecretKey }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Purpose, entity.Provider, entity.Status }); builder.ToTable(table => table.HasCheckConstraint( "ck_tenant_secrets_encryption_envelope", "octet_length(encrypted_payload) > 0 and octet_length(encryption_nonce) = 12 and " + "octet_length(encryption_tag) = 16 and encryption_key_id <> ''")); } } internal sealed class SmsVerificationCodeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureEntity("sms_verification_codes"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); builder.Property(entity => entity.Phone).HasMaxLength(32); builder.Property(entity => entity.Purpose).HasSnakeCaseEnum(); builder.Property(entity => entity.CodeHash).HasMaxLength(256); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.IpAddress).HasMaxLength(64); builder.Property(entity => entity.UserAgent).HasMaxLength(1000); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose, entity.ExpiresAt }); builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose, entity.CreatedAt }) .HasFilter("consumed_at is null and status in ('pending', 'sent')"); builder.HasIndex(entity => new { entity.TenantId, entity.Phone, entity.Purpose }) .IsUnique() .HasFilter("consumed_at is null and status in ('pending', 'sent')"); builder.ToTable(table => { table.HasCheckConstraint("ck_sms_verification_codes_attempts", "attempts >= 0"); }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class AuthLoginEventConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureEntity("auth_login_events"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.Identifier).HasMaxLength(320); builder.Property(entity => entity.Result).HasSnakeCaseEnum(); builder.Property(entity => entity.FailureCode).HasMaxLength(100); builder.Property(entity => entity.IpAddress).HasMaxLength(64); builder.Property(entity => entity.UserAgent).HasMaxLength(1000); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.SetNull); } } internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("auth_sessions"); builder.ConfigureTimestamps(); builder.Property(entity => entity.TokenHash).HasMaxLength(256); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.IpAddress).HasMaxLength(64); builder.Property(entity => entity.UserAgent).HasMaxLength(1000); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => entity.TokenHash).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.ExpiresAt }) .HasFilter("revoked_at is null"); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class SmsSendRateLimitConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("sms_send_rate_limits"); builder.HasKey(entity => new { entity.TenantId, entity.Dimension, entity.ScopeHash, entity.BucketStart }); builder.Property(entity => entity.Dimension).HasSnakeCaseEnum(); builder.Property(entity => entity.ScopeHash).HasMaxLength(128); builder.Property(entity => entity.RequestCount).HasDefaultValue(0); builder.Property(entity => entity.UpdatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => entity.UpdatedAt); builder.ToTable(table => { table.HasCheckConstraint("ck_sms_send_rate_limits_request_count", "request_count >= 0"); }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class TenantRoleTemplateConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_role_templates"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Code).HasMaxLength(100); builder.Property(entity => entity.Name).HasMaxLength(200); builder.Property(entity => entity.BaseRole).HasSnakeCaseEnum(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.Permissions).IsJson("{}"); builder.Property(entity => entity.MenuPermissions).IsJson("{}"); builder.Property(entity => entity.ModulePermissions).IsJson("{}"); builder.Property(entity => entity.FieldPermissions).IsJson("{}"); builder.Property(entity => entity.DataScope).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.SortOrder }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.CreatedBy) .OnDelete(DeleteBehavior.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UpdatedBy) .OnDelete(DeleteBehavior.SetNull); } } internal sealed class TenantClassConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_classes"); builder.ConfigureTimestamps(); builder.Property(entity => entity.LegacyId).HasMaxLength(64); builder.Property(entity => entity.Code).HasMaxLength(100); builder.Property(entity => entity.Name).HasMaxLength(200); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Code }) .IsUnique() .HasFilter("code is not null and code <> ''"); builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.SortOrder, entity.CreatedAt }); builder.HasIndex(entity => new { entity.TenantId, entity.RegionId }) .HasFilter("region_id is not null"); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => entity.CreatedBy) .OnDelete(DeleteBehavior.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UpdatedBy) .OnDelete(DeleteBehavior.SetNull); } } internal sealed class TenantClassMemberConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_class_members"); builder.ConfigureTimestamps(); builder.Property(entity => entity.MemberType).HasSnakeCaseEnum(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.JoinedAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.UserId, entity.MemberType }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.MemberType }); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.MemberType }); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.ClassId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class TenantStudentNoteConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_student_notes"); builder.ConfigureTimestamps(); builder.Property(entity => entity.NoteType).HasSnakeCaseEnum(); builder.Property(entity => entity.Visibility).HasSnakeCaseEnum(); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.StudentUserId, entity.IsPinned, entity.CreatedAt }); builder.HasIndex(entity => new { entity.TenantId, entity.CreatedBy, entity.CreatedAt }) .HasFilter("created_by is not null"); builder.HasOne().WithMany() .HasForeignKey(entity => entity.StudentUserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.CreatedBy) .OnDelete(DeleteBehavior.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UpdatedBy) .OnDelete(DeleteBehavior.SetNull); } } internal sealed class TenantStudentFollowupConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_student_followups"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Title).HasMaxLength(300); builder.Property(entity => entity.FollowupType).HasSnakeCaseEnum(); builder.Property(entity => entity.Priority).HasSnakeCaseEnum(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.StudentUserId, entity.Status, entity.DueAt, entity.CreatedAt }); builder.HasIndex(entity => new { entity.TenantId, entity.AssignedToUserId, entity.Status, entity.DueAt }) .HasFilter("assigned_to_user_id is not null"); builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.DueAt }) .HasFilter("class_id is not null"); builder.HasOne().WithMany() .HasForeignKey(entity => entity.StudentUserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.AssignedToUserId) .OnDelete(DeleteBehavior.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.ClassId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => entity.CompletedBy) .OnDelete(DeleteBehavior.SetNull); } }