@@ -84,7 +84,8 @@ internal sealed class ContentAssetConfiguration : IEntityTypeConfiguration<Conte
|
||||
{
|
||||
table.HasCheckConstraint("ck_content_assets_file_size", "file_size_bytes is null or file_size_bytes >= 0");
|
||||
table.HasCheckConstraint("ck_content_assets_download_count", "download_count >= 0");
|
||||
table.HasCheckConstraint("ck_content_assets_verified_size", "verified_size_bytes is null or verified_size_bytes >= 0");
|
||||
table.HasCheckConstraint("ck_content_assets_verified_size",
|
||||
"verified_size_bytes is null or verified_size_bytes >= 0");
|
||||
table.HasCheckConstraint(
|
||||
"ck_content_assets_verified_checksum",
|
||||
"verified_checksum_sha256 is null or verified_checksum_sha256 ~ '^[a-f0-9]{64}$'");
|
||||
@@ -141,7 +142,8 @@ internal sealed class ContentImportJobConfiguration : IEntityTypeConfiguration<C
|
||||
});
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_content_import_jobs_counts", "total_count >= 0 and valid_count >= 0 and error_count >= 0 and warning_count >= 0 and inserted_count >= 0 and updated_count >= 0 and skipped_count >= 0");
|
||||
table.HasCheckConstraint("ck_content_import_jobs_counts",
|
||||
"total_count >= 0 and valid_count >= 0 and error_count >= 0 and warning_count >= 0 and inserted_count >= 0 and updated_count >= 0 and skipped_count >= 0");
|
||||
});
|
||||
|
||||
builder.HasOne<User>().WithMany()
|
||||
@@ -342,9 +344,10 @@ internal sealed class VideoPlaybackProgressConfiguration : IEntityTypeConfigurat
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_video_playback_progress_position", "position_seconds >= 0");
|
||||
table.HasCheckConstraint("ck_video_playback_progress_duration", "duration_seconds is null or duration_seconds >= 0");
|
||||
table.HasCheckConstraint("ck_video_playback_progress_duration",
|
||||
"duration_seconds is null or duration_seconds >= 0");
|
||||
table.HasCheckConstraint("ck_video_playback_progress_watched", "watched_seconds >= 0");
|
||||
table.HasCheckConstraint("ck_video_playback_progress_play_count", "play_count >= 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,6 @@ internal sealed class SubjectConfiguration : IEntityTypeConfiguration<Subject>
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class CategoryConfiguration : IEntityTypeConfiguration<Category>
|
||||
@@ -184,4 +183,4 @@ internal sealed class CategoryConfiguration : IEntityTypeConfiguration<Category>
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ internal sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
|
||||
builder.Property(entity => entity.DetailImages).IsJson("[]");
|
||||
builder.Property(entity => entity.IsActive).HasDefaultValue(true);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RegionId, entity.Type, entity.IsActive, entity.SortOrder });
|
||||
builder.HasIndex(entity => new
|
||||
{ entity.TenantId, entity.RegionId, entity.Type, entity.IsActive, entity.SortOrder });
|
||||
builder.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
@@ -47,7 +48,8 @@ internal sealed class SvipPlanConfiguration : IEntityTypeConfiguration<SvipPlan>
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RegionId, entity.IsActive, entity.SortOrder });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_svip_plans_price", "price_cents >= 0 and (original_price_cents is null or original_price_cents >= 0)");
|
||||
table.HasCheckConstraint("ck_svip_plans_price",
|
||||
"price_cents >= 0 and (original_price_cents is null or original_price_cents >= 0)");
|
||||
table.HasCheckConstraint("ck_svip_plans_days", "days >= 0");
|
||||
});
|
||||
builder.HasOne<Region>().WithMany()
|
||||
@@ -208,7 +210,8 @@ internal sealed class CodeBatchConfiguration : IEntityTypeConfiguration<CodeBatc
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_code_batches_counts", "total_count >= 0");
|
||||
table.HasCheckConstraint("ck_code_batches_amounts", "default_unit_price_cents >= 0 and cost_price_cents >= 0");
|
||||
table.HasCheckConstraint("ck_code_batches_amounts",
|
||||
"default_unit_price_cents >= 0 and cost_price_cents >= 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -368,8 +371,10 @@ internal sealed class CommerceReconciliationBatchConfiguration : IEntityTypeConf
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_batches_counts", "total_count >= 0 and matched_count >= 0 and mismatch_count >= 0 and missing_local_count >= 0 and missing_provider_count >= 0 and duplicate_count >= 0 and ignored_count >= 0");
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_batches_amounts", "amount_cents >= 0 and refund_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_batches_counts",
|
||||
"total_count >= 0 and matched_count >= 0 and mismatch_count >= 0 and missing_local_count >= 0 and missing_provider_count >= 0 and duplicate_count >= 0 and ignored_count >= 0");
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_batches_amounts",
|
||||
"amount_cents >= 0 and refund_amount_cents >= 0");
|
||||
});
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
@@ -402,7 +407,8 @@ internal sealed class CommerceReconciliationItemConfiguration : IEntityTypeConfi
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_items_row", "row_no > 0");
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_items_amounts", "amount_cents >= 0 and refund_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commerce_reconciliation_items_amounts",
|
||||
"amount_cents >= 0 and refund_amount_cents >= 0");
|
||||
});
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<CommerceReconciliationBatch>().WithMany()
|
||||
@@ -453,7 +459,8 @@ internal sealed class CommerceReconciliationIssueConfiguration : IEntityTypeConf
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.BatchId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.OrderNo, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.PaymentId, entity.CreatedAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_commerce_reconciliation_issues_amounts", "amount_cents >= 0 and refund_amount_cents >= 0"));
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_commerce_reconciliation_issues_amounts",
|
||||
"amount_cents >= 0 and refund_amount_cents >= 0"));
|
||||
builder.HasOne<CommerceReconciliationBatch>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.BatchId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
@@ -480,7 +487,8 @@ internal sealed class CommerceReconciliationIssueConfiguration : IEntityTypeConf
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CommerceReconciliationIssueEventConfiguration : IEntityTypeConfiguration<CommerceReconciliationIssueEvent>
|
||||
internal sealed class
|
||||
CommerceReconciliationIssueEventConfiguration : IEntityTypeConfiguration<CommerceReconciliationIssueEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CommerceReconciliationIssueEvent> builder)
|
||||
{
|
||||
@@ -517,7 +525,8 @@ internal sealed class CommerceAdjustmentVoucherConfiguration : IEntityTypeConfig
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.VoucherNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.IssueId, entity.CreatedAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_commerce_adjustment_vouchers_amount", "amount_cents > 0"));
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_commerce_adjustment_vouchers_amount", "amount_cents > 0"));
|
||||
builder.HasOne<CommerceReconciliationIssue>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.IssueId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
@@ -547,7 +556,8 @@ internal sealed class CommerceAdjustmentVoucherConfiguration : IEntityTypeConfig
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CommerceAdjustmentVoucherEventConfiguration : IEntityTypeConfiguration<CommerceAdjustmentVoucherEvent>
|
||||
internal sealed class
|
||||
CommerceAdjustmentVoucherEventConfiguration : IEntityTypeConfiguration<CommerceAdjustmentVoucherEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CommerceAdjustmentVoucherEvent> builder)
|
||||
{
|
||||
@@ -566,4 +576,4 @@ internal sealed class CommerceAdjustmentVoucherEventConfiguration : IEntityTypeC
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ActorUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,4 +72,4 @@ internal static class ConfigurationSupport
|
||||
: Enum.Parse<TEnum>(value.Replace("_", string.Empty), true))
|
||||
.HasMaxLength(maxLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,7 @@ internal sealed class ContentNodeConfiguration : IEntityTypeConfiguration<Conten
|
||||
builder.HasIndex(entity => entity.Path).HasMethod("gist");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.MarkerType })
|
||||
.HasFilter("marker_type is not null");
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_content_nodes_depth", "depth >= 0");
|
||||
});
|
||||
builder.ToTable(table => { table.HasCheckConstraint("ck_content_nodes_depth", "depth >= 0"); });
|
||||
|
||||
builder.HasOne<ContentEntry>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.EntryId })
|
||||
@@ -264,4 +261,4 @@ internal sealed class PracticeBlueprintConfiguration : IEntityTypeConfiguration<
|
||||
.HasForeignKey(entity => entity.UpdatedBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ internal sealed class ContentAssetAccessEventConfiguration : IEntityTypeConfigur
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt })
|
||||
.HasFilter("user_id is not null");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.AccessType, entity.Result, entity.CreatedAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_content_asset_access_events_expiry", "expires_in_seconds is null or expires_in_seconds > 0"));
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_content_asset_access_events_expiry",
|
||||
"expires_in_seconds is null or expires_in_seconds > 0"));
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<ContentAsset>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.AssetId })
|
||||
@@ -41,7 +42,8 @@ internal sealed class ContentAssetAccessEventConfiguration : IEntityTypeConfigur
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ContentAssetSecurityScanEventConfiguration : IEntityTypeConfiguration<ContentAssetSecurityScanEvent>
|
||||
internal sealed class
|
||||
ContentAssetSecurityScanEventConfiguration : IEntityTypeConfiguration<ContentAssetSecurityScanEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContentAssetSecurityScanEvent> builder)
|
||||
{
|
||||
@@ -131,4 +133,4 @@ internal sealed class AiRecommendationReportConfiguration : IEntityTypeConfigura
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ internal sealed class ReferralTrackConfiguration : IEntityTypeConfiguration<Refe
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LeadId, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RefCode, entity.EventType, entity.CreatedAt });
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.TargetUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<ReferralLead>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.LeadId })
|
||||
@@ -66,9 +67,12 @@ internal sealed class ReferralLeadConfiguration : IEntityTypeConfiguration<Refer
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.StudentUserId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.ReferrerUserId, entity.BoundAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.AssignedToUserId, entity.Status, entity.BoundAt });
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.StudentUserId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.AssignedToUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.StudentUserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.AssignedToUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.AssignedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<ReferralTrack>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.FirstTrackId })
|
||||
@@ -190,7 +194,8 @@ internal sealed class TenantCommissionSettingConfiguration : IEntityTypeConfigur
|
||||
builder.HasIndex(entity => entity.TenantId).IsUnique();
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_tenant_commission_settings_default_rate", "default_rate >= 0 and default_rate <= 1");
|
||||
table.HasCheckConstraint("ck_tenant_commission_settings_default_rate",
|
||||
"default_rate >= 0 and default_rate <= 1");
|
||||
table.HasCheckConstraint("ck_tenant_commission_settings_min_settlement", "min_settlement_cents >= 0");
|
||||
});
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UpdatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
@@ -219,11 +224,14 @@ internal sealed class CommissionSettlementConfiguration : IEntityTypeConfigurati
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_commission_settlements_counts", "source_count >= 0 and paid_user_count >= 0");
|
||||
table.HasCheckConstraint("ck_commission_settlements_amounts", "gross_amount_cents >= 0 and commission_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commission_settlements_rates", "default_rate >= 0 and default_rate <= 1 and (effective_rate is null or (effective_rate >= 0 and effective_rate <= 1))");
|
||||
table.HasCheckConstraint("ck_commission_settlements_amounts",
|
||||
"gross_amount_cents >= 0 and commission_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commission_settlements_rates",
|
||||
"default_rate >= 0 and default_rate <= 1 and (effective_rate is null or (effective_rate >= 0 and effective_rate <= 1))");
|
||||
table.HasCheckConstraint("ck_commission_settlements_period", "period_end >= period_start");
|
||||
});
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.GeneratedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReviewedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.PaidBy).OnDelete(DeleteBehavior.SetNull);
|
||||
@@ -247,15 +255,19 @@ internal sealed class CommissionSettlementItemConfiguration : IEntityTypeConfigu
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.SettlementId });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_commission_settlement_items_amounts", "gross_amount_cents >= 0 and commission_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commission_settlement_items_rate", "commission_rate >= 0 and commission_rate <= 1");
|
||||
table.HasCheckConstraint("ck_commission_settlement_items_amounts",
|
||||
"gross_amount_cents >= 0 and commission_amount_cents >= 0");
|
||||
table.HasCheckConstraint("ck_commission_settlement_items_rate",
|
||||
"commission_rate >= 0 and commission_rate <= 1");
|
||||
});
|
||||
builder.HasOne<CommissionSettlement>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.SettlementId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.StudentUserId).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ReferrerUserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.StudentUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +301,8 @@ internal sealed class CommissionSettlementProofConfiguration : IEntityTypeConfig
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CommissionSettlementExportEventConfiguration : IEntityTypeConfiguration<CommissionSettlementExportEvent>
|
||||
internal sealed class
|
||||
CommissionSettlementExportEventConfiguration : IEntityTypeConfiguration<CommissionSettlementExportEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CommissionSettlementExportEvent> builder)
|
||||
{
|
||||
@@ -310,4 +323,4 @@ internal sealed class CommissionSettlementExportEventConfiguration : IEntityType
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ExportedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,4 +94,4 @@ internal sealed class StudentProfileConfiguration : IEntityTypeConfiguration<Stu
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,8 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
|
||||
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.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);
|
||||
@@ -288,4 +289,4 @@ internal sealed class RecentPracticeConfiguration : IEntityTypeConfiguration<Rec
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,8 @@ internal sealed class PracticeSessionReportConfiguration : IEntityTypeConfigurat
|
||||
.HasFilter("blueprint_id is not null");
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_practice_session_reports_counts", "total_questions >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_reports_counts",
|
||||
"total_questions >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_reports_scores", "score >= 0 and total_score >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_reports_accuracy", "accuracy >= 0 and accuracy <= 1");
|
||||
table.HasCheckConstraint("ck_practice_session_reports_duration", "duration_seconds >= 0");
|
||||
@@ -244,7 +245,8 @@ internal sealed class PracticeSessionReportSectionConfiguration : IEntityTypeCon
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.ReportId, entity.SortOrder });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_practice_session_report_sections_counts", "question_count >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_report_sections_counts",
|
||||
"question_count >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_report_sections_scores", "score >= 0 and total_score >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_report_sections_accuracy", "accuracy >= 0 and accuracy <= 1");
|
||||
});
|
||||
@@ -269,7 +271,8 @@ internal sealed class DashboardDailyStatConfiguration : IEntityTypeConfiguration
|
||||
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
||||
builder.Property(entity => entity.LegacyRegionId).HasMaxLength(64);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.StatDate, entity.RegionId }).IsUnique().AreNullsDistinct(false);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.StatDate, entity.RegionId }).IsUnique()
|
||||
.AreNullsDistinct(false);
|
||||
|
||||
builder.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
@@ -288,11 +291,12 @@ internal sealed class RevenueDailyStatConfiguration : IEntityTypeConfiguration<R
|
||||
builder.Property(entity => entity.LegacyRegionId).HasMaxLength(64);
|
||||
builder.Property(entity => entity.SaleType).HasMaxLength(50);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.StatDate, entity.RegionId, entity.SaleType }).IsUnique().AreNullsDistinct(false);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.StatDate, entity.RegionId, entity.SaleType })
|
||||
.IsUnique().AreNullsDistinct(false);
|
||||
|
||||
builder.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,8 @@ internal sealed class PlatformBackendRoleConfiguration : IEntityTypeConfiguratio
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBackendRolePermissionConfiguration : IEntityTypeConfiguration<PlatformBackendRolePermission>
|
||||
internal sealed class
|
||||
PlatformBackendRolePermissionConfiguration : IEntityTypeConfiguration<PlatformBackendRolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBackendRolePermission> builder)
|
||||
{
|
||||
@@ -285,7 +286,8 @@ internal sealed class AuthorizationScopeVersionConfiguration : IEntityTypeConfig
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AuthorizationCacheInvalidationConfiguration : IEntityTypeConfiguration<AuthorizationCacheInvalidation>
|
||||
internal sealed class
|
||||
AuthorizationCacheInvalidationConfiguration : IEntityTypeConfiguration<AuthorizationCacheInvalidation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AuthorizationCacheInvalidation> builder)
|
||||
{
|
||||
@@ -297,7 +299,8 @@ internal sealed class AuthorizationCacheInvalidationConfiguration : IEntityTypeC
|
||||
value => value == null ? null : Enum.Parse<AuthRealm>(value, true));
|
||||
builder.Property(entity => entity.LastError).HasMaxLength(2000);
|
||||
builder.HasIndex(entity => new { entity.ProcessedAt, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TargetType, entity.TenantId, entity.UserId, entity.SessionId, entity.Version });
|
||||
builder.HasIndex(entity => new
|
||||
{ entity.TargetType, entity.TenantId, entity.UserId, entity.SessionId, entity.Version });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +494,7 @@ internal sealed class TenantThemeConfigConfiguration : IEntityTypeConfiguration<
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.PublishedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.DraftUpdatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.DraftUpdatedBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ internal sealed class TenantBillingProfileConfiguration : IEntityTypeConfigurati
|
||||
builder.Property(entity => entity.BankName).HasMaxLength(200);
|
||||
builder.Property(entity => entity.BankAccountMasked).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasOne<Tenant>().WithOne().HasForeignKey<TenantBillingProfile>(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Tenant>().WithOne().HasForeignKey<TenantBillingProfile>(entity => entity.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +41,8 @@ internal sealed class TenantBillingPolicyConfiguration : IEntityTypeConfiguratio
|
||||
builder.ToTable(table => table.HasCheckConstraint(
|
||||
"ck_tenant_billing_policies_renewal_lead_days",
|
||||
"renewal_lead_days between 1 and 90"));
|
||||
builder.HasOne<Tenant>().WithOne().HasForeignKey<TenantBillingPolicy>(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Tenant>().WithOne().HasForeignKey<TenantBillingPolicy>(entity => entity.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +81,8 @@ internal sealed class PlatformOperationIdempotencyConfiguration : IEntityTypeCon
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBillingInvoiceReminderConfiguration : IEntityTypeConfiguration<PlatformBillingInvoiceReminder>
|
||||
internal sealed class
|
||||
PlatformBillingInvoiceReminderConfiguration : IEntityTypeConfiguration<PlatformBillingInvoiceReminder>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBillingInvoiceReminder> builder)
|
||||
{
|
||||
@@ -89,7 +92,8 @@ internal sealed class PlatformBillingInvoiceReminderConfiguration : IEntityTypeC
|
||||
builder.Property(entity => entity.Channel).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.InvoiceId, entity.ReminderType, entity.Channel, entity.ReminderDate }).IsUnique();
|
||||
builder.HasIndex(entity => new
|
||||
{ entity.TenantId, entity.InvoiceId, entity.ReminderType, entity.Channel, entity.ReminderDate }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.ReminderDate });
|
||||
builder.HasIndex(entity => new { entity.InvoiceId, entity.ReminderDate });
|
||||
builder.ToTable(table =>
|
||||
@@ -143,15 +147,20 @@ internal sealed class PlatformAuditAlertConfiguration : IEntityTypeConfiguration
|
||||
builder.HasIndex(entity => new { entity.Status, entity.Severity, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => entity.AuditLogId);
|
||||
builder.HasOne<PlatformAuditAlertRule>().WithMany().HasForeignKey(entity => entity.RuleId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<AuditLog>().WithMany().HasForeignKey(entity => entity.AuditLogId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformAuditAlertRule>().WithMany().HasForeignKey(entity => entity.RuleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<AuditLog>().WithMany().HasForeignKey(entity => entity.AuditLogId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.AcknowledgedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.AcknowledgedBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.ResolvedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBillingDunningNotificationChannelConfiguration : IEntityTypeConfiguration<PlatformBillingDunningNotificationChannel>
|
||||
internal sealed class
|
||||
PlatformBillingDunningNotificationChannelConfiguration : IEntityTypeConfiguration<
|
||||
PlatformBillingDunningNotificationChannel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBillingDunningNotificationChannel> builder)
|
||||
{
|
||||
@@ -162,21 +171,27 @@ internal sealed class PlatformBillingDunningNotificationChannelConfiguration : I
|
||||
builder.Property(entity => entity.Provider).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.WebhookUrl).HasMaxLength(2048);
|
||||
builder.Property(entity => entity.SecretRef).HasMaxLength(300);
|
||||
builder.Property(entity => entity.ReminderTypes).HasColumnType("text[]").HasDefaultValueSql("array['overdue', 'final_notice']::text[]");
|
||||
builder.Property(entity => entity.ReminderChannels).HasColumnType("text[]").HasDefaultValueSql("array['internal']::text[]");
|
||||
builder.Property(entity => entity.ReminderTypes).HasColumnType("text[]")
|
||||
.HasDefaultValueSql("array['overdue', 'final_notice']::text[]");
|
||||
builder.Property(entity => entity.ReminderChannels).HasColumnType("text[]")
|
||||
.HasDefaultValueSql("array['internal']::text[]");
|
||||
builder.Property(entity => entity.TenantIds).HasColumnType("uuid[]").HasDefaultValueSql("'{}'::uuid[]");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => entity.ChannelCode).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Enabled, entity.MinReminderLevel, entity.ChannelCode });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_platform_billing_dunning_channels_level", "min_reminder_level between 1 and 20");
|
||||
table.HasCheckConstraint("ck_platform_billing_dunning_channels_timeout", "timeout_seconds between 1 and 60");
|
||||
table.HasCheckConstraint("ck_platform_billing_dunning_channels_level",
|
||||
"min_reminder_level between 1 and 20");
|
||||
table.HasCheckConstraint("ck_platform_billing_dunning_channels_timeout",
|
||||
"timeout_seconds between 1 and 60");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBillingDunningNotificationEventConfiguration : IEntityTypeConfiguration<PlatformBillingDunningNotificationEvent>
|
||||
internal sealed class
|
||||
PlatformBillingDunningNotificationEventConfiguration : IEntityTypeConfiguration<
|
||||
PlatformBillingDunningNotificationEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBillingDunningNotificationEvent> builder)
|
||||
{
|
||||
@@ -192,7 +207,8 @@ internal sealed class PlatformBillingDunningNotificationEventConfiguration : IEn
|
||||
builder.HasIndex(entity => new { entity.ReminderId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.InvoiceId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_dunning_events_attempts", "attempts >= 0"));
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_platform_billing_dunning_events_attempts", "attempts >= 0"));
|
||||
builder.HasOne<PlatformBillingDunningNotificationChannel>().WithMany()
|
||||
.HasForeignKey(entity => entity.ChannelId)
|
||||
.HasConstraintName("fk_platform_billing_dunning_events_channel")
|
||||
@@ -243,7 +259,8 @@ internal sealed class PlatformPaymentChannelConfiguration : IEntityTypeConfigura
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.AppId, entity.Provider }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Status, entity.Priority, entity.Provider });
|
||||
builder.HasOne<PlatformPaymentApp>().WithMany().HasForeignKey(entity => entity.AppId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformPaymentApp>().WithMany().HasForeignKey(entity => entity.AppId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +279,8 @@ internal sealed class PlatformApprovalPolicyConfiguration : IEntityTypeConfigura
|
||||
{
|
||||
table.HasCheckConstraint("ck_platform_approval_policies_version", "version > 0");
|
||||
table.HasCheckConstraint("ck_platform_approval_policies_expiry", "expires_after_hours between 1 and 720");
|
||||
table.HasCheckConstraint("ck_platform_approval_policies_threshold", "amount_threshold_cents is null or amount_threshold_cents > 0");
|
||||
table.HasCheckConstraint("ck_platform_approval_policies_threshold",
|
||||
"amount_threshold_cents is null or amount_threshold_cents > 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -296,7 +314,8 @@ internal sealed class PlatformApprovalRequestConfiguration : IEntityTypeConfigur
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformConfigurationDefinitionConfiguration : IEntityTypeConfiguration<PlatformConfigurationDefinition>
|
||||
internal sealed class
|
||||
PlatformConfigurationDefinitionConfiguration : IEntityTypeConfiguration<PlatformConfigurationDefinition>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformConfigurationDefinition> builder)
|
||||
{
|
||||
@@ -326,10 +345,12 @@ internal sealed class PlatformConfigurationVersionConfiguration : IEntityTypeCon
|
||||
builder.Property(entity => entity.Reason).HasMaxLength(1000);
|
||||
builder.HasIndex(entity => new { entity.DefinitionId, entity.Environment, entity.Version }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.DefinitionId, entity.Environment, entity.Status });
|
||||
builder.HasOne<PlatformConfigurationDefinition>().WithMany().HasForeignKey(entity => entity.DefinitionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformConfigurationDefinition>().WithMany().HasForeignKey(entity => entity.DefinitionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.PublishedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformConfigurationVersion>().WithMany().HasForeignKey(entity => entity.RolledBackFromVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformConfigurationVersion>().WithMany()
|
||||
.HasForeignKey(entity => entity.RolledBackFromVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,8 +386,10 @@ internal sealed class PlatformNotificationDeliveryConfiguration : IEntityTypeCon
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TemplateId, entity.RecipientUserId, entity.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Status, entity.CreatedAt });
|
||||
builder.HasOne<PlatformNotificationTemplate>().WithMany().HasForeignKey(entity => entity.TemplateId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.RecipientUserId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformNotificationTemplate>().WithMany().HasForeignKey(entity => entity.TemplateId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.RecipientUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,4 +62,4 @@ internal sealed class PocketBaseImportIssueConfiguration : IEntityTypeConfigurat
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ internal sealed class PointActivityClaimConfiguration : IEntityTypeConfiguration
|
||||
builder.Property(entity => entity.SourceType).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.Property(entity => entity.ClaimedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.TaskId, entity.SourceType, entity.SourceId })
|
||||
builder.HasIndex(entity => new
|
||||
{ entity.TenantId, entity.UserId, entity.TaskId, entity.SourceType, entity.SourceId })
|
||||
.IsUnique()
|
||||
.HasFilter("source_type is not null and source_id is not null");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
|
||||
@@ -49,10 +50,7 @@ internal sealed class PointActivityClaimConfiguration : IEntityTypeConfiguration
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_point_activity_claims_points", "points > 0");
|
||||
});
|
||||
builder.ToTable(table => { table.HasCheckConstraint("ck_point_activity_claims_points", "points > 0"); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +107,4 @@ internal sealed class PointExchangeOrderConfiguration : IEntityTypeConfiguration
|
||||
table.HasCheckConstraint("ck_point_exchange_orders_points_cost", "points_cost > 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,4 +106,4 @@ internal sealed class QuestionVersionConfiguration : IEntityTypeConfiguration<Qu
|
||||
.HasForeignKey(entity => entity.CreatedBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ internal sealed class SaasFeatureConfiguration : IEntityTypeConfiguration<SaasFe
|
||||
builder.Property(value => value.Status).HasSnakeCaseEnum();
|
||||
builder.HasIndex(value => value.Code).IsUnique();
|
||||
builder.HasIndex(value => new { value.Status, value.Category, value.SortOrder });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_saas_features_reference_price", "reference_price_cents >= 0"));
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_saas_features_reference_price", "reference_price_cents >= 0"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +72,13 @@ internal sealed class SaasOfferingVersionConfiguration : IEntityTypeConfiguratio
|
||||
builder.Property(value => value.Metadata).IsJson("{}");
|
||||
builder.HasIndex(value => new { value.OfferingId, value.Version }).IsUnique();
|
||||
builder.HasIndex(value => new { value.OfferingId, value.Status, value.EffectiveAt });
|
||||
builder.HasOne<SaasOffering>().WithMany().HasForeignKey(value => value.OfferingId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOffering>().WithMany().HasForeignKey(value => value.OfferingId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_saas_offering_versions_version", "version > 0");
|
||||
table.HasCheckConstraint("ck_saas_offering_versions_amount", "original_amount_cents >= 0 and amount_cents >= 0 and amount_cents <= original_amount_cents");
|
||||
table.HasCheckConstraint("ck_saas_offering_versions_amount",
|
||||
"original_amount_cents >= 0 and amount_cents >= 0 and amount_cents <= original_amount_cents");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -87,8 +90,10 @@ internal sealed class SaasOfferingVersionFeatureConfiguration : IEntityTypeConfi
|
||||
builder.ConfigureEntity("saas_offering_version_features");
|
||||
builder.Property(value => value.FeatureCode).HasMaxLength(120);
|
||||
builder.HasIndex(value => new { value.OfferingVersionId, value.FeatureCode }).IsUnique();
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code).HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code)
|
||||
.HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +110,10 @@ internal sealed class SaasFeatureLimitDefinitionConfiguration : IEntityTypeConfi
|
||||
builder.Property(value => value.Kind).HasSnakeCaseEnum();
|
||||
builder.HasIndex(value => value.MetricCode).IsUnique();
|
||||
builder.HasIndex(value => new { value.FeatureCode, value.Kind });
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code).HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_saas_feature_limit_warning", "warning_percent between 1 and 100"));
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code)
|
||||
.HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_saas_feature_limit_warning", "warning_percent between 1 and 100"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +124,10 @@ internal sealed class SaasOfferingVersionLimitConfiguration : IEntityTypeConfigu
|
||||
builder.ConfigureEntity("saas_offering_version_limits");
|
||||
builder.Property(value => value.MetricCode).HasMaxLength(120);
|
||||
builder.HasIndex(value => new { value.OfferingVersionId, value.MetricCode }).IsUnique();
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasFeatureLimitDefinition>().WithMany().HasPrincipalKey(value => value.MetricCode).HasForeignKey(value => value.MetricCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasFeatureLimitDefinition>().WithMany().HasPrincipalKey(value => value.MetricCode)
|
||||
.HasForeignKey(value => value.MetricCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_saas_offering_version_limits_value", "limit_value >= 0"));
|
||||
}
|
||||
}
|
||||
@@ -133,10 +142,14 @@ internal sealed class TenantSaasSubscriptionConfiguration : IEntityTypeConfigura
|
||||
builder.Property(value => value.LifecycleVersion).IsConcurrencyToken();
|
||||
builder.Property(value => value.Metadata).IsJson("{}");
|
||||
builder.HasIndex(value => new { value.TenantId, value.Status, value.CurrentPeriodEnd });
|
||||
builder.HasIndex(value => value.TenantId).IsUnique().HasFilter("status in ('trial', 'active', 'past_due', 'suspended')");
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.BaseOfferingVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.ScheduledBaseOfferingVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_saas_subscriptions_period", "current_period_end > current_period_start and current_period_start >= starts_at"));
|
||||
builder.HasIndex(value => value.TenantId).IsUnique()
|
||||
.HasFilter("status in ('trial', 'active', 'past_due', 'suspended')");
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.BaseOfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.ScheduledBaseOfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_saas_subscriptions_period",
|
||||
"current_period_end > current_period_start and current_period_start >= starts_at"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,12 +174,14 @@ internal sealed class TenantSaasSubscriptionItemConfiguration : IEntityTypeConfi
|
||||
.HasForeignKey(value => new { value.TenantId, value.SubscriptionId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformBillingOrderItem>().WithMany()
|
||||
.HasForeignKey(value => new { value.TenantId, value.SourceOrderItemId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_saas_subscription_items_period", "ends_at > starts_at"));
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_tenant_saas_subscription_items_period", "ends_at > starts_at"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +195,8 @@ internal sealed class TenantFeatureOverrideConfiguration : IEntityTypeConfigurat
|
||||
builder.Property(value => value.Mode).HasSnakeCaseEnum();
|
||||
builder.Property(value => value.Reason).HasMaxLength(1000);
|
||||
builder.HasIndex(value => new { value.TenantId, value.FeatureCode }).IsUnique();
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code).HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<SaasFeature>().WithMany().HasPrincipalKey(value => value.Code)
|
||||
.HasForeignKey(value => value.FeatureCode).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +208,10 @@ internal sealed class TenantFeatureUsageConfiguration : IEntityTypeConfiguration
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(value => value.MetricCode).HasMaxLength(120);
|
||||
builder.Property(value => value.Version).IsConcurrencyToken();
|
||||
builder.HasIndex(value => new { value.TenantId, value.MetricCode, value.PeriodStart, value.PeriodEnd }).IsUnique();
|
||||
builder.HasOne<SaasFeatureLimitDefinition>().WithMany().HasPrincipalKey(value => value.MetricCode).HasForeignKey(value => value.MetricCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(value => new { value.TenantId, value.MetricCode, value.PeriodStart, value.PeriodEnd })
|
||||
.IsUnique();
|
||||
builder.HasOne<SaasFeatureLimitDefinition>().WithMany().HasPrincipalKey(value => value.MetricCode)
|
||||
.HasForeignKey(value => value.MetricCode).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_tenant_feature_usage_values", "used_value >= 0 and limit_value_snapshot >= 0");
|
||||
@@ -218,7 +236,8 @@ internal sealed class PlatformBillingQuoteConfiguration : IEntityTypeConfigurati
|
||||
builder.HasIndex(value => new { value.TenantId, value.QuoteNo }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.Status, value.ExpiresAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_quotes_amounts", "original_amount_cents >= 0 and discount_amount_cents >= 0 and total_amount_cents >= 0 and total_amount_cents = original_amount_cents - discount_amount_cents"));
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_quotes_amounts",
|
||||
"original_amount_cents >= 0 and discount_amount_cents >= 0 and total_amount_cents >= 0 and total_amount_cents = original_amount_cents - discount_amount_cents"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +251,12 @@ internal sealed class PlatformBillingQuoteItemConfiguration : IEntityTypeConfigu
|
||||
builder.Property(value => value.Snapshot).IsJson("{}");
|
||||
builder.HasIndex(value => new { value.TenantId, value.QuoteId, value.OfferingVersionId }).IsUnique();
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(value => value.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformBillingQuote>().WithMany().HasForeignKey(value => new { value.TenantId, value.QuoteId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_quote_items_amounts", "quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"));
|
||||
builder.HasOne<PlatformBillingQuote>().WithMany().HasForeignKey(value => new { value.TenantId, value.QuoteId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_quote_items_amounts",
|
||||
"quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +275,10 @@ internal sealed class PlatformBillingOrderConfiguration : IEntityTypeConfigurati
|
||||
builder.HasIndex(value => new { value.TenantId, value.OrderNo }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.Status, value.CreatedAt });
|
||||
builder.HasOne<PlatformBillingQuote>().WithMany().HasForeignKey(value => new { value.TenantId, value.QuoteId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_orders_amounts", "original_amount_cents >= 0 and discount_amount_cents >= 0 and total_amount_cents >= 0 and total_amount_cents = original_amount_cents - discount_amount_cents"));
|
||||
builder.HasOne<PlatformBillingQuote>().WithMany().HasForeignKey(value => new { value.TenantId, value.QuoteId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_orders_amounts",
|
||||
"original_amount_cents >= 0 and discount_amount_cents >= 0 and total_amount_cents >= 0 and total_amount_cents = original_amount_cents - discount_amount_cents"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,9 +292,12 @@ internal sealed class PlatformBillingOrderItemConfiguration : IEntityTypeConfigu
|
||||
builder.Property(value => value.Snapshot).IsJson("{}");
|
||||
builder.HasIndex(value => new { value.TenantId, value.OrderId, value.OfferingVersionId }).IsUnique();
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(value => value.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_order_items_amounts", "quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"));
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<SaasOfferingVersion>().WithMany().HasForeignKey(value => value.OfferingVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_order_items_amounts",
|
||||
"quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +317,8 @@ internal sealed class PlatformBillingPaymentConfiguration : IEntityTypeConfigura
|
||||
builder.HasIndex(value => new { value.TenantId, value.PaymentNo }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.OrderId, value.Status });
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_payments_amount", "amount_cents >= 0"));
|
||||
}
|
||||
}
|
||||
@@ -309,7 +337,9 @@ internal sealed class PlatformBillingPaymentEventConfiguration : IEntityTypeConf
|
||||
builder.HasIndex(value => new { value.TenantId, value.Provider, value.ProviderEventId }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.PaymentId, value.CreatedAt });
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(value => value.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformBillingPayment>().WithMany().HasForeignKey(value => new { value.TenantId, value.PaymentId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PlatformBillingPayment>().WithMany()
|
||||
.HasForeignKey(value => new { value.TenantId, value.PaymentId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,8 +360,11 @@ internal sealed class PlatformBillingRefundConfiguration : IEntityTypeConfigurat
|
||||
builder.HasIndex(value => new { value.TenantId, value.RefundNo }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.OrderId, value.Status });
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformBillingPayment>().WithMany().HasForeignKey(value => new { value.TenantId, value.PaymentId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PlatformBillingPayment>().WithMany()
|
||||
.HasForeignKey(value => new { value.TenantId, value.PaymentId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_refunds_amount", "amount_cents > 0"));
|
||||
}
|
||||
}
|
||||
@@ -348,7 +381,9 @@ internal sealed class PlatformBillingInvoiceConfiguration : IEntityTypeConfigura
|
||||
builder.Property(value => value.BillingProfileSnapshot).IsJson("{}");
|
||||
builder.HasIndex(value => new { value.TenantId, value.InvoiceNo }).IsUnique();
|
||||
builder.HasIndex(value => new { value.TenantId, value.Status, value.DueDate });
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId }).HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_platform_billing_invoices_amount", "total_amount_cents >= 0"));
|
||||
builder.HasOne<PlatformBillingOrder>().WithMany().HasForeignKey(value => new { value.TenantId, value.OrderId })
|
||||
.HasPrincipalKey(value => new { value.TenantId, value.Id }).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table =>
|
||||
table.HasCheckConstraint("ck_platform_billing_invoices_amount", "total_amount_cents >= 0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,8 @@ internal sealed class ScorelineRecordConfiguration : IEntityTypeConfiguration<Sc
|
||||
builder.Property(entity => entity.MajorName).HasMaxLength(300);
|
||||
builder.Property(entity => entity.FieldValues).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RegionId, entity.SchoolId, entity.MajorId, entity.Year });
|
||||
builder.HasIndex(entity => new
|
||||
{ entity.TenantId, entity.RegionId, entity.SchoolId, entity.MajorId, entity.Year });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Year });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Year, entity.SchoolName, entity.MajorName, entity.Id })
|
||||
.IsDescending(false, true, false, false, false);
|
||||
@@ -62,4 +63,4 @@ internal sealed class ScorelineRecordConfiguration : IEntityTypeConfiguration<Sc
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,8 @@ internal sealed class UserWordProgressConfiguration : IEntityTypeConfiguration<U
|
||||
{
|
||||
table.HasCheckConstraint("ck_user_word_progress_review_count", "review_count >= 0");
|
||||
table.HasCheckConstraint("ck_user_word_progress_correct_streak", "correct_streak >= 0");
|
||||
table.HasCheckConstraint("ck_user_word_progress_ease_factor", "ease_factor >= 1.30 and ease_factor <= 3.00");
|
||||
table.HasCheckConstraint("ck_user_word_progress_ease_factor",
|
||||
"ease_factor >= 1.30 and ease_factor <= 3.00");
|
||||
});
|
||||
|
||||
builder.HasOne<User>().WithMany()
|
||||
@@ -319,4 +320,4 @@ internal sealed class SubjectShareConfiguration : IEntityTypeConfiguration<Subje
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,4 @@ internal sealed class QuestionTaxonomyAssignmentConfiguration : IEntityTypeConfi
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,4 +156,4 @@ internal sealed class TenantFrontendConfigConfiguration : IEntityTypeConfigurati
|
||||
table.HasCheckConstraint("ck_tenant_frontend_configs_config_version", "config_version > 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ internal sealed class TenantSecretConfiguration : IEntityTypeConfiguration<Tenan
|
||||
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.SecretKey })
|
||||
.IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Purpose, entity.Provider, entity.Status });
|
||||
builder.ToTable(table => table.HasCheckConstraint(
|
||||
"ck_tenant_secrets_encryption_envelope",
|
||||
@@ -66,10 +67,7 @@ internal sealed class SmsVerificationCodeConfiguration : IEntityTypeConfiguratio
|
||||
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.ToTable(table => { table.HasCheckConstraint("ck_sms_verification_codes_attempts", "attempts >= 0"); });
|
||||
|
||||
builder.HasOne<Tenant>().WithMany()
|
||||
.HasForeignKey(entity => entity.TenantId)
|
||||
@@ -303,7 +301,8 @@ internal sealed class TenantClassMemberConfiguration : IEntityTypeConfiguration<
|
||||
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.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 });
|
||||
|
||||
@@ -353,7 +352,8 @@ internal sealed class TenantStudentFollowupConfiguration : IEntityTypeConfigurat
|
||||
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.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 })
|
||||
@@ -373,4 +373,4 @@ internal sealed class TenantStudentFollowupConfiguration : IEntityTypeConfigurat
|
||||
.HasForeignKey(entity => entity.CompletedBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,4 @@ internal static class MigrationBuilderTenantGuardExtensions
|
||||
{
|
||||
migrationBuilder.Sql(PostgreSqlSaasCatalogConstraintSql.DropOfferingVersionImmutabilityGuards);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
@@ -11,43 +10,25 @@ internal static class ModelBuilderExtensions
|
||||
{
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
property.SetColumnName(ToSnakeCase(property.Name));
|
||||
}
|
||||
foreach (var property in entityType.GetProperties()) property.SetColumnName(ToSnakeCase(property.Name));
|
||||
|
||||
foreach (var key in entityType.GetKeys())
|
||||
{
|
||||
if (key.GetName() is { } keyName)
|
||||
{
|
||||
key.SetName(ToSnakeCase(keyName));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var foreignKey in entityType.GetForeignKeys())
|
||||
{
|
||||
if (foreignKey.GetConstraintName() is { } constraintName)
|
||||
{
|
||||
foreignKey.SetConstraintName(ToSnakeCase(constraintName));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var index in entityType.GetIndexes())
|
||||
{
|
||||
if (index.GetDatabaseName() is { } indexName)
|
||||
{
|
||||
index.SetDatabaseName(ToSnakeCase(indexName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToSnakeCase(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value)) return value;
|
||||
|
||||
var builder = new StringBuilder(value.Length + 8);
|
||||
for (var index = 0; index < value.Length; index++)
|
||||
@@ -55,10 +36,8 @@ internal static class ModelBuilderExtensions
|
||||
var character = value[index];
|
||||
if (char.IsUpper(character) && index > 0 &&
|
||||
(char.IsLower(value[index - 1]) ||
|
||||
index + 1 < value.Length && char.IsLower(value[index + 1])))
|
||||
{
|
||||
(index + 1 < value.Length && char.IsLower(value[index + 1]))))
|
||||
builder.Append('_');
|
||||
}
|
||||
|
||||
builder.Append(char.ToLowerInvariant(character));
|
||||
}
|
||||
@@ -70,4 +49,4 @@ internal static class ModelBuilderExtensions
|
||||
internal sealed class SnakeCaseEnumConverter<TEnum>() : ValueConverter<TEnum, string>(
|
||||
value => ModelBuilderExtensions.ToSnakeCase(value.ToString()),
|
||||
value => Enum.Parse<TEnum>(value.Replace("_", string.Empty), true))
|
||||
where TEnum : struct, Enum;
|
||||
where TEnum : struct, Enum;
|
||||
@@ -1,21 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -32,4 +16,4 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<QuestionTaxonomyAssignment> QuestionTaxonomyAssignments => Set<QuestionTaxonomyAssignment>();
|
||||
public DbSet<ScorelineField> ScorelineFields => Set<ScorelineField>();
|
||||
public DbSet<ScorelineRecord> ScorelineRecords => Set<ScorelineRecord>();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -37,9 +22,15 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<CommerceReconciliationBatch> CommerceReconciliationBatches => Set<CommerceReconciliationBatch>();
|
||||
public DbSet<CommerceReconciliationItem> CommerceReconciliationItems => Set<CommerceReconciliationItem>();
|
||||
public DbSet<CommerceReconciliationIssue> CommerceReconciliationIssues => Set<CommerceReconciliationIssue>();
|
||||
public DbSet<CommerceReconciliationIssueEvent> CommerceReconciliationIssueEvents => Set<CommerceReconciliationIssueEvent>();
|
||||
|
||||
public DbSet<CommerceReconciliationIssueEvent> CommerceReconciliationIssueEvents =>
|
||||
Set<CommerceReconciliationIssueEvent>();
|
||||
|
||||
public DbSet<CommerceAdjustmentVoucher> CommerceAdjustmentVouchers => Set<CommerceAdjustmentVoucher>();
|
||||
public DbSet<CommerceAdjustmentVoucherEvent> CommerceAdjustmentVoucherEvents => Set<CommerceAdjustmentVoucherEvent>();
|
||||
|
||||
public DbSet<CommerceAdjustmentVoucherEvent> CommerceAdjustmentVoucherEvents =>
|
||||
Set<CommerceAdjustmentVoucherEvent>();
|
||||
|
||||
public DbSet<PointActivityTask> PointActivityTasks => Set<PointActivityTask>();
|
||||
public DbSet<PointActivityClaim> PointActivityClaims => Set<PointActivityClaim>();
|
||||
public DbSet<PointExchangeItem> PointExchangeItems => Set<PointExchangeItem>();
|
||||
@@ -56,5 +47,7 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<CommissionSettlement> CommissionSettlements => Set<CommissionSettlement>();
|
||||
public DbSet<CommissionSettlementItem> CommissionSettlementItems => Set<CommissionSettlementItem>();
|
||||
public DbSet<CommissionSettlementProof> CommissionSettlementProofs => Set<CommissionSettlementProof>();
|
||||
public DbSet<CommissionSettlementExportEvent> CommissionSettlementExportEvents => Set<CommissionSettlementExportEvent>();
|
||||
}
|
||||
|
||||
public DbSet<CommissionSettlementExportEvent> CommissionSettlementExportEvents =>
|
||||
Set<CommissionSettlementExportEvent>();
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -52,4 +37,4 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<TenantQuestionBankPreference> TenantQuestionBankPreferences => Set<TenantQuestionBankPreference>();
|
||||
public DbSet<TenantQuestionReference> TenantQuestionReferences => Set<TenantQuestionReference>();
|
||||
public DbSet<AiRecommendationReport> AiRecommendationReports => Set<AiRecommendationReport>();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -38,4 +22,4 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<PracticeSessionReportSection> PracticeSessionReportSections => Set<PracticeSessionReportSection>();
|
||||
public DbSet<DashboardDailyStat> DashboardDailyStats => Set<DashboardDailyStat>();
|
||||
public DbSet<RevenueDailyStat> RevenueDailyStats => Set<RevenueDailyStat>();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -36,7 +21,10 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<PlatformBackendRoleMenu> PlatformBackendRoleMenus => Set<PlatformBackendRoleMenu>();
|
||||
public DbSet<PlatformBackendUserRole> PlatformBackendUserRoles => Set<PlatformBackendUserRole>();
|
||||
public DbSet<AuthorizationScopeVersion> AuthorizationScopeVersions => Set<AuthorizationScopeVersion>();
|
||||
public DbSet<AuthorizationCacheInvalidation> AuthorizationCacheInvalidations => Set<AuthorizationCacheInvalidation>();
|
||||
|
||||
public DbSet<AuthorizationCacheInvalidation> AuthorizationCacheInvalidations =>
|
||||
Set<AuthorizationCacheInvalidation>();
|
||||
|
||||
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
|
||||
public DbSet<TenantLifecycleOperation> TenantLifecycleOperations => Set<TenantLifecycleOperation>();
|
||||
public DbSet<WorkerHeartbeat> WorkerHeartbeats => Set<WorkerHeartbeat>();
|
||||
@@ -50,17 +38,28 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<TenantBillingPolicy> TenantBillingPolicies => Set<TenantBillingPolicy>();
|
||||
public DbSet<TenantOwnerActivationGrant> TenantOwnerActivationGrants => Set<TenantOwnerActivationGrant>();
|
||||
public DbSet<PlatformOperationIdempotency> PlatformOperationIdempotencies => Set<PlatformOperationIdempotency>();
|
||||
public DbSet<PlatformBillingInvoiceReminder> PlatformBillingInvoiceReminders => Set<PlatformBillingInvoiceReminder>();
|
||||
|
||||
public DbSet<PlatformBillingInvoiceReminder> PlatformBillingInvoiceReminders =>
|
||||
Set<PlatformBillingInvoiceReminder>();
|
||||
|
||||
public DbSet<PlatformAuditAlertRule> PlatformAuditAlertRules => Set<PlatformAuditAlertRule>();
|
||||
public DbSet<PlatformAuditAlert> PlatformAuditAlerts => Set<PlatformAuditAlert>();
|
||||
public DbSet<PlatformBillingDunningNotificationChannel> PlatformBillingDunningNotificationChannels => Set<PlatformBillingDunningNotificationChannel>();
|
||||
public DbSet<PlatformBillingDunningNotificationEvent> PlatformBillingDunningNotificationEvents => Set<PlatformBillingDunningNotificationEvent>();
|
||||
|
||||
public DbSet<PlatformBillingDunningNotificationChannel> PlatformBillingDunningNotificationChannels =>
|
||||
Set<PlatformBillingDunningNotificationChannel>();
|
||||
|
||||
public DbSet<PlatformBillingDunningNotificationEvent> PlatformBillingDunningNotificationEvents =>
|
||||
Set<PlatformBillingDunningNotificationEvent>();
|
||||
|
||||
public DbSet<PlatformPaymentApp> PlatformPaymentApps => Set<PlatformPaymentApp>();
|
||||
public DbSet<PlatformPaymentChannel> PlatformPaymentChannels => Set<PlatformPaymentChannel>();
|
||||
public DbSet<PlatformApprovalPolicy> PlatformApprovalPolicies => Set<PlatformApprovalPolicy>();
|
||||
public DbSet<PlatformApprovalRequest> PlatformApprovalRequests => Set<PlatformApprovalRequest>();
|
||||
public DbSet<PlatformConfigurationDefinition> PlatformConfigurationDefinitions => Set<PlatformConfigurationDefinition>();
|
||||
|
||||
public DbSet<PlatformConfigurationDefinition> PlatformConfigurationDefinitions =>
|
||||
Set<PlatformConfigurationDefinition>();
|
||||
|
||||
public DbSet<PlatformConfigurationVersion> PlatformConfigurationVersions => Set<PlatformConfigurationVersion>();
|
||||
public DbSet<PlatformNotificationTemplate> PlatformNotificationTemplates => Set<PlatformNotificationTemplate>();
|
||||
public DbSet<PlatformNotificationDelivery> PlatformNotificationDeliveries => Set<PlatformNotificationDelivery>();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -43,5 +28,4 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<PocketBaseImportRun> PocketBaseImportRuns => Set<PocketBaseImportRun>();
|
||||
public DbSet<PocketBaseRawRecord> PocketBaseRawRecords => Set<PocketBaseRawRecord>();
|
||||
public DbSet<PocketBaseImportIssue> PocketBaseImportIssues => Set<PocketBaseImportIssue>();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
@@ -23,7 +9,6 @@ public sealed partial class TikuDbContext
|
||||
{
|
||||
public DbSet<Tenant> Tenants => Set<Tenant>();
|
||||
public new DbSet<User> Users => Set<User>();
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||
public DbSet<UserIdentity> UserIdentities => Set<UserIdentity>();
|
||||
public DbSet<TenantMembership> TenantMemberships => Set<TenantMembership>();
|
||||
public DbSet<TenantDomain> TenantDomains => Set<TenantDomain>();
|
||||
@@ -46,4 +31,5 @@ public sealed partial class TikuDbContext
|
||||
public DbSet<TenantStudentNote> TenantStudentNotes => Set<TenantStudentNote>();
|
||||
public DbSet<TenantStudentFollowup> TenantStudentFollowups => Set<TenantStudentFollowup>();
|
||||
public DbSet<StudentProfile> StudentProfiles => Set<StudentProfile>();
|
||||
}
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||
}
|
||||
@@ -3,222 +3,222 @@ namespace Tiku.Infrastructure.Persistence;
|
||||
internal static class PostgreSqlSaasCatalogConstraintSql
|
||||
{
|
||||
public const string CreateOfferingVersionImmutabilityGuards = """
|
||||
create or replace function tiku_guard_saas_offering_version_immutable()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
if old.status in ('published', 'retired') then
|
||||
raise exception 'published or retired SaaS offering versions cannot be deleted'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
create or replace function tiku_guard_saas_offering_version_immutable()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
if old.status in ('published', 'retired') then
|
||||
raise exception 'published or retired SaaS offering versions cannot be deleted'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
|
||||
return old;
|
||||
end if;
|
||||
return old;
|
||||
end if;
|
||||
|
||||
if old.status not in ('published', 'retired') then
|
||||
return new;
|
||||
end if;
|
||||
if old.status not in ('published', 'retired') then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
if old.status = 'published'
|
||||
and new.status = 'retired'
|
||||
and new.retired_at is not null
|
||||
and new.id is not distinct from old.id
|
||||
and new.offering_id is not distinct from old.offering_id
|
||||
and new.version is not distinct from old.version
|
||||
and new.billing_cycle is not distinct from old.billing_cycle
|
||||
and new.original_amount_cents is not distinct from old.original_amount_cents
|
||||
and new.amount_cents is not distinct from old.amount_cents
|
||||
and new.currency is not distinct from old.currency
|
||||
and new.effective_at is not distinct from old.effective_at
|
||||
and new.published_at is not distinct from old.published_at
|
||||
and new.metadata is not distinct from old.metadata
|
||||
and new.created_at is not distinct from old.created_at then
|
||||
return new;
|
||||
end if;
|
||||
if old.status = 'published'
|
||||
and new.status = 'retired'
|
||||
and new.retired_at is not null
|
||||
and new.id is not distinct from old.id
|
||||
and new.offering_id is not distinct from old.offering_id
|
||||
and new.version is not distinct from old.version
|
||||
and new.billing_cycle is not distinct from old.billing_cycle
|
||||
and new.original_amount_cents is not distinct from old.original_amount_cents
|
||||
and new.amount_cents is not distinct from old.amount_cents
|
||||
and new.currency is not distinct from old.currency
|
||||
and new.effective_at is not distinct from old.effective_at
|
||||
and new.published_at is not distinct from old.published_at
|
||||
and new.metadata is not distinct from old.metadata
|
||||
and new.created_at is not distinct from old.created_at then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'published or retired SaaS offering versions are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end;
|
||||
$$;
|
||||
raise exception 'published or retired SaaS offering versions are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end;
|
||||
$$;
|
||||
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_versions') is not null then
|
||||
execute 'create trigger trg_saas_offering_versions_published_immutable
|
||||
before update or delete on saas_offering_versions
|
||||
for each row execute function tiku_guard_saas_offering_version_immutable()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_versions') is not null then
|
||||
execute 'create trigger trg_saas_offering_versions_published_immutable
|
||||
before update or delete on saas_offering_versions
|
||||
for each row execute function tiku_guard_saas_offering_version_immutable()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
|
||||
public const string CreateOfferingVersionChildrenImmutabilityGuards = """
|
||||
create or replace function tiku_guard_saas_offering_version_child_immutable()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
version_status text;
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = old.offering_version_id;
|
||||
create or replace function tiku_guard_saas_offering_version_child_immutable()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
version_status text;
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = old.offering_version_id;
|
||||
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
|
||||
return old;
|
||||
end if;
|
||||
return old;
|
||||
end if;
|
||||
|
||||
if tg_op = 'UPDATE' and old.offering_version_id <> new.offering_version_id then
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = old.offering_version_id;
|
||||
if tg_op = 'UPDATE' and old.offering_version_id <> new.offering_version_id then
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = old.offering_version_id;
|
||||
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
end if;
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
end if;
|
||||
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = new.offering_version_id;
|
||||
select status into version_status
|
||||
from saas_offering_versions
|
||||
where id = new.offering_version_id;
|
||||
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
if version_status is distinct from 'draft' then
|
||||
raise exception 'published or retired SaaS offering version children are immutable'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_saas_offering_versions_published_immutable';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_version_features') is not null then
|
||||
execute 'create trigger trg_saas_offering_version_features_published_immutable
|
||||
before insert or update or delete on saas_offering_version_features
|
||||
for each row execute function tiku_guard_saas_offering_version_child_immutable()';
|
||||
end if;
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_version_features') is not null then
|
||||
execute 'create trigger trg_saas_offering_version_features_published_immutable
|
||||
before insert or update or delete on saas_offering_version_features
|
||||
for each row execute function tiku_guard_saas_offering_version_child_immutable()';
|
||||
end if;
|
||||
|
||||
if to_regclass('saas_offering_version_limits') is not null then
|
||||
execute 'create trigger trg_saas_offering_version_limits_published_immutable
|
||||
before insert or update or delete on saas_offering_version_limits
|
||||
for each row execute function tiku_guard_saas_offering_version_child_immutable()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
if to_regclass('saas_offering_version_limits') is not null then
|
||||
execute 'create trigger trg_saas_offering_version_limits_published_immutable
|
||||
before insert or update or delete on saas_offering_version_limits
|
||||
for each row execute function tiku_guard_saas_offering_version_child_immutable()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
|
||||
public const string CreateSubscriptionOfferingTypeGuards = """
|
||||
create or replace function tiku_guard_saas_subscription_offering_type()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
offering_type text;
|
||||
source_version_id uuid;
|
||||
begin
|
||||
if tg_table_name = 'tenant_saas_subscriptions' then
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.base_offering_version_id;
|
||||
create or replace function tiku_guard_saas_subscription_offering_type()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
offering_type text;
|
||||
source_version_id uuid;
|
||||
begin
|
||||
if tg_table_name = 'tenant_saas_subscriptions' then
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.base_offering_version_id;
|
||||
|
||||
if offering_type is distinct from 'base_plan' then
|
||||
raise exception 'subscription base offering version must belong to a base plan'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_base_plan';
|
||||
end if;
|
||||
if offering_type is distinct from 'base_plan' then
|
||||
raise exception 'subscription base offering version must belong to a base plan'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_base_plan';
|
||||
end if;
|
||||
|
||||
if new.scheduled_base_offering_version_id is not null then
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.scheduled_base_offering_version_id;
|
||||
if offering_type is distinct from 'base_plan' then
|
||||
raise exception 'scheduled subscription offering version must belong to a base plan'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_base_plan';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
if new.scheduled_base_offering_version_id is not null then
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.scheduled_base_offering_version_id;
|
||||
if offering_type is distinct from 'base_plan' then
|
||||
raise exception 'scheduled subscription offering version must belong to a base plan'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_base_plan';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.offering_version_id;
|
||||
if offering_type is distinct from new.item_type::text then
|
||||
raise exception 'subscription item type must match its offering type'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_item_offering_type';
|
||||
end if;
|
||||
select o.type into offering_type
|
||||
from saas_offering_versions v
|
||||
join saas_offerings o on o.id = v.offering_id
|
||||
where v.id = new.offering_version_id;
|
||||
if offering_type is distinct from new.item_type::text then
|
||||
raise exception 'subscription item type must match its offering type'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_item_offering_type';
|
||||
end if;
|
||||
|
||||
if new.source_order_item_id is not null then
|
||||
select offering_version_id into source_version_id
|
||||
from platform_billing_order_items
|
||||
where tenant_id = new.tenant_id and id = new.source_order_item_id;
|
||||
if source_version_id is distinct from new.offering_version_id then
|
||||
raise exception 'subscription item must match its order item snapshot'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_item_order_snapshot';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
if new.source_order_item_id is not null then
|
||||
select offering_version_id into source_version_id
|
||||
from platform_billing_order_items
|
||||
where tenant_id = new.tenant_id and id = new.source_order_item_id;
|
||||
if source_version_id is distinct from new.offering_version_id then
|
||||
raise exception 'subscription item must match its order item snapshot'
|
||||
using errcode = '23514', constraint = 'ck_tenant_saas_subscription_item_order_snapshot';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('tenant_saas_subscriptions') is not null then
|
||||
execute 'create trigger trg_tenant_saas_subscriptions_base_plan
|
||||
before insert or update on tenant_saas_subscriptions
|
||||
for each row execute function tiku_guard_saas_subscription_offering_type()';
|
||||
end if;
|
||||
if to_regclass('tenant_saas_subscription_items') is not null then
|
||||
execute 'create trigger trg_tenant_saas_subscription_items_offering_type
|
||||
before insert or update on tenant_saas_subscription_items
|
||||
for each row execute function tiku_guard_saas_subscription_offering_type()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('tenant_saas_subscriptions') is not null then
|
||||
execute 'create trigger trg_tenant_saas_subscriptions_base_plan
|
||||
before insert or update on tenant_saas_subscriptions
|
||||
for each row execute function tiku_guard_saas_subscription_offering_type()';
|
||||
end if;
|
||||
if to_regclass('tenant_saas_subscription_items') is not null then
|
||||
execute 'create trigger trg_tenant_saas_subscription_items_offering_type
|
||||
before insert or update on tenant_saas_subscription_items
|
||||
for each row execute function tiku_guard_saas_subscription_offering_type()';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
""";
|
||||
|
||||
public const string DropOfferingVersionImmutabilityGuards = """
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_version_features') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_version_features_published_immutable on saas_offering_version_features';
|
||||
end if;
|
||||
do $guard$
|
||||
begin
|
||||
if to_regclass('saas_offering_version_features') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_version_features_published_immutable on saas_offering_version_features';
|
||||
end if;
|
||||
|
||||
if to_regclass('saas_offering_version_limits') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_version_limits_published_immutable on saas_offering_version_limits';
|
||||
end if;
|
||||
if to_regclass('saas_offering_version_limits') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_version_limits_published_immutable on saas_offering_version_limits';
|
||||
end if;
|
||||
|
||||
if to_regclass('saas_offering_versions') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_versions_published_immutable on saas_offering_versions';
|
||||
end if;
|
||||
if to_regclass('saas_offering_versions') is not null then
|
||||
execute 'drop trigger if exists trg_saas_offering_versions_published_immutable on saas_offering_versions';
|
||||
end if;
|
||||
|
||||
if to_regclass('tenant_saas_subscription_items') is not null then
|
||||
execute 'drop trigger if exists trg_tenant_saas_subscription_items_offering_type on tenant_saas_subscription_items';
|
||||
end if;
|
||||
if to_regclass('tenant_saas_subscription_items') is not null then
|
||||
execute 'drop trigger if exists trg_tenant_saas_subscription_items_offering_type on tenant_saas_subscription_items';
|
||||
end if;
|
||||
|
||||
if to_regclass('tenant_saas_subscriptions') is not null then
|
||||
execute 'drop trigger if exists trg_tenant_saas_subscriptions_base_plan on tenant_saas_subscriptions';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
if to_regclass('tenant_saas_subscriptions') is not null then
|
||||
execute 'drop trigger if exists trg_tenant_saas_subscriptions_base_plan on tenant_saas_subscriptions';
|
||||
end if;
|
||||
end
|
||||
$guard$;
|
||||
|
||||
drop function if exists tiku_guard_saas_offering_version_child_immutable();
|
||||
drop function if exists tiku_guard_saas_offering_version_immutable();
|
||||
drop function if exists tiku_guard_saas_subscription_offering_type();
|
||||
""";
|
||||
}
|
||||
drop function if exists tiku_guard_saas_offering_version_child_immutable();
|
||||
drop function if exists tiku_guard_saas_offering_version_immutable();
|
||||
drop function if exists tiku_guard_saas_subscription_offering_type();
|
||||
""";
|
||||
}
|
||||
@@ -3,87 +3,87 @@ namespace Tiku.Infrastructure.Persistence;
|
||||
internal static class PostgreSqlTenantConstraintSql
|
||||
{
|
||||
public const string CreateTenantQuestionReferenceGuard = """
|
||||
create or replace function tiku_guard_tenant_question_reference()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
owner_mode text;
|
||||
begin
|
||||
select mode into owner_mode
|
||||
from tenants
|
||||
where id = new.question_owner_tenant_id;
|
||||
create or replace function tiku_guard_tenant_question_reference()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
owner_mode text;
|
||||
begin
|
||||
select mode into owner_mode
|
||||
from tenants
|
||||
where id = new.question_owner_tenant_id;
|
||||
|
||||
if owner_mode = 'platform_owned' then
|
||||
if new.source <> 'platform' then
|
||||
raise exception 'tenant question reference to platform question must use platform source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
if owner_mode = 'platform_owned' then
|
||||
if new.source <> 'platform' then
|
||||
raise exception 'tenant question reference to platform question must use platform source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
if new.question_owner_tenant_id = new.tenant_id then
|
||||
if new.source <> 'tenant' then
|
||||
raise exception 'tenant question reference to tenant-owned question must use tenant source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
if new.question_owner_tenant_id = new.tenant_id then
|
||||
if new.source <> 'tenant' then
|
||||
raise exception 'tenant question reference to tenant-owned question must use tenant source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'tenant question reference cannot point to another tenant private question'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
raise exception 'tenant question reference cannot point to another tenant private question'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_tenant_question_references_platform_or_self_owner
|
||||
before insert or update of tenant_id, question_owner_tenant_id, question_id, source
|
||||
on tenant_question_references
|
||||
for each row
|
||||
execute function tiku_guard_tenant_question_reference();
|
||||
""";
|
||||
create trigger trg_tenant_question_references_platform_or_self_owner
|
||||
before insert or update of tenant_id, question_owner_tenant_id, question_id, source
|
||||
on tenant_question_references
|
||||
for each row
|
||||
execute function tiku_guard_tenant_question_reference();
|
||||
""";
|
||||
|
||||
public const string CreateTaxonomyParentGuard = """
|
||||
create or replace function tiku_guard_taxonomy_parent_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
parent_owner_mode text;
|
||||
begin
|
||||
if new.parent_owner_tenant_id is null and new.parent_id is null then
|
||||
return new;
|
||||
end if;
|
||||
create or replace function tiku_guard_taxonomy_parent_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
parent_owner_mode text;
|
||||
begin
|
||||
if new.parent_owner_tenant_id is null and new.parent_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select mode into parent_owner_mode
|
||||
from tenants
|
||||
where id = new.parent_owner_tenant_id;
|
||||
select mode into parent_owner_mode
|
||||
from tenants
|
||||
where id = new.parent_owner_tenant_id;
|
||||
|
||||
if parent_owner_mode = 'platform_owned' or new.parent_owner_tenant_id = new.tenant_id then
|
||||
return new;
|
||||
end if;
|
||||
if parent_owner_mode = 'platform_owned' or new.parent_owner_tenant_id = new.tenant_id then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'taxonomy parent must belong to platform or current tenant'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_taxonomy_nodes_parent_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
raise exception 'taxonomy parent must belong to platform or current tenant'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_taxonomy_nodes_parent_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_taxonomy_nodes_parent_platform_or_self_owner
|
||||
before insert or update of tenant_id, parent_owner_tenant_id, parent_id
|
||||
on taxonomy_nodes
|
||||
for each row
|
||||
execute function tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
create trigger trg_taxonomy_nodes_parent_platform_or_self_owner
|
||||
before insert or update of tenant_id, parent_owner_tenant_id, parent_id
|
||||
on taxonomy_nodes
|
||||
for each row
|
||||
execute function tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
|
||||
public const string DropTenantGuards = """
|
||||
drop trigger if exists trg_tenant_question_references_platform_or_self_owner on tenant_question_references;
|
||||
drop function if exists tiku_guard_tenant_question_reference();
|
||||
drop trigger if exists trg_taxonomy_nodes_parent_platform_or_self_owner on taxonomy_nodes;
|
||||
drop function if exists tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
}
|
||||
drop trigger if exists trg_tenant_question_references_platform_or_self_owner on tenant_question_references;
|
||||
drop function if exists tiku_guard_tenant_question_reference();
|
||||
drop trigger if exists trg_taxonomy_nodes_parent_platform_or_self_owner on taxonomy_nodes;
|
||||
drop function if exists tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
}
|
||||
@@ -25,19 +25,13 @@ public sealed class TenantIsolationSaveChangesInterceptor(ITenantContext tenantC
|
||||
|
||||
private void Enforce(DbContext? dbContext)
|
||||
{
|
||||
if (dbContext is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (dbContext is null) return;
|
||||
|
||||
foreach (var entry in dbContext.ChangeTracker.Entries()
|
||||
.Where(entry => entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
|
||||
{
|
||||
var tenantProperty = entry.Metadata.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (tenantProperty?.ClrType != typeof(Guid)) continue;
|
||||
|
||||
var property = entry.Property("TenantId");
|
||||
var currentTenantId = (Guid)(property.CurrentValue ?? Guid.Empty);
|
||||
@@ -45,11 +39,9 @@ public sealed class TenantIsolationSaveChangesInterceptor(ITenantContext tenantC
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
if (!tenantContext.TenantId.HasValue && !tenantContext.IsSystem)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot add tenant-owned data without a resolved tenant.");
|
||||
}
|
||||
|
||||
if (currentTenantId == Guid.Empty && tenantContext.TenantId.HasValue)
|
||||
{
|
||||
@@ -58,32 +50,26 @@ public sealed class TenantIsolationSaveChangesInterceptor(ITenantContext tenantC
|
||||
}
|
||||
|
||||
if (!tenantContext.IsSystem && currentTenantId != tenantContext.TenantId)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot add data for another tenant.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.IsModified || currentTenantId != originalTenantId)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Tenant ownership cannot be changed.");
|
||||
}
|
||||
|
||||
if (!tenantContext.IsSystem &&
|
||||
(!tenantContext.TenantId.HasValue || originalTenantId != tenantContext.TenantId.Value))
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot modify or delete data owned by another tenant.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantIsolationException(Type entityType, string message)
|
||||
: InvalidOperationException($"Tenant isolation rejected {entityType.Name}: {message}");
|
||||
: InvalidOperationException($"Tenant isolation rejected {entityType.Name}: {message}");
|
||||
@@ -1,21 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Import;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -40,6 +30,7 @@ public sealed partial class TikuDbContext(
|
||||
context.InitializeSystem(null, "Direct DbContext construction for model tooling");
|
||||
return context;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@@ -64,10 +55,7 @@ public sealed partial class TikuDbContext(
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var tenantProperty = entityType.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid) || entityType.BaseType is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (tenantProperty?.ClrType != typeof(Guid) || entityType.BaseType is not null) continue;
|
||||
|
||||
applyMethod.MakeGenericMethod(entityType.ClrType).Invoke(this, [modelBuilder]);
|
||||
}
|
||||
@@ -89,57 +77,38 @@ public sealed partial class TikuDbContext(
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var tenantProperty = entityType.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (tenantProperty?.ClrType != typeof(Guid)) continue;
|
||||
|
||||
if (!typeof(ITenantOwned).IsAssignableFrom(entityType.ClrType))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tenant entity '{entityType.ClrType.Name}' must implement {nameof(ITenantOwned)}.");
|
||||
}
|
||||
|
||||
if (!entityType.GetDeclaredQueryFilters().Any())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tenant entity '{entityType.ClrType.Name}' does not have a tenant query filter.");
|
||||
}
|
||||
|
||||
foreach (var index in entityType.GetDeclaredIndexes().Where(index => index.IsUnique))
|
||||
{
|
||||
if (index.Properties.All(property => property.Name != "TenantId") &&
|
||||
index.FindAnnotation("Tiku:GlobalUnique")?.Value is not true)
|
||||
{
|
||||
invalidUniqueIndexes.Add(
|
||||
$"{entityType.ClrType.Name}({string.Join(",", index.Properties.Select(property => property.Name))})");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var foreignKey in entityType.GetDeclaredForeignKeys().Where(foreignKey =>
|
||||
typeof(ITenantOwned).IsAssignableFrom(foreignKey.PrincipalEntityType.ClrType)))
|
||||
{
|
||||
if (foreignKey.PrincipalKey.Properties.All(property => property.Name != "TenantId"))
|
||||
{
|
||||
invalidTenantForeignKeys.Add(
|
||||
$"{entityType.ClrType.Name}->{foreignKey.PrincipalEntityType.ClrType.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidUniqueIndexes.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unique indexes on tenant entities must include TenantId: {string.Join("; ", invalidUniqueIndexes)}");
|
||||
}
|
||||
|
||||
|
||||
if (invalidTenantForeignKeys.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Foreign keys between tenant entities must use a tenant-qualified principal key: {string.Join("; ", invalidTenantForeignKeys)}");
|
||||
}
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
@@ -165,25 +134,15 @@ public sealed partial class TikuDbContext(
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<User>().Where(entry => entry.State == EntityState.Modified))
|
||||
{
|
||||
if (entry.Property(user => user.Status).IsModified ||
|
||||
entry.Property(user => user.PasswordHash).IsModified)
|
||||
{
|
||||
entry.Entity.SecurityStamp = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<IHasTimestamps>())
|
||||
{
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.CreatedAt = now;
|
||||
}
|
||||
if (entry.State == EntityState.Added) entry.Entity.CreatedAt = now;
|
||||
|
||||
if (entry.State is EntityState.Added or EntityState.Modified)
|
||||
{
|
||||
entry.Entity.UpdatedAt = now;
|
||||
}
|
||||
if (entry.State is EntityState.Added or EntityState.Modified) entry.Entity.UpdatedAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user