using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Tiku.Domain.Content; using Tiku.Domain.Identity; using Tiku.Domain.Learning; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Persistence.Configurations; internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("practice_sessions"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.UserId, entity.Id }); builder.Property(entity => entity.Mode).HasMaxLength(50); builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum(); builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.TotalScore).HasPrecision(8, 2); builder.Property(entity => entity.Status).HasSnakeCaseEnum().HasDefaultValue(PracticeSessionStatus.Active); builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L); builder.Property(entity => entity.AccessMode) .HasSnakeCaseEnum() .HasDefaultValue(PracticeAccessMode.Free); builder.Property(entity => entity.AccessSnapshot).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StrongRevocationVersion }); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StartedAt }); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.StartedAt }); builder.ToTable(table => { table.HasCheckConstraint( "ck_practice_sessions_consumed_free_quota", "consumed_free_quota >= 0"); }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("practice_session_questions"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id }); builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.ContentReleaseQuestionId }).IsUnique(); builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.ContentReleaseQuestionId }) .HasDatabaseName("ix_psq_release_question"); builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionAssetId }) .HasDatabaseName("ix_psq_question_asset"); builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionAssetId, entity.QuestionRevisionId }) .HasDatabaseName("ix_psq_question_revision"); builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionPlacementId }) .HasDatabaseName("ix_psq_question_placement"); builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.AssessmentPolicyVersionId }) .HasDatabaseName("ix_psq_assessment_policy"); builder.Property(entity => entity.Score).HasPrecision(8, 2); builder.Property(entity => entity.QuestionType).HasMaxLength(50).HasDefaultValue("choice"); builder.Property(entity => entity.TypeLabelSnapshot).HasMaxLength(100); builder.Property(entity => entity.TagsSnapshot).IsJson("[]"); builder.Property(entity => entity.GradingRulesSnapshot).IsJson("{}"); builder.Property(entity => entity.SnapshotVersion).HasDefaultValue(1); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionOwnerTenantId, Id = entity.ContentReleaseQuestionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionOwnerTenantId, Id = entity.QuestionAssetId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionOwnerTenantId, entity.QuestionAssetId, Id = entity.QuestionRevisionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionOwnerTenantId, Id = entity.QuestionPlacementId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionOwnerTenantId, Id = entity.AssessmentPolicyVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); } } internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("answer_records"); builder.Property(entity => entity.LegacyId).HasMaxLength(64); builder.Property(entity => entity.LegacyQuestionId).HasMaxLength(64); builder.Property(entity => entity.LegacyCategoryId).HasMaxLength(64); builder.Property(entity => entity.SelectedOptions).IsJson("[]"); builder.Property(entity => entity.GradingStatus).HasSnakeCaseEnum() .HasDefaultValue(AnswerGradingStatus.PendingReview); builder.Property(entity => entity.AwardedScore).HasPrecision(8, 2); builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200); builder.Property(entity => entity.RequestHash).HasMaxLength(64); builder.Property(entity => entity.Revision).HasDefaultValue(1); builder.Property(entity => entity.AnsweredAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId }); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId, entity.ClientSequence }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId, entity.IdempotencyKey }).IsUnique().HasDatabaseName("ux_answer_records_idempotency"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId, entity.SessionQuestionId, entity.Revision }).IsUnique().HasDatabaseName("ux_answer_records_session_question_revision"); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.UserId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId, Id = entity.SessionQuestionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); } } internal sealed class CurrentAnswerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("current_answers"); builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L); builder.Property(entity => entity.UpdatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId, entity.SessionQuestionId }).IsUnique().HasDatabaseName("ux_current_answers_session_question"); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.AnswerRecordId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId, Id = entity.SessionQuestionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class LearningOperationIdempotencyConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureEntity("learning_operation_idempotencies"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); builder.Property(entity => entity.OperationType).HasMaxLength(50); builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200); builder.Property(entity => entity.RequestHash).HasMaxLength(64); builder.Property(entity => entity.ResponseSnapshot).IsJson("{}"); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.PracticeSessionId, entity.OperationType, entity.IdempotencyKey }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.UserId, Id = entity.PracticeSessionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.UserId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("favorite_questions"); builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionAssetOwnerTenantId, entity.QuestionAssetId }); 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.Cascade); ConfigureQuestionAssetEvidence(builder); } private static void ConfigureQuestionAssetEvidence(EntityTypeBuilder builder) { builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, Id = entity.QuestionAssetId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, entity.QuestionAssetId, Id = entity.LastQuestionRevisionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, Id = entity.LastQuestionPlacementId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); } } internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("wrong_questions"); builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionAssetOwnerTenantId, entity.QuestionAssetId }); builder.Property(entity => entity.WrongCount).HasDefaultValue(1); builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastWrongAt }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.TenantId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, Id = entity.QuestionAssetId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, entity.QuestionAssetId, Id = entity.LastQuestionRevisionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, Id = entity.LastQuestionPlacementId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); } } internal sealed class RecentPracticeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("recent_practices"); builder.ConfigureTimestamps(); builder.Property(entity => entity.LegacyId).HasMaxLength(64); builder.Property(entity => entity.PracticeType).HasMaxLength(50); builder.Property(entity => entity.TargetLegacyId).HasMaxLength(64); builder.Property(entity => entity.TargetName).HasMaxLength(300); builder.Property(entity => entity.Color).HasMaxLength(50); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastAccessAt }); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) .OnDelete(DeleteBehavior.Cascade); } }