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 TenantBusinessLicenseConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("tenant_business_licenses"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L); builder.HasIndex(entity => new { entity.TenantId, entity.BusinessLineId }).IsUnique(); builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at")); } } internal sealed class TenantLicensedTargetConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantOwned("tenant_licensed_targets"); builder.HasIndex(entity => new { entity.TenantId, entity.TenantBusinessLicenseId, entity.ExamTargetProfileVersionId }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.TenantBusinessLicenseId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.ExamTargetProfileVersionId) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at")); } } internal sealed class ProductAccessManifestConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("product_access_manifests"); builder.ConfigureTimestamps(); builder.HasIndex(entity => new { entity.TenantId, entity.LearningProductId }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, ProductAccessManifestId = entity.Id, Id = entity.CurrentVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); } } internal sealed class ProductAccessManifestVersionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantOwned("product_access_manifest_versions"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id }); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.VersionNo }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.ToTable(table => { table.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); table.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); table.HasCheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); table.HasCheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); }); } } internal sealed class ProductManifestReleaseConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantOwned("product_manifest_releases"); builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestVersionId, entity.ContentOwnerTenantId, entity.ContentReleaseId }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.ContentOwnerTenantId, Id = entity.ContentReleaseId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { TenantId = entity.PlatformContentPackageOwnerTenantId, Id = entity.PlatformContentPackageVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)")); } } internal sealed class ProductManifestTargetConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantOwned("product_manifest_targets"); builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestVersionId, entity.ExamTargetProfileVersionId }).IsUnique(); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => entity.ExamTargetProfileVersionId) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_product_manifest_target_role", "may_be_primary or may_be_alternate")); } } internal sealed class ProductManifestResourceConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantOwned("product_manifest_resources"); builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum(); builder.Property(entity => entity.DisplayName).HasMaxLength(300); builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestVersionId, entity.ResourceType, entity.ResourceId }).HasDatabaseName("ux_product_manifest_resource").IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestVersionId, entity.SortOrder, entity.Id }).HasDatabaseName("ix_product_manifest_resource_order"); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); } } internal sealed class StudentEntitlementConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("student_entitlements_v2"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.SourceType).HasMaxLength(100); builder.Property(entity => entity.RevokedReason).HasMaxLength(1000); builder.ToTable(table => table.HasCheckConstraint( "ck_student_entitlements_v2_used_question_count", "used_question_count >= 0")); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.StartsAt, entity.EndsAt }); builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId }) .HasFilter("source_id is not null"); builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at")); } } internal sealed class StudentTargetSelectionHistoryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("student_target_selection_history"); builder.ConfigureTimestamps(); builder.Property(entity => entity.Role).HasSnakeCaseEnum(); builder.Property(entity => entity.SourceType).HasMaxLength(100); builder.Property(entity => entity.Reason).HasMaxLength(1000); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId, entity.ExamTargetProfileVersionId }).IsUnique().HasFilter("is_current"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId }) .IsUnique() .HasDatabaseName("ux_student_target_primary") .HasFilter("is_current and role = 'primary'"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId, entity.EffectiveAt }).HasDatabaseName("ix_student_target_selection_history"); builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId }); builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => entity.ExamTargetProfileVersionId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.EntitlementId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_student_target_selection_source", "(source_type = 'student_entitlement' and entitlement_id = source_id) or " + "(source_type = 'class_assignment_grant' and entitlement_id is null)")); } } internal sealed class ClassAssignmentGrantConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("class_assignment_grants"); builder.ConfigureTimestamps(); builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum(); builder.Property(entity => entity.Status).HasSnakeCaseEnum(); builder.Property(entity => entity.RevokedReason).HasMaxLength(1000); builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.StartsAt }); builder.HasIndex(entity => new { entity.TenantId, entity.ResourceType, entity.ResourceId, entity.Status }); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ClassId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) .OnDelete(DeleteBehavior.Restrict); builder.ToTable(table => table.HasCheckConstraint( "ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at")); } } internal sealed class EffectiveAccessProjectionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ConfigureTenantEntity("effective_access_projections"); builder.Property(entity => entity.AlternateProfileVersionIds).HasColumnType("uuid[]"); builder.Property(entity => entity.ContentReleaseIds).HasColumnType("uuid[]"); builder.Property(entity => entity.AudienceSegmentIds).HasColumnType("uuid[]"); builder.Property(entity => entity.ManifestVersionIds).HasColumnType("uuid[]"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId }).IsUnique(); builder.HasIndex(entity => new { entity.TenantId, entity.ValidUntil }); builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) .OnDelete(DeleteBehavior.Restrict); } }