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 TenantExternalProviderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_external_providers"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Capability).HasSnakeCaseEnum(); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.DisplayName).HasMaxLength(200); builder.Property(entity => entity.SecretRef).HasMaxLength(300); builder.Property(entity => entity.ConfigPublic).IsJson("{}"); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.Capability, entity.Provider }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.Capability, entity.Status, entity.Priority }); } } 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.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.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.SetNull); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.SetNull); } } internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureEntity("auth_sessions"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Realm).HasSnakeCaseEnum(); builder.Property(entity => entity.TokenHash).HasMaxLength(256); builder.Property(entity => entity.SecurityStamp).HasMaxLength(128); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.RevokedReason).HasMaxLength(100); 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() .HasAnnotation("Tiku:GlobalUnique", true); builder.HasIndex(entity => new { entity.Realm, entity.TenantId, entity.UserId, entity.ExpiresAt }) .HasFilter("revoked_at is null"); builder.HasIndex(entity => new { entity.TokenFamilyId, entity.RevokedAt }); builder.ToTable(table => table.HasCheckConstraint( "ck_auth_sessions_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)")); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class AuthChallengeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureEntity("auth_challenges"); builder.Property(entity => entity.Realm).HasSnakeCaseEnum(); builder.Property(entity => entity.Purpose).HasSnakeCaseEnum(); builder.Property(entity => entity.TokenHash).HasMaxLength(64); builder.Property(entity => entity.SecurityStamp).HasMaxLength(128); builder.Property(entity => entity.Provider).HasMaxLength(50); builder.Property(entity => entity.IpAddress).HasMaxLength(100); builder.Property(entity => entity.UserAgent).HasMaxLength(1024); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => entity.TokenHash).IsUnique(); builder.HasIndex(entity => new { entity.UserId, entity.Purpose, entity.ExpiresAt }); builder.ToTable(table => table.HasCheckConstraint( "ck_auth_challenges_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)")); builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany().HasForeignKey(entity => entity.TenantId).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 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); } }