forked from xiongyuxing/tiku-backend.net
feat: add learning reports and practice access persistence
This commit is contained in:
@@ -58,6 +58,7 @@ public sealed class ContentNode : AuditableTenantEntity
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsSelectable { get; set; } = true;
|
||||
public bool IsLeaf { get; set; }
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
public Guid? CreatedBy { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
@@ -100,6 +101,7 @@ public sealed class QuestionCollection : AuditableTenantEntity
|
||||
public QuestionCollectionType CollectionType { get; set; } = QuestionCollectionType.Dynamic;
|
||||
public QuestionCollectionSourceType SourceType { get; set; } = QuestionCollectionSourceType.Filters;
|
||||
public JsonElement Filters { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
public int QuestionCount { get; set; }
|
||||
public decimal? TotalScore { get; set; }
|
||||
public int? DurationMinutes { get; set; }
|
||||
@@ -163,6 +165,7 @@ public sealed class PracticeBlueprint : AuditableTenantEntity
|
||||
public decimal? PassScore { get; set; }
|
||||
public JsonElement Sections { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement Rules { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
public ContentStatus Status { get; set; } = ContentStatus.Active;
|
||||
public int SortOrder { get; set; }
|
||||
public Guid? CreatedBy { get; set; }
|
||||
|
||||
@@ -20,9 +20,20 @@ public sealed class PracticeSession : TenantEntity
|
||||
public int? DurationMinutes { get; set; }
|
||||
public decimal? TotalScore { get; set; }
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public PracticeAccessMode AccessMode { get; set; } = PracticeAccessMode.Free;
|
||||
public Guid? AccessEntitlementId { get; set; }
|
||||
public int ConsumedFreeQuota { get; set; }
|
||||
public JsonElement AccessSnapshot { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public enum PracticeAccessMode
|
||||
{
|
||||
Free,
|
||||
Svip,
|
||||
Staff
|
||||
}
|
||||
|
||||
public sealed class AnswerRecord : TenantEntity
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
229
Tiku.Domain/Learning/LearningReportEntities.cs
Normal file
229
Tiku.Domain/Learning/LearningReportEntities.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using System.Text.Json;
|
||||
using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Domain.Learning;
|
||||
|
||||
public sealed class ExamDate : AuditableTenantEntity
|
||||
{
|
||||
public Guid? RegionId { get; set; }
|
||||
public Guid? SchoolId { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
public string ExamName { get; set; } = string.Empty;
|
||||
public DateTimeOffset? ExamAt { get; set; }
|
||||
public string? ExamType { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class Report : AuditableTenantEntity
|
||||
{
|
||||
public Guid? QuestionId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? HandledBy { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
public ReportType? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public ReportStatus Status { get; set; } = ReportStatus.Pending;
|
||||
public ReportPriority Priority { get; set; } = ReportPriority.Normal;
|
||||
public DateTimeOffset? HandledAt { get; set; }
|
||||
public string? Resolution { get; set; }
|
||||
public string? Contact { get; set; }
|
||||
public JsonElement Attachments { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class ReportStatusEvent : Entity
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid ReportId { get; set; }
|
||||
public ReportStatus? FromStatus { get; set; }
|
||||
public ReportStatus ToStatus { get; set; } = ReportStatus.Pending;
|
||||
public string? Note { get; set; }
|
||||
public Guid? ActorUserId { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class UserScoreEvent : Entity
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public UserScoreEventType EventType { get; set; } = UserScoreEventType.ManualAdjust;
|
||||
public int Points { get; set; }
|
||||
public int BalanceAfter { get; set; }
|
||||
public string? SourceType { get; set; }
|
||||
public Guid? SourceId { get; set; }
|
||||
public string? IdempotencyKey { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PracticeDailyUsage : AuditableTenantEntity
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public DateOnly UsageDate { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
public PracticeUsageScopeType ScopeType { get; set; } = PracticeUsageScopeType.Tenant;
|
||||
public Guid? ScopeId { get; set; }
|
||||
public int FreeLimit { get; set; } = 25;
|
||||
public int UsedCount { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class PracticeAccessEvent : Entity
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? PracticeSessionId { get; set; }
|
||||
public PracticeAccessEventType EventType { get; set; } = PracticeAccessEventType.SessionCreated;
|
||||
public PracticeAccessEventMode AccessMode { get; set; } = PracticeAccessEventMode.Free;
|
||||
public int RequestedCount { get; set; }
|
||||
public int GrantedCount { get; set; }
|
||||
public int ConsumedFreeQuota { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public PracticeUsageScopeType? ScopeType { get; set; }
|
||||
public Guid? ScopeId { get; set; }
|
||||
public Guid? EntitlementId { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PracticeSessionReport : AuditableTenantEntity
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public Guid PracticeSessionId { get; set; }
|
||||
public Guid? BlueprintId { get; set; }
|
||||
public Guid? CollectionId { get; set; }
|
||||
public string Mode { get; set; } = "mock_exam";
|
||||
public int TotalQuestions { get; set; }
|
||||
public int AnsweredCount { get; set; }
|
||||
public int CorrectCount { get; set; }
|
||||
public int WrongCount { get; set; }
|
||||
public int UnansweredCount { get; set; }
|
||||
public decimal Score { get; set; }
|
||||
public decimal TotalScore { get; set; }
|
||||
public decimal Accuracy { get; set; }
|
||||
public int DurationSeconds { get; set; }
|
||||
public DateTimeOffset? StartedAt { get; set; }
|
||||
public DateTimeOffset SubmittedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public JsonElement SectionStats { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement QuestionResults { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement WrongQuestionIds { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class PracticeSessionReportSection : AuditableTenantEntity
|
||||
{
|
||||
public Guid ReportId { get; set; }
|
||||
public Guid PracticeSessionId { get; set; }
|
||||
public string SectionKey { get; set; } = string.Empty;
|
||||
public string? SectionName { get; set; }
|
||||
public string? QuestionType { get; set; }
|
||||
public int QuestionCount { get; set; }
|
||||
public int AnsweredCount { get; set; }
|
||||
public int CorrectCount { get; set; }
|
||||
public int WrongCount { get; set; }
|
||||
public int UnansweredCount { get; set; }
|
||||
public decimal Score { get; set; }
|
||||
public decimal TotalScore { get; set; }
|
||||
public decimal Accuracy { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class DashboardDailyStat : AuditableTenantEntity
|
||||
{
|
||||
public Guid? RegionId { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
public string? LegacyRegionId { get; set; }
|
||||
public DateOnly StatDate { get; set; }
|
||||
public int NewUsers { get; set; }
|
||||
public int NewQuestions { get; set; }
|
||||
public int NewOrders { get; set; }
|
||||
public int NewRevenueCents { get; set; }
|
||||
public int ActiveUsers { get; set; }
|
||||
public DateTimeOffset? RebuiltAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RevenueDailyStat : AuditableTenantEntity
|
||||
{
|
||||
public Guid? RegionId { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
public string? LegacyRegionId { get; set; }
|
||||
public DateOnly StatDate { get; set; }
|
||||
public string? SaleType { get; set; }
|
||||
public int RealRevenueCents { get; set; }
|
||||
public int OrderCount { get; set; }
|
||||
public int CodeCount { get; set; }
|
||||
public int CodeUsed { get; set; }
|
||||
public int CodeEstimated { get; set; }
|
||||
public int EstimatedRevenueCents { get; set; }
|
||||
public DateTimeOffset? RebuiltAt { get; set; }
|
||||
}
|
||||
|
||||
public enum ReportType
|
||||
{
|
||||
QuestionError,
|
||||
ContentError,
|
||||
VideoError,
|
||||
AssetError,
|
||||
SystemBug,
|
||||
Suggestion,
|
||||
Other
|
||||
}
|
||||
|
||||
public enum ReportStatus
|
||||
{
|
||||
Pending,
|
||||
Accepted,
|
||||
Rejected,
|
||||
Resolved,
|
||||
Closed
|
||||
}
|
||||
|
||||
public enum ReportPriority
|
||||
{
|
||||
Low,
|
||||
Normal,
|
||||
High,
|
||||
Urgent
|
||||
}
|
||||
|
||||
public enum UserScoreEventType
|
||||
{
|
||||
CheckIn,
|
||||
ManualAdjust,
|
||||
FeedbackReward,
|
||||
ActivityReward,
|
||||
RedeemCost
|
||||
}
|
||||
|
||||
public enum PracticeUsageScopeType
|
||||
{
|
||||
Tenant,
|
||||
Region,
|
||||
Subject,
|
||||
QuestionBank,
|
||||
ContentEntry,
|
||||
ContentNode,
|
||||
Collection,
|
||||
Blueprint
|
||||
}
|
||||
|
||||
public enum PracticeAccessEventType
|
||||
{
|
||||
SessionCreated,
|
||||
SessionDenied,
|
||||
QuotaConsumed
|
||||
}
|
||||
|
||||
public enum PracticeAccessEventMode
|
||||
{
|
||||
Free,
|
||||
Svip,
|
||||
Staff,
|
||||
Denied
|
||||
}
|
||||
@@ -60,6 +60,7 @@ internal sealed class ContentNodeConfiguration : IEntityTypeConfiguration<Conten
|
||||
builder.Property(entity => entity.MarkerType).HasNullableSnakeCaseEnum();
|
||||
builder.Property(entity => entity.MarkerConfig).IsJson("{}");
|
||||
builder.Property(entity => entity.Path).HasColumnType("ltree");
|
||||
builder.Property(entity => entity.AccessRules).IsJson("{}");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.EntryId, entity.NodeKey }).IsUnique();
|
||||
@@ -110,6 +111,7 @@ internal sealed class QuestionCollectionConfiguration : IEntityTypeConfiguration
|
||||
builder.Property(entity => entity.CollectionType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.SourceType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Filters).IsJson("{}");
|
||||
builder.Property(entity => entity.AccessRules).IsJson("{}");
|
||||
builder.Property(entity => entity.TotalScore).HasPrecision(8, 2);
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
@@ -213,6 +215,7 @@ internal sealed class PracticeBlueprintConfiguration : IEntityTypeConfiguration<
|
||||
builder.Property(entity => entity.PassScore).HasPrecision(8, 2);
|
||||
builder.Property(entity => entity.Sections).IsJson("[]");
|
||||
builder.Property(entity => entity.Rules).IsJson("{}");
|
||||
builder.Property(entity => entity.AccessRules).IsJson("{}");
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new
|
||||
|
||||
@@ -19,9 +19,20 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<Pr
|
||||
builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(entity => entity.QuestionIds).IsJson("[]");
|
||||
builder.Property(entity => entity.TotalScore).HasPrecision(8, 2);
|
||||
builder.Property(entity => entity.AccessMode)
|
||||
.HasSnakeCaseEnum()
|
||||
.HasDefaultValue(PracticeAccessMode.Free);
|
||||
builder.Property(entity => entity.AccessSnapshot).IsJson("{}");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StartedAt });
|
||||
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint(
|
||||
"ck_practice_sessions_consumed_free_quota",
|
||||
"consumed_free_quota >= 0");
|
||||
});
|
||||
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Configurations;
|
||||
|
||||
internal sealed class ExamDateConfiguration : IEntityTypeConfiguration<ExamDate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ExamDate> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("exam_dates");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
||||
builder.Property(entity => entity.ExamName).HasMaxLength(300);
|
||||
builder.Property(entity => entity.ExamType).HasMaxLength(50);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.SchoolId, entity.ExamAt });
|
||||
|
||||
builder.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<School>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.SchoolId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ReportConfiguration : IEntityTypeConfiguration<Report>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Report> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("reports");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
||||
builder.Property(entity => entity.Type).HasNullableSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Title).HasMaxLength(300);
|
||||
builder.Property(entity => entity.Category).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Priority).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Contact).HasMaxLength(300);
|
||||
builder.Property(entity => entity.Attachments).IsJson("[]");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.QuestionId, entity.CreatedAt })
|
||||
.HasFilter("question_id is not null");
|
||||
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.HandledBy)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ReportStatusEventConfiguration : IEntityTypeConfiguration<ReportStatusEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ReportStatusEvent> builder)
|
||||
{
|
||||
builder.ConfigureEntity("report_status_events");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.Property(entity => entity.FromStatus).HasNullableSnakeCaseEnum();
|
||||
builder.Property(entity => entity.ToStatus).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.ReportId, entity.CreatedAt });
|
||||
|
||||
builder.HasOne<Tenant>().WithMany()
|
||||
.HasForeignKey(entity => entity.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Report>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.ReportId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.ActorUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UserScoreEventConfiguration : IEntityTypeConfiguration<UserScoreEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserScoreEvent> builder)
|
||||
{
|
||||
builder.ConfigureEntity("user_score_events");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.Property(entity => entity.EventType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.SourceType).HasMaxLength(100);
|
||||
builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.IdempotencyKey }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
|
||||
|
||||
builder.HasOne<Tenant>().WithMany()
|
||||
.HasForeignKey(entity => entity.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PracticeDailyUsageConfiguration : IEntityTypeConfiguration<PracticeDailyUsage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PracticeDailyUsage> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("practice_daily_usage");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.UsageDate).HasDefaultValueSql("current_date");
|
||||
builder.Property(entity => entity.ScopeType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.UsageDate });
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.UserId,
|
||||
entity.UsageDate,
|
||||
entity.ScopeType,
|
||||
entity.ScopeId
|
||||
}).IsUnique().AreNullsDistinct(false);
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_practice_daily_usage_free_limit", "free_limit >= 0");
|
||||
table.HasCheckConstraint("ck_practice_daily_usage_used_count", "used_count >= 0");
|
||||
});
|
||||
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PracticeAccessEventConfiguration : IEntityTypeConfiguration<PracticeAccessEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PracticeAccessEvent> builder)
|
||||
{
|
||||
builder.ConfigureEntity("practice_access_events");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.Property(entity => entity.EventType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.AccessMode).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.ScopeType).HasNullableSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Reason).HasMaxLength(1000);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_practice_access_events_requested_count", "requested_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_access_events_granted_count", "granted_count >= 0");
|
||||
table.HasCheckConstraint("ck_practice_access_events_consumed_free_quota", "consumed_free_quota >= 0");
|
||||
});
|
||||
|
||||
builder.HasOne<Tenant>().WithMany()
|
||||
.HasForeignKey(entity => entity.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<PracticeSession>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PracticeSessionReportConfiguration : IEntityTypeConfiguration<PracticeSessionReport>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PracticeSessionReport> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("practice_session_reports");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.Mode).HasMaxLength(50);
|
||||
builder.Property(entity => entity.Score).HasPrecision(10, 2);
|
||||
builder.Property(entity => entity.TotalScore).HasPrecision(10, 2);
|
||||
builder.Property(entity => entity.Accuracy).HasPrecision(6, 4);
|
||||
builder.Property(entity => entity.SubmittedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(entity => entity.SectionStats).IsJson("[]");
|
||||
builder.Property(entity => entity.QuestionResults).IsJson("[]");
|
||||
builder.Property(entity => entity.WrongQuestionIds).IsJson("[]");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.SubmittedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.BlueprintId, entity.SubmittedAt })
|
||||
.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_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");
|
||||
});
|
||||
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PracticeSession>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PracticeBlueprint>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.BlueprintId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionCollection>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.CollectionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PracticeSessionReportSectionConfiguration : IEntityTypeConfiguration<PracticeSessionReportSection>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PracticeSessionReportSection> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("practice_session_report_sections");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.SectionKey).HasMaxLength(100);
|
||||
builder.Property(entity => entity.SectionName).HasMaxLength(300);
|
||||
builder.Property(entity => entity.QuestionType).HasMaxLength(50);
|
||||
builder.Property(entity => entity.Score).HasPrecision(10, 2);
|
||||
builder.Property(entity => entity.TotalScore).HasPrecision(10, 2);
|
||||
builder.Property(entity => entity.Accuracy).HasPrecision(6, 4);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.ReportId, entity.SectionKey }).IsUnique();
|
||||
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_scores", "score >= 0 and total_score >= 0");
|
||||
table.HasCheckConstraint("ck_practice_session_report_sections_accuracy", "accuracy >= 0 and accuracy <= 1");
|
||||
});
|
||||
|
||||
builder.HasOne<PracticeSessionReport>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.ReportId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<PracticeSession>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DashboardDailyStatConfiguration : IEntityTypeConfiguration<DashboardDailyStat>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DashboardDailyStat> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("dashboard_daily_stats");
|
||||
builder.ConfigureTimestamps();
|
||||
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.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RevenueDailyStatConfiguration : IEntityTypeConfiguration<RevenueDailyStat>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RevenueDailyStat> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("revenue_daily_stats");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
||||
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.HasOne<Region>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RegionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,764 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLearningReportsAndPracticeAccessPersistence : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<JsonElement>(
|
||||
name: "access_rules",
|
||||
table: "question_collections",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::jsonb");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "access_entitlement_id",
|
||||
table: "practice_sessions",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "access_mode",
|
||||
table: "practice_sessions",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false,
|
||||
defaultValue: "free");
|
||||
|
||||
migrationBuilder.AddColumn<JsonElement>(
|
||||
name: "access_snapshot",
|
||||
table: "practice_sessions",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::jsonb");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "consumed_free_quota",
|
||||
table: "practice_sessions",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<JsonElement>(
|
||||
name: "access_rules",
|
||||
table: "practice_blueprints",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::jsonb");
|
||||
|
||||
migrationBuilder.AddColumn<JsonElement>(
|
||||
name: "access_rules",
|
||||
table: "content_nodes",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::jsonb");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "dashboard_daily_stats",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
legacy_region_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
stat_date = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
new_users = table.Column<int>(type: "integer", nullable: false),
|
||||
new_questions = table.Column<int>(type: "integer", nullable: false),
|
||||
new_orders = table.Column<int>(type: "integer", nullable: false),
|
||||
new_revenue_cents = table.Column<int>(type: "integer", nullable: false),
|
||||
active_users = table.Column<int>(type: "integer", nullable: false),
|
||||
rebuilt_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_dashboard_daily_stats", x => x.id);
|
||||
table.UniqueConstraint("ak_dashboard_daily_stats_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_dashboard_daily_stats_regions_tenant_id_region_id",
|
||||
columns: x => new { x.tenant_id, x.region_id },
|
||||
principalTable: "regions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_dashboard_daily_stats_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "exam_dates",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
school_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
exam_name = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
|
||||
exam_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
exam_type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
is_active = table.Column<bool>(type: "boolean", nullable: false),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_exam_dates", x => x.id);
|
||||
table.UniqueConstraint("ak_exam_dates_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_exam_dates_regions_tenant_id_region_id",
|
||||
columns: x => new { x.tenant_id, x.region_id },
|
||||
principalTable: "regions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_exam_dates_schools_tenant_id_school_id",
|
||||
columns: x => new { x.tenant_id, x.school_id },
|
||||
principalTable: "schools",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_exam_dates_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "practice_access_events",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
practice_session_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
event_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
access_mode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
requested_count = table.Column<int>(type: "integer", nullable: false),
|
||||
granted_count = table.Column<int>(type: "integer", nullable: false),
|
||||
consumed_free_quota = table.Column<int>(type: "integer", nullable: false),
|
||||
reason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
scope_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
scope_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
entitlement_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_practice_access_events", x => x.id);
|
||||
table.UniqueConstraint("ak_practice_access_events_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_practice_access_events_consumed_free_quota", "consumed_free_quota >= 0");
|
||||
table.CheckConstraint("ck_practice_access_events_granted_count", "granted_count >= 0");
|
||||
table.CheckConstraint("ck_practice_access_events_requested_count", "requested_count >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_access_events_practice_sessions_tenant_id_practice~",
|
||||
columns: x => new { x.tenant_id, x.practice_session_id },
|
||||
principalTable: "practice_sessions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_access_events_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_access_events_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "practice_daily_usage",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
usage_date = table.Column<DateOnly>(type: "date", nullable: false, defaultValueSql: "current_date"),
|
||||
scope_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
scope_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
free_limit = table.Column<int>(type: "integer", nullable: false),
|
||||
used_count = table.Column<int>(type: "integer", nullable: false),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_practice_daily_usage", x => x.id);
|
||||
table.UniqueConstraint("ak_practice_daily_usage_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_practice_daily_usage_free_limit", "free_limit >= 0");
|
||||
table.CheckConstraint("ck_practice_daily_usage_used_count", "used_count >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_daily_usage_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_daily_usage_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "practice_session_reports",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
practice_session_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
blueprint_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
collection_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
mode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
total_questions = table.Column<int>(type: "integer", nullable: false),
|
||||
answered_count = table.Column<int>(type: "integer", nullable: false),
|
||||
correct_count = table.Column<int>(type: "integer", nullable: false),
|
||||
wrong_count = table.Column<int>(type: "integer", nullable: false),
|
||||
unanswered_count = table.Column<int>(type: "integer", nullable: false),
|
||||
score = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
|
||||
total_score = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
|
||||
accuracy = table.Column<decimal>(type: "numeric(6,4)", precision: 6, scale: 4, nullable: false),
|
||||
duration_seconds = table.Column<int>(type: "integer", nullable: false),
|
||||
started_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
submitted_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
section_stats = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
|
||||
question_results = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
|
||||
wrong_question_ids = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_practice_session_reports", x => x.id);
|
||||
table.UniqueConstraint("ak_practice_session_reports_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_practice_session_reports_accuracy", "accuracy >= 0 and accuracy <= 1");
|
||||
table.CheckConstraint("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.CheckConstraint("ck_practice_session_reports_duration", "duration_seconds >= 0");
|
||||
table.CheckConstraint("ck_practice_session_reports_scores", "score >= 0 and total_score >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_reports_practice_blueprints_tenant_id_blue~",
|
||||
columns: x => new { x.tenant_id, x.blueprint_id },
|
||||
principalTable: "practice_blueprints",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_reports_practice_sessions_tenant_id_practi~",
|
||||
columns: x => new { x.tenant_id, x.practice_session_id },
|
||||
principalTable: "practice_sessions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_reports_question_collections_tenant_id_col~",
|
||||
columns: x => new { x.tenant_id, x.collection_id },
|
||||
principalTable: "question_collections",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_reports_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_reports_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "reports",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
question_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
handled_by = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
category = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
priority = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
handled_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
resolution = table.Column<string>(type: "text", nullable: true),
|
||||
contact = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
attachments = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_reports", x => x.id);
|
||||
table.UniqueConstraint("ak_reports_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_questions_tenant_id_question_id",
|
||||
columns: x => new { x.tenant_id, x.question_id },
|
||||
principalTable: "questions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_users_handled_by",
|
||||
column: x => x.handled_by,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "revenue_daily_stats",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
legacy_region_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
stat_date = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
sale_type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
real_revenue_cents = table.Column<int>(type: "integer", nullable: false),
|
||||
order_count = table.Column<int>(type: "integer", nullable: false),
|
||||
code_count = table.Column<int>(type: "integer", nullable: false),
|
||||
code_used = table.Column<int>(type: "integer", nullable: false),
|
||||
code_estimated = table.Column<int>(type: "integer", nullable: false),
|
||||
estimated_revenue_cents = table.Column<int>(type: "integer", nullable: false),
|
||||
rebuilt_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_revenue_daily_stats", x => x.id);
|
||||
table.UniqueConstraint("ak_revenue_daily_stats_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_revenue_daily_stats_regions_tenant_id_region_id",
|
||||
columns: x => new { x.tenant_id, x.region_id },
|
||||
principalTable: "regions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_revenue_daily_stats_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "user_score_events",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
event_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
points = table.Column<int>(type: "integer", nullable: false),
|
||||
balance_after = table.Column<int>(type: "integer", nullable: false),
|
||||
source_type = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
source_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
idempotency_key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_user_score_events", x => x.id);
|
||||
table.UniqueConstraint("ak_user_score_events_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_user_score_events_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_user_score_events_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "practice_session_report_sections",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
report_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
practice_session_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
section_key = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
section_name = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
question_type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
question_count = table.Column<int>(type: "integer", nullable: false),
|
||||
answered_count = table.Column<int>(type: "integer", nullable: false),
|
||||
correct_count = table.Column<int>(type: "integer", nullable: false),
|
||||
wrong_count = table.Column<int>(type: "integer", nullable: false),
|
||||
unanswered_count = table.Column<int>(type: "integer", nullable: false),
|
||||
score = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
|
||||
total_score = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
|
||||
accuracy = table.Column<decimal>(type: "numeric(6,4)", precision: 6, scale: 4, nullable: false),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_practice_session_report_sections", x => x.id);
|
||||
table.UniqueConstraint("ak_practice_session_report_sections_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_practice_session_report_sections_accuracy", "accuracy >= 0 and accuracy <= 1");
|
||||
table.CheckConstraint("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.CheckConstraint("ck_practice_session_report_sections_scores", "score >= 0 and total_score >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_report_sections_practice_session_reports_t~",
|
||||
columns: x => new { x.tenant_id, x.report_id },
|
||||
principalTable: "practice_session_reports",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_report_sections_practice_sessions_tenant_i~",
|
||||
columns: x => new { x.tenant_id, x.practice_session_id },
|
||||
principalTable: "practice_sessions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_practice_session_report_sections_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "report_status_events",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
report_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
from_status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
to_status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
note = table.Column<string>(type: "text", nullable: true),
|
||||
actor_user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_report_status_events", x => x.id);
|
||||
table.UniqueConstraint("ak_report_status_events_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_report_status_events_reports_tenant_id_report_id",
|
||||
columns: x => new { x.tenant_id, x.report_id },
|
||||
principalTable: "reports",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_report_status_events_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_report_status_events_users_actor_user_id",
|
||||
column: x => x.actor_user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.AddCheckConstraint(
|
||||
name: "ck_practice_sessions_consumed_free_quota",
|
||||
table: "practice_sessions",
|
||||
sql: "consumed_free_quota >= 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_dashboard_daily_stats_tenant_id_legacy_id",
|
||||
table: "dashboard_daily_stats",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_dashboard_daily_stats_tenant_id_region_id",
|
||||
table: "dashboard_daily_stats",
|
||||
columns: new[] { "tenant_id", "region_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_dashboard_daily_stats_tenant_id_stat_date_region_id",
|
||||
table: "dashboard_daily_stats",
|
||||
columns: new[] { "tenant_id", "stat_date", "region_id" },
|
||||
unique: true)
|
||||
.Annotation("Npgsql:NullsDistinct", false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_exam_dates_tenant_id_legacy_id",
|
||||
table: "exam_dates",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_exam_dates_tenant_id_region_id",
|
||||
table: "exam_dates",
|
||||
columns: new[] { "tenant_id", "region_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_exam_dates_tenant_id_school_id_exam_at",
|
||||
table: "exam_dates",
|
||||
columns: new[] { "tenant_id", "school_id", "exam_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_access_events_tenant_id_practice_session_id",
|
||||
table: "practice_access_events",
|
||||
columns: new[] { "tenant_id", "practice_session_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_access_events_tenant_id_user_id_created_at",
|
||||
table: "practice_access_events",
|
||||
columns: new[] { "tenant_id", "user_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_access_events_user_id",
|
||||
table: "practice_access_events",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_daily_usage_tenant_id_user_id_usage_date",
|
||||
table: "practice_daily_usage",
|
||||
columns: new[] { "tenant_id", "user_id", "usage_date" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_daily_usage_tenant_id_user_id_usage_date_scope_typ~",
|
||||
table: "practice_daily_usage",
|
||||
columns: new[] { "tenant_id", "user_id", "usage_date", "scope_type", "scope_id" },
|
||||
unique: true)
|
||||
.Annotation("Npgsql:NullsDistinct", false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_daily_usage_user_id",
|
||||
table: "practice_daily_usage",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_report_sections_tenant_id_practice_session~",
|
||||
table: "practice_session_report_sections",
|
||||
columns: new[] { "tenant_id", "practice_session_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_report_sections_tenant_id_report_id_sectio~",
|
||||
table: "practice_session_report_sections",
|
||||
columns: new[] { "tenant_id", "report_id", "section_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_report_sections_tenant_id_report_id_sort_o~",
|
||||
table: "practice_session_report_sections",
|
||||
columns: new[] { "tenant_id", "report_id", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_reports_tenant_id_blueprint_id_submitted_at",
|
||||
table: "practice_session_reports",
|
||||
columns: new[] { "tenant_id", "blueprint_id", "submitted_at" },
|
||||
filter: "blueprint_id is not null");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_reports_tenant_id_collection_id",
|
||||
table: "practice_session_reports",
|
||||
columns: new[] { "tenant_id", "collection_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_reports_tenant_id_practice_session_id",
|
||||
table: "practice_session_reports",
|
||||
columns: new[] { "tenant_id", "practice_session_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_reports_tenant_id_user_id_submitted_at",
|
||||
table: "practice_session_reports",
|
||||
columns: new[] { "tenant_id", "user_id", "submitted_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_practice_session_reports_user_id",
|
||||
table: "practice_session_reports",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_report_status_events_actor_user_id",
|
||||
table: "report_status_events",
|
||||
column: "actor_user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_report_status_events_tenant_id_report_id_created_at",
|
||||
table: "report_status_events",
|
||||
columns: new[] { "tenant_id", "report_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_handled_by",
|
||||
table: "reports",
|
||||
column: "handled_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_tenant_id_legacy_id",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_tenant_id_question_id_created_at",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "question_id", "created_at" },
|
||||
filter: "question_id is not null");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_tenant_id_status_created_at",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "status", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_user_id",
|
||||
table: "reports",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_revenue_daily_stats_tenant_id_legacy_id",
|
||||
table: "revenue_daily_stats",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_revenue_daily_stats_tenant_id_region_id",
|
||||
table: "revenue_daily_stats",
|
||||
columns: new[] { "tenant_id", "region_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_revenue_daily_stats_tenant_id_stat_date_region_id_sale_type",
|
||||
table: "revenue_daily_stats",
|
||||
columns: new[] { "tenant_id", "stat_date", "region_id", "sale_type" },
|
||||
unique: true)
|
||||
.Annotation("Npgsql:NullsDistinct", false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_score_events_tenant_id_idempotency_key",
|
||||
table: "user_score_events",
|
||||
columns: new[] { "tenant_id", "idempotency_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_score_events_tenant_id_user_id_created_at",
|
||||
table: "user_score_events",
|
||||
columns: new[] { "tenant_id", "user_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_score_events_user_id",
|
||||
table: "user_score_events",
|
||||
column: "user_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "dashboard_daily_stats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "exam_dates");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "practice_access_events");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "practice_daily_usage");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "practice_session_report_sections");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "report_status_events");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "revenue_daily_stats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "user_score_events");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "practice_session_reports");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "reports");
|
||||
|
||||
migrationBuilder.DropCheckConstraint(
|
||||
name: "ck_practice_sessions_consumed_free_quota",
|
||||
table: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_rules",
|
||||
table: "question_collections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_entitlement_id",
|
||||
table: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_mode",
|
||||
table: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_snapshot",
|
||||
table: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "consumed_free_quota",
|
||||
table: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_rules",
|
||||
table: "practice_blueprints");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "access_rules",
|
||||
table: "content_nodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,16 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
|
||||
public DbSet<FavoriteQuestion> FavoriteQuestions => Set<FavoriteQuestion>();
|
||||
public DbSet<WrongQuestion> WrongQuestions => Set<WrongQuestion>();
|
||||
public DbSet<RecentPractice> RecentPractices => Set<RecentPractice>();
|
||||
public DbSet<ExamDate> ExamDates => Set<ExamDate>();
|
||||
public DbSet<Report> Reports => Set<Report>();
|
||||
public DbSet<ReportStatusEvent> ReportStatusEvents => Set<ReportStatusEvent>();
|
||||
public DbSet<UserScoreEvent> UserScoreEvents => Set<UserScoreEvent>();
|
||||
public DbSet<PracticeDailyUsage> PracticeDailyUsages => Set<PracticeDailyUsage>();
|
||||
public DbSet<PracticeAccessEvent> PracticeAccessEvents => Set<PracticeAccessEvent>();
|
||||
public DbSet<PracticeSessionReport> PracticeSessionReports => Set<PracticeSessionReport>();
|
||||
public DbSet<PracticeSessionReportSection> PracticeSessionReportSections => Set<PracticeSessionReportSection>();
|
||||
public DbSet<DashboardDailyStat> DashboardDailyStats => Set<DashboardDailyStat>();
|
||||
public DbSet<RevenueDailyStat> RevenueDailyStats => Set<RevenueDailyStat>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -23,7 +24,7 @@ public sealed class PersistenceModelTests
|
||||
.Select(entity => entity.GetTableName())
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(45, tableNames.Count);
|
||||
Assert.Equal(55, tableNames.Count);
|
||||
Assert.Contains("tenants", tableNames);
|
||||
Assert.Contains("tenant_settings", tableNames);
|
||||
Assert.Contains("content_entries", tableNames);
|
||||
@@ -47,6 +48,16 @@ public sealed class PersistenceModelTests
|
||||
Assert.Contains("app_assets", tableNames);
|
||||
Assert.Contains("video_explanations", tableNames);
|
||||
Assert.Contains("question_videos", tableNames);
|
||||
Assert.Contains("exam_dates", tableNames);
|
||||
Assert.Contains("reports", tableNames);
|
||||
Assert.Contains("report_status_events", tableNames);
|
||||
Assert.Contains("user_score_events", tableNames);
|
||||
Assert.Contains("practice_daily_usage", tableNames);
|
||||
Assert.Contains("practice_access_events", tableNames);
|
||||
Assert.Contains("practice_session_reports", tableNames);
|
||||
Assert.Contains("practice_session_report_sections", tableNames);
|
||||
Assert.Contains("dashboard_daily_stats", tableNames);
|
||||
Assert.Contains("revenue_daily_stats", tableNames);
|
||||
Assert.Contains("questions", tableNames);
|
||||
Assert.Contains("question_versions", tableNames);
|
||||
Assert.Contains("answer_records", tableNames);
|
||||
@@ -296,4 +307,67 @@ public sealed class PersistenceModelTests
|
||||
compositeForeignKeys,
|
||||
properties => Assert.Contains("TenantId", properties));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(ContentNode), nameof(ContentNode.AccessRules), "'{}'::jsonb")]
|
||||
[InlineData(typeof(QuestionCollection), nameof(QuestionCollection.AccessRules), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PracticeBlueprint), nameof(PracticeBlueprint.AccessRules), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PracticeSession), nameof(PracticeSession.AccessSnapshot), "'{}'::jsonb")]
|
||||
[InlineData(typeof(Report), nameof(Report.Attachments), "'[]'::jsonb")]
|
||||
[InlineData(typeof(Report), nameof(Report.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PracticeSessionReport), nameof(PracticeSessionReport.QuestionResults), "'[]'::jsonb")]
|
||||
[InlineData(typeof(PracticeSessionReport), nameof(PracticeSessionReport.WrongQuestionIds), "'[]'::jsonb")]
|
||||
[InlineData(typeof(PracticeSessionReportSection), nameof(PracticeSessionReportSection.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PracticeDailyUsage), nameof(PracticeDailyUsage.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PracticeAccessEvent), nameof(PracticeAccessEvent.Metadata), "'{}'::jsonb")]
|
||||
public void Learning_report_json_properties_are_mapped_to_jsonb(
|
||||
Type entityType,
|
||||
string propertyName,
|
||||
string expectedDefaultValueSql)
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var property = context.Model.FindEntityType(entityType)!
|
||||
.FindProperty(propertyName)!;
|
||||
|
||||
Assert.Equal("jsonb", property.GetColumnType());
|
||||
Assert.Equal(expectedDefaultValueSql, property.GetDefaultValueSql());
|
||||
Assert.Equal(typeof(System.Text.Json.JsonElement), property.ClrType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Practice_usage_unique_scope_treats_nulls_as_not_distinct()
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var index = context.GetService<IDesignTimeModel>()
|
||||
.Model
|
||||
.FindEntityType(typeof(PracticeDailyUsage))!
|
||||
.GetIndexes()
|
||||
.Single(index => index.IsUnique
|
||||
&& index.Properties.Select(property => property.Name).SequenceEqual([
|
||||
nameof(PracticeDailyUsage.TenantId),
|
||||
nameof(PracticeDailyUsage.UserId),
|
||||
nameof(PracticeDailyUsage.UsageDate),
|
||||
nameof(PracticeDailyUsage.ScopeType),
|
||||
nameof(PracticeDailyUsage.ScopeId)
|
||||
]));
|
||||
|
||||
Assert.False(index.GetAreNullsDistinct());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Practice_report_scores_have_expected_precision()
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var reportType = context.Model.FindEntityType(typeof(PracticeSessionReport))!;
|
||||
var score = reportType.FindProperty(nameof(PracticeSessionReport.Score))!;
|
||||
var accuracy = reportType.FindProperty(nameof(PracticeSessionReport.Accuracy))!;
|
||||
|
||||
Assert.Equal(10, score.GetPrecision());
|
||||
Assert.Equal(2, score.GetScale());
|
||||
Assert.Equal(6, accuracy.GetPrecision());
|
||||
Assert.Equal(4, accuracy.GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user